20260819173221
This commit is contained in:
1 parent
1ffc8e1ac9
commit
a0a4273f4c
326 files changed
+301646
-2314
No files matched your search
@@ -0,0 +1,96 @@
|
||||
import { chromium } from 'playwright-core'
|
||||
|
||||
const BASE = 'http://localhost:5082'
|
||||
const chromePath = 'C:/Program Files/Google/Chrome/Application/chrome.exe'
|
||||
const seedStorage = `
|
||||
sessionStorage.setItem('fms-login', JSON.stringify({
|
||||
loginInfo: { token: 'probe-token', user: { id: 1, account: 'admin', name: '探针用户' } },
|
||||
}));
|
||||
localStorage.setItem('fms-user', JSON.stringify({ userInfo: { orgId: 'PROBE', account: 'admin' } }));
|
||||
localStorage.setItem('app', JSON.stringify({ darkMode: false }));
|
||||
`
|
||||
|
||||
const browser = await chromium.launch({
|
||||
executablePath: chromePath,
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu-sandbox'],
|
||||
})
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 800 } })
|
||||
await context.addInitScript(seedStorage)
|
||||
const page = await context.newPage()
|
||||
page.on('pageerror', (e) => console.log('[pageerror]', String(e).slice(0, 400)))
|
||||
page.on('console', (m) => {
|
||||
if (/error/i.test(m.text())) console.log('[console.error]', m.text().slice(0, 300))
|
||||
})
|
||||
|
||||
await page.goto(`${BASE}/dashboard`, { waitUntil: 'domcontentloaded' })
|
||||
await page.waitForSelector('.fms-table')
|
||||
await page.waitForFunction(() => document.querySelectorAll('.stk-tbody-main tr').length > 0)
|
||||
await page.waitForTimeout(1200)
|
||||
|
||||
// 目标 td 信息 + 点击点实际命中的元素
|
||||
const info = await page.evaluate(() => {
|
||||
const root = document.querySelector('.fms-table')
|
||||
const trs = [...root.querySelectorAll('.stk-tbody-main tr')]
|
||||
const out = {
|
||||
trCount: trs.length,
|
||||
trClasses: trs.map((tr) => tr.className),
|
||||
firstTrHTML: trs[0]?.outerHTML.slice(0, 300),
|
||||
secondTrHTML: trs[1]?.outerHTML.slice(0, 300),
|
||||
}
|
||||
const td = trs[1]?.querySelector('td[data-col-key]')
|
||||
if (td) {
|
||||
const r = td.getBoundingClientRect()
|
||||
const hit = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2)
|
||||
out.td = {
|
||||
colKey: td.getAttribute('data-col-key'),
|
||||
rect: { left: r.left, top: r.top, w: r.width, h: r.height },
|
||||
hit: hit ? `${hit.tagName}.${hit.className}` : null,
|
||||
tdClass: td.className,
|
||||
inner: td.innerHTML.slice(0, 200),
|
||||
}
|
||||
} else {
|
||||
out.td = null
|
||||
}
|
||||
// StkTable 选区容器信息
|
||||
const viewportEls = [...root.querySelectorAll('*')].filter(
|
||||
(el) => el.scrollHeight > el.clientHeight + 2,
|
||||
)
|
||||
out.scrollers = viewportEls.map((el) => ({
|
||||
cls: el.className,
|
||||
sh: el.scrollHeight,
|
||||
ch: el.clientHeight,
|
||||
}))
|
||||
return out
|
||||
})
|
||||
console.log(JSON.stringify(info, null, 2))
|
||||
|
||||
// 用真实鼠标点击
|
||||
const tdInfo = await page.evaluate(() => {
|
||||
const root = document.querySelector('.fms-table')
|
||||
const trs = root.querySelectorAll('.stk-tbody-main tr')
|
||||
const td = trs[1]?.querySelector('td[data-col-key]')
|
||||
if (!td) return null
|
||||
const r = td.getBoundingClientRect()
|
||||
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
||||
})
|
||||
if (tdInfo) {
|
||||
console.log('\n点击坐标:', tdInfo)
|
||||
await page.mouse.click(tdInfo.x, tdInfo.y)
|
||||
await page.waitForTimeout(800)
|
||||
const after = await page.evaluate(() => {
|
||||
const root = document.querySelector('.fms-table')
|
||||
return {
|
||||
editors: root.querySelectorAll('.fms-cell-editor-root').length,
|
||||
editorInputs: root.querySelectorAll('.fms-cell-editor-root input').length,
|
||||
csSel: root.querySelectorAll('[data-cs-s]').length,
|
||||
selectedCells: root.querySelectorAll('.stk-cell-selected, [data-cs-s="selected"]').length,
|
||||
activeElement: document.activeElement
|
||||
? `${document.activeElement.tagName}.${document.activeElement.className}`
|
||||
: null,
|
||||
}
|
||||
})
|
||||
console.log('点击后状态:', JSON.stringify(after, null, 2))
|
||||
}
|
||||
|
||||
await browser.close()
|
||||
@@ -0,0 +1,217 @@
|
||||
import { chromium } from 'playwright-core'
|
||||
|
||||
const BASE = process.env.FMS_BASE || 'http://localhost:5082'
|
||||
const SCROLL_MS = Number(process.env.SCROLL_MS || 3500)
|
||||
const STEP = Number(process.env.STEP || 120)
|
||||
const ROUNDS = Number(process.env.ROUNDS || 2)
|
||||
const HEADED = process.env.HEADED === '1'
|
||||
// 场景顺序固定:idle/scroll 在前(编辑态会污染后续场景),edit 永远最后
|
||||
const SCENES = (process.env.SCENES || 'idle,scroll,scroll-hidden,edit').split(',')
|
||||
|
||||
const chromePath =
|
||||
process.env.CHROME_PATH ||
|
||||
'C:/Program Files/Google/Chrome/Application/chrome.exe'
|
||||
|
||||
const seedStorage = `
|
||||
sessionStorage.setItem('fms-login', JSON.stringify({
|
||||
loginInfo: { token: 'probe-token', user: { id: 1, account: 'admin', name: '探针用户' } },
|
||||
}));
|
||||
localStorage.setItem('fms-user', JSON.stringify({ userInfo: { orgId: 'PROBE', account: 'admin' } }));
|
||||
localStorage.setItem('app', JSON.stringify({ darkMode: false }));
|
||||
`
|
||||
|
||||
async function measure(page, { scene, durationMs }) {
|
||||
// 编辑场景:用 Playwright 真实鼠标点击(带真实坐标),StkTable 选区需要真实事件
|
||||
if (scene === 'edit') {
|
||||
const rect = await page.evaluate(() => {
|
||||
const root = document.querySelector('.fms-table')
|
||||
const trs = root.querySelectorAll('.stk-tbody-main tr')
|
||||
// 跳过序号列(data-col-key="")与 vt-x 占位列,点真实数据列
|
||||
const td = [...trs[1]?.querySelectorAll('td[data-col-key]')].find(
|
||||
(el) => el.getAttribute('data-col-key') !== '',
|
||||
)
|
||||
if (!td) return null
|
||||
const r = td.getBoundingClientRect()
|
||||
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
||||
})
|
||||
if (!rect) throw new Error('no target td for edit')
|
||||
await page.mouse.click(rect.x, rect.y)
|
||||
await page.waitForTimeout(500)
|
||||
const mounted = await page.evaluate(
|
||||
() =>
|
||||
document.querySelectorAll(
|
||||
'.fms-cell-editor-root input:not([type=checkbox]):not([type=hidden]), .fms-cell-editor-root textarea, .fms-cell-editor-root .select',
|
||||
).length > 0,
|
||||
)
|
||||
if (!mounted) throw new Error('editor did not mount after real click')
|
||||
}
|
||||
|
||||
return page.evaluate(
|
||||
async ({ scene, durationMs, step }) => {
|
||||
const root = document.querySelector('.fms-table')
|
||||
if (!root) throw new Error('no .fms-table')
|
||||
const els = [...root.querySelectorAll('*')]
|
||||
const scrollers = els.filter(
|
||||
(el) => el.scrollHeight > el.clientHeight + 2 && getComputedStyle(el).overflowY !== 'hidden',
|
||||
)
|
||||
const scroller = scrollers.sort(
|
||||
(a, b) => b.scrollHeight - b.clientHeight - (a.scrollHeight - a.clientHeight),
|
||||
)[0]
|
||||
if (!scroller) throw new Error('no vertical scroller')
|
||||
const maxScroll = scroller.scrollHeight - scroller.clientHeight
|
||||
const driveScroll = scene === 'scroll' || scene === 'scroll-hidden' || scene === 'edit'
|
||||
|
||||
// scroll-hidden:跳过 paint(保留布局与 JS 链路),隔离渲染 vs JS 开销
|
||||
if (scene === 'scroll-hidden') {
|
||||
root.style.visibility = 'hidden'
|
||||
}
|
||||
|
||||
scroller.scrollTop = 0
|
||||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)))
|
||||
|
||||
if (driveScroll) {
|
||||
for (let i = 0; i < 60; i++) {
|
||||
scroller.scrollTop = (scroller.scrollTop + step) % (maxScroll + 1)
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
}
|
||||
scroller.scrollTop = 0
|
||||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)))
|
||||
}
|
||||
|
||||
let frames = 0
|
||||
let minGap = Infinity
|
||||
let maxGap = 0
|
||||
let prev = performance.now()
|
||||
const start = performance.now()
|
||||
await new Promise((resolve) => {
|
||||
const tick = (now) => {
|
||||
const gap = now - prev
|
||||
prev = now
|
||||
frames++
|
||||
if (gap < minGap) minGap = gap
|
||||
if (gap > maxGap) maxGap = gap
|
||||
if (driveScroll) {
|
||||
if (scroller.scrollTop + step < maxScroll) scroller.scrollTop += step
|
||||
else scroller.scrollTop = 0
|
||||
}
|
||||
if (now - start < durationMs) requestAnimationFrame(tick)
|
||||
else resolve()
|
||||
}
|
||||
requestAnimationFrame(tick)
|
||||
})
|
||||
const elapsed = (performance.now() - start) / 1000
|
||||
const fps = frames / elapsed
|
||||
const editorCount = root.querySelectorAll(
|
||||
'.fms-cell-editor-root input:not([type=checkbox]):not([type=hidden]), .fms-cell-editor-root textarea, .fms-cell-editor-root .select',
|
||||
).length
|
||||
const displayCount = root.querySelectorAll('.fms-cell-display').length
|
||||
const nativeCellCount = root.querySelectorAll('.stk-tbody-main .table-cell-wrapper').length
|
||||
return {
|
||||
scene,
|
||||
fps: Math.round(fps * 10) / 10,
|
||||
avgFrameMs: Math.round(((elapsed * 1000) / frames) * 10) / 10,
|
||||
minGapMs: Math.round(minGap),
|
||||
maxGapMs: Math.round(maxGap),
|
||||
renderedRows: root.querySelectorAll('.stk-tbody-main tr').length,
|
||||
editorCount,
|
||||
displayCount,
|
||||
nativeCellCount,
|
||||
maxScroll: Math.round(maxScroll),
|
||||
}
|
||||
},
|
||||
{ scene, durationMs, step: STEP },
|
||||
)
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({
|
||||
executablePath: chromePath,
|
||||
headless: !HEADED,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu-sandbox',
|
||||
'--disable-background-timer-throttling',
|
||||
],
|
||||
})
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 800 } })
|
||||
await context.addInitScript(seedStorage)
|
||||
const page = await context.newPage()
|
||||
page.on('pageerror', (err) => console.log('[pageerror]', String(err).slice(0, 300)))
|
||||
|
||||
console.log(`打开 ${BASE}/dashboard ... (headless=${!HEADED})`)
|
||||
await page.goto(`${BASE}/dashboard`, { waitUntil: 'domcontentloaded', timeout: 30000 })
|
||||
await page.waitForSelector('.fms-table', { timeout: 20000 })
|
||||
await page.waitForFunction(() => document.querySelectorAll('.stk-tbody-main tr').length > 0)
|
||||
await page.waitForTimeout(1200)
|
||||
console.log(`页面: ${await page.title()} | ${page.url()}`)
|
||||
|
||||
// 空页面 rAF 基准(headless 上限)
|
||||
try {
|
||||
const base = await page.evaluate(async ({ durationMs }) => {
|
||||
let frames = 0
|
||||
const start = performance.now()
|
||||
await new Promise((resolve) => {
|
||||
const tick = (now) => {
|
||||
frames++
|
||||
if (now - start < durationMs) requestAnimationFrame(tick)
|
||||
else resolve()
|
||||
}
|
||||
requestAnimationFrame(tick)
|
||||
})
|
||||
return { fps: Math.round((frames / ((performance.now() - start) / 1000)) * 10) / 10 }
|
||||
}, { durationMs: 2000 })
|
||||
console.log(`[同页 rAF 上限基准] FPS=${base.fps}`)
|
||||
} catch (e) {
|
||||
console.log('rAF 基准跳过:', String(e).slice(0, 120))
|
||||
}
|
||||
|
||||
const results = {}
|
||||
for (let round = 1; round <= ROUNDS; round++) {
|
||||
console.log(`\n===== 第 ${round}/${ROUNDS} 轮 =====`)
|
||||
for (const scene of SCENES) {
|
||||
const r = await measure(page, { scene, durationMs: SCROLL_MS })
|
||||
const tag = {
|
||||
idle: '静止',
|
||||
scroll: '滚动',
|
||||
'scroll-hidden': '滚动(隐藏paint)',
|
||||
edit: '编辑中滚动',
|
||||
}[scene] || scene
|
||||
console.log(
|
||||
`[${tag}] FPS=${r.fps} avgFrame=${r.avgFrameMs}ms minGap=${r.minGapMs} maxGap=${r.maxGapMs} ` +
|
||||
`rows=${r.renderedRows} 原生格=${r.nativeCellCount} 编辑器=${r.editorCount} 展示格=${r.displayCount}`,
|
||||
)
|
||||
;(results[scene] ??= []).push(r.fps)
|
||||
// 恢复可见性并回顶
|
||||
await page.evaluate(() => {
|
||||
const root = document.querySelector('.fms-table')
|
||||
if (root) root.style.visibility = ''
|
||||
const els = root ? [...root.querySelectorAll('*')] : []
|
||||
const sc = els
|
||||
.filter((el) => el.scrollHeight > el.clientHeight + 2)
|
||||
.sort((a, b) => b.scrollHeight - b.clientHeight - (a.scrollHeight - a.clientHeight))[0]
|
||||
if (sc) sc.scrollTop = 0
|
||||
})
|
||||
await page.waitForTimeout(300)
|
||||
}
|
||||
// 每轮结束重载页面,清掉编辑态/滚动位置,避免污染下一轮
|
||||
if (round < ROUNDS) {
|
||||
await page.reload({ waitUntil: 'domcontentloaded' })
|
||||
await page.waitForSelector('.fms-table', { timeout: 20000 })
|
||||
await page.waitForFunction(() => document.querySelectorAll('.stk-tbody-main tr').length > 0)
|
||||
await page.waitForTimeout(1200)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n===== 汇总(FPS) =====')
|
||||
for (const [scene, list] of Object.entries(results)) {
|
||||
const avg = list.reduce((a, b) => a + b, 0) / list.length
|
||||
const tag = {
|
||||
idle: '静止',
|
||||
scroll: '滚动',
|
||||
'scroll-hidden': '滚动(隐藏paint)',
|
||||
edit: '编辑中滚动',
|
||||
}[scene] || scene
|
||||
console.log(`${tag}: ${list.join(' / ')} → 平均 ${Math.round(avg * 10) / 10}`)
|
||||
}
|
||||
|
||||
await browser.close()
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "fps-probe", "private": true, "type": "module" }
|
||||
@@ -0,0 +1,23 @@
|
||||
# 切换 fms-table cell 渲染实现用于 A/B 测速
|
||||
# 用法: .\swap-cellrenderer.ps1 <old|new>
|
||||
$target = 'fms-vue\src\components\fms-table\utils\createCellRenderer.js'
|
||||
$backup = 'fms-vue\src\components\fms-table\utils\createCellRenderer.js.current.bak'
|
||||
$old = 'fms-vue\src\components\fms-table\utils\createCellRenderer.old.js'
|
||||
|
||||
if (-not (Test-Path $target)) { throw "missing $target" }
|
||||
|
||||
switch ($args[0]) {
|
||||
'old' {
|
||||
if (-not (Test-Path $backup)) { Copy-Item $target $backup }
|
||||
Copy-Item $old $target -Force
|
||||
Write-Host '→ 已切换到【旧版】(所有列注入 customCell)'
|
||||
}
|
||||
'new' {
|
||||
if (-not (Test-Path $backup)) { throw "no backup: $backup" }
|
||||
Copy-Item $backup $target -Force
|
||||
Write-Host '→ 已还原【新版】(纯展示列走原生 td)'
|
||||
}
|
||||
default {
|
||||
Write-Host '用法: .\swap-cellrenderer.ps1 <old|new>'
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user