20260820173056
This commit is contained in:
1 parent
8e75c7989a
commit
68ac5db038
24 files changed
+975
-6035
No files matched your search
@@ -0,0 +1,120 @@
|
||||
import { chromium } from 'playwright-core'
|
||||
|
||||
// JS 派发 wheel 事件驱动滚动(与真实滚轮走同一 onTableWheel → scrollTop += deltaY 路径,
|
||||
// 不受 CDP 输入合并影响;transform 滚动模式下 DOM scrollTop 恒为 0 也可用)
|
||||
const BASE = process.env.FMS_BASE || 'http://localhost:5082'
|
||||
const DURATION_MS = Number(process.env.DURATION_MS || 3000)
|
||||
const WHEEL_DELTA = Number(process.env.WHEEL_DELTA || 100)
|
||||
const WHEEL_INTERVAL_MS = Number(process.env.WHEEL_INTERVAL_MS || 8)
|
||||
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 }));
|
||||
`
|
||||
|
||||
const browser = await chromium.launch({
|
||||
executablePath: chromePath,
|
||||
headless: process.env.HEADED !== '1',
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu-sandbox',
|
||||
...(process.env.HEADED === '1' ? ['--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 ...`)
|
||||
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)
|
||||
|
||||
async function measure(durationMs, drive) {
|
||||
return page.evaluate(
|
||||
async ({ durationMs, drive, delta, intervalMs }) => {
|
||||
const root = document.querySelector('.fms-table')
|
||||
// wheel 监听器绑定在 .stk-table 滚动容器上;transform 模式下无纵向溢出,直接用容器派发
|
||||
const sc =
|
||||
[...root.querySelectorAll('*')].filter(
|
||||
(el) => el.scrollHeight > el.clientHeight + 2 && getComputedStyle(el).overflowY !== 'hidden',
|
||||
)[0] || root.querySelector('.stk-table')
|
||||
const maxScroll = sc ? sc.scrollHeight - sc.clientHeight : 0
|
||||
if (sc) sc.scrollTop = 0
|
||||
|
||||
let wheelEvents = 0
|
||||
let scrollEvents = 0
|
||||
let frames = 0
|
||||
let minGap = Infinity
|
||||
let maxGap = 0
|
||||
let prev = performance.now()
|
||||
const start = performance.now()
|
||||
|
||||
const scrollListener = () => scrollEvents++
|
||||
if (sc) sc.addEventListener('scroll', scrollListener)
|
||||
|
||||
await new Promise((resolve) => {
|
||||
const tick = (now) => {
|
||||
// 每帧同步派发滚轮事件(不受 setInterval 节流影响),等价于高频真实滚轮
|
||||
if (drive === 'wheel' && sc) {
|
||||
sc.dispatchEvent(
|
||||
new WheelEvent('wheel', { deltaY: delta, deltaMode: 0, cancelable: true, bubbles: true }),
|
||||
)
|
||||
wheelEvents++
|
||||
}
|
||||
const gap = now - prev
|
||||
prev = now
|
||||
frames++
|
||||
if (gap < minGap) minGap = gap
|
||||
if (gap > maxGap) maxGap = gap
|
||||
if (now - start < durationMs) requestAnimationFrame(tick)
|
||||
else resolve()
|
||||
}
|
||||
requestAnimationFrame(tick)
|
||||
})
|
||||
if (sc) sc.removeEventListener('scroll', scrollListener)
|
||||
const elapsed = (performance.now() - start) / 1000
|
||||
|
||||
const tbodyTransform = root.querySelector('.stk-tbody-main')?.style.transform || ''
|
||||
return {
|
||||
scene: drive,
|
||||
fps: Math.round((frames / elapsed) * 10) / 10,
|
||||
avgFrameMs: Math.round(((elapsed * 1000) / frames) * 10) / 10,
|
||||
minGapMs: Math.round(minGap),
|
||||
maxGapMs: Math.round(maxGap),
|
||||
wheelEvents,
|
||||
scrollEvents,
|
||||
scrollTop: sc?.scrollTop ?? 0,
|
||||
renderedRows: root.querySelectorAll('.stk-tbody-main tr').length,
|
||||
customThumb: root.querySelectorAll('.stk-sb-thumb.vertical').length,
|
||||
tbodyTransform,
|
||||
}
|
||||
},
|
||||
{ durationMs, drive, delta: WHEEL_DELTA, intervalMs: WHEEL_INTERVAL_MS },
|
||||
)
|
||||
}
|
||||
|
||||
for (let round = 1; round <= 2; round++) {
|
||||
console.log(`\n===== 第 ${round}/2 轮 =====`)
|
||||
const idle = await measure(1200, 'idle')
|
||||
console.log(
|
||||
`[静止] FPS=${idle.fps} avgFrame=${idle.avgFrameMs}ms rows=${idle.renderedRows} thumb=${idle.customThumb}`,
|
||||
)
|
||||
const scroll = await measure(DURATION_MS, 'wheel')
|
||||
console.log(
|
||||
`[滚轮] FPS=${scroll.fps} avgFrame=${scroll.avgFrameMs}ms minGap=${scroll.minGapMs} maxGap=${scroll.maxGapMs} ` +
|
||||
`wheelEvt=${scroll.wheelEvents} scrollEvt=${scroll.scrollEvents} scrollTop=${scroll.scrollTop} ` +
|
||||
`rows=${scroll.renderedRows} thumb=${scroll.customThumb} transform=${scroll.tbodyTransform.slice(0, 40)}`,
|
||||
)
|
||||
await page.waitForTimeout(400)
|
||||
}
|
||||
|
||||
await browser.close()
|
||||
@@ -0,0 +1,125 @@
|
||||
import { chromium } from 'playwright-core'
|
||||
|
||||
const BASE = process.env.FMS_BASE || 'http://localhost:5082'
|
||||
const SCROLL_MS = Number(process.env.SCROLL_MS || 3000)
|
||||
const ROUNDS = Number(process.env.ROUNDS || 2)
|
||||
const WHEEL_INTERVAL = Number(process.env.WHEEL_INTERVAL || 10) // ms 每次滚轮间隔(模拟快速滚轮)
|
||||
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 }));
|
||||
`
|
||||
|
||||
const browser = await chromium.launch({
|
||||
executablePath: chromePath,
|
||||
headless: true,
|
||||
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 ...`)
|
||||
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)
|
||||
|
||||
async function measure(durationMs) {
|
||||
// 把鼠标移到表格上,确保滚轮事件落在表格滚动容器上
|
||||
const rect = await page.evaluate(() => {
|
||||
const root = document.querySelector('.fms-table')
|
||||
const r = root?.getBoundingClientRect()
|
||||
return r ? { x: r.left + r.width / 2, y: r.top + Math.min(200, r.height / 2) } : null
|
||||
})
|
||||
if (rect) await page.mouse.move(rect.x, rect.y)
|
||||
|
||||
// 用真实滚轮事件驱动(transform 模式下 DOM scrollTop 恒为 0,程序化 scrollTop 无效)
|
||||
const wheelDriver = (async () => {
|
||||
const end = Date.now() + durationMs
|
||||
while (Date.now() < end) {
|
||||
await page.mouse.wheel(0, 100)
|
||||
await page.waitForTimeout(WHEEL_INTERVAL)
|
||||
}
|
||||
})()
|
||||
|
||||
const result = await page.evaluate(
|
||||
async ({ durationMs }) => {
|
||||
let frames = 0
|
||||
let minGap = Infinity
|
||||
let maxGap = 0
|
||||
let prev = performance.now()
|
||||
const start = performance.now()
|
||||
const root0 = document.querySelector('.fms-table')
|
||||
const sc0 = root0
|
||||
? [...root0.querySelectorAll('*')].filter((el) => el.scrollHeight > el.clientHeight + 2)[0]
|
||||
: null
|
||||
const scrollTop0 = sc0?.scrollTop ?? 0
|
||||
const vs0 = window.__vs0
|
||||
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 (now - start < durationMs) requestAnimationFrame(tick)
|
||||
else resolve()
|
||||
}
|
||||
requestAnimationFrame(tick)
|
||||
})
|
||||
const elapsed = (performance.now() - start) / 1000
|
||||
const root = document.querySelector('.fms-table')
|
||||
const scroller = root
|
||||
? [...root.querySelectorAll('*')].filter(
|
||||
(el) => el.scrollHeight > el.clientHeight + 2,
|
||||
)[0]
|
||||
: null
|
||||
return {
|
||||
fps: Math.round((frames / elapsed) * 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 ?? 0,
|
||||
customThumb: root?.querySelectorAll('.stk-sb-thumb.vertical').length ?? 0,
|
||||
scrollerOverflow: scroller ? getComputedStyle(scroller).overflowY : null,
|
||||
scrolledBy: (scroller?.scrollTop ?? 0) - scrollTop0,
|
||||
}
|
||||
},
|
||||
{ durationMs },
|
||||
)
|
||||
await wheelDriver
|
||||
return result
|
||||
}
|
||||
|
||||
for (let round = 1; round <= ROUNDS; round++) {
|
||||
console.log(`\n===== 第 ${round}/${ROUNDS} 轮 =====`)
|
||||
const idle = await measure(1200)
|
||||
console.log(`[静止] FPS=${idle.fps} avgFrame=${idle.avgFrameMs}ms rows=${idle.renderedRows} thumb=${idle.customThumb}`)
|
||||
const scroll = await measure(SCROLL_MS)
|
||||
console.log(
|
||||
`[滚轮滚动] FPS=${scroll.fps} avgFrame=${scroll.avgFrameMs}ms minGap=${scroll.minGapMs} maxGap=${scroll.maxGapMs} ` +
|
||||
`rows=${scroll.renderedRows} thumb=${scroll.customThumb} overflow=${scroll.scrollerOverflow}`,
|
||||
)
|
||||
await page.evaluate(() => {
|
||||
const root = document.querySelector('.fms-table')
|
||||
const els = root ? [...root.querySelectorAll('*')] : []
|
||||
const sc = els.filter((el) => el.scrollHeight > el.clientHeight + 2)[0]
|
||||
if (sc) sc.scrollTop = 0
|
||||
})
|
||||
await page.waitForTimeout(400)
|
||||
}
|
||||
|
||||
await browser.close()
|
||||
@@ -0,0 +1,180 @@
|
||||
import { chromium } from 'playwright-core'
|
||||
|
||||
const BASE = process.env.FMS_BASE || 'http://localhost:5082'
|
||||
const DURATION_MS = Number(process.env.DURATION_MS || 2500)
|
||||
const STEP = Number(process.env.STEP || 120)
|
||||
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 }));
|
||||
`
|
||||
|
||||
const browser = await chromium.launch({
|
||||
executablePath: chromePath,
|
||||
headless: process.env.HEADED !== '1',
|
||||
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)))
|
||||
|
||||
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)
|
||||
|
||||
const cdp = await context.newCDPSession(page)
|
||||
|
||||
// 预热:driver='scrollTop' 程序化滚动(padding 模式)/ 'wheel' 每帧派发滚轮(transform 模式)
|
||||
const DRIVE = process.env.DRIVE || 'scrollTop'
|
||||
|
||||
async function warmup(drive) {
|
||||
await page.evaluate(async ({ drive }) => {
|
||||
const root = document.querySelector('.fms-table')
|
||||
const els = [...root.querySelectorAll('*')]
|
||||
const scroller = els
|
||||
.filter((el) => el.scrollHeight > el.clientHeight + 2 && getComputedStyle(el).overflowY !== 'hidden')
|
||||
.sort((a, b) => b.scrollHeight - b.clientHeight - (a.scrollHeight - a.clientHeight))[0] ||
|
||||
root.querySelector('.stk-table')
|
||||
window.__scroller = scroller
|
||||
window.__maxScroll = scroller ? scroller.scrollHeight - scroller.clientHeight : 0
|
||||
if (scroller) scroller.scrollTop = 0
|
||||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)))
|
||||
const start = performance.now()
|
||||
await new Promise((resolve) => {
|
||||
const tick = (now) => {
|
||||
if (drive === 'wheel') {
|
||||
scroller.dispatchEvent(
|
||||
new WheelEvent('wheel', { deltaY: 100, deltaMode: 0, cancelable: true, bubbles: true }),
|
||||
)
|
||||
} else if (scroller.scrollTop + 120 < window.__maxScroll) {
|
||||
scroller.scrollTop += 120
|
||||
} else {
|
||||
scroller.scrollTop = 0
|
||||
}
|
||||
if (now - start < 800) requestAnimationFrame(tick)
|
||||
else resolve()
|
||||
}
|
||||
requestAnimationFrame(tick)
|
||||
})
|
||||
}, { drive })
|
||||
}
|
||||
|
||||
await warmup(DRIVE)
|
||||
|
||||
const collected = []
|
||||
let complete = false
|
||||
cdp.on('Tracing.dataCollected', (event) => {
|
||||
if (event.value && Array.isArray(event.value)) collected.push(...event.value)
|
||||
})
|
||||
cdp.on('Tracing.tracingComplete', () => {
|
||||
complete = true
|
||||
})
|
||||
await cdp.send('Tracing.start', {
|
||||
transferMode: 'ReportEvents',
|
||||
traceConfig: { included_categories: ['devtools.timeline', 'v8.execute', 'blink.user_timing'] },
|
||||
})
|
||||
console.log('tracing started, driving scroll ...')
|
||||
|
||||
const drivePromise = page.evaluate(async ({ step, durationMs, drive }) => {
|
||||
const scroller = window.__scroller || document.querySelector('.stk-table')
|
||||
const start = performance.now()
|
||||
await new Promise((resolve) => {
|
||||
const tick = (now) => {
|
||||
if (drive === 'wheel') {
|
||||
scroller.dispatchEvent(
|
||||
new WheelEvent('wheel', { deltaY: 100, deltaMode: 0, cancelable: true, bubbles: true }),
|
||||
)
|
||||
} else if (scroller.scrollTop + step < window.__maxScroll) {
|
||||
scroller.scrollTop += step
|
||||
} else {
|
||||
scroller.scrollTop = 0
|
||||
}
|
||||
if (now - start < durationMs) requestAnimationFrame(tick)
|
||||
else resolve()
|
||||
}
|
||||
requestAnimationFrame(tick)
|
||||
})
|
||||
}, { step: STEP, durationMs: DURATION_MS, drive: DRIVE })
|
||||
|
||||
await drivePromise
|
||||
await cdp.send('Tracing.end')
|
||||
// 等待 dataCollected 批次送达
|
||||
for (let i = 0; i < 20 && !complete; i++) await page.waitForTimeout(250)
|
||||
await page.waitForTimeout(500)
|
||||
console.log(`tracing ended, collected ${collected.length} events (complete=${complete})`)
|
||||
|
||||
const events = collected
|
||||
|
||||
// 按事件名聚合:count / totalDur / selfTime(总时长 - 子事件时长)
|
||||
const agg = new Map()
|
||||
const byPhase = new Map()
|
||||
for (const ev of events) {
|
||||
if (!ev.name || !ev.dur) continue
|
||||
const key = ev.name
|
||||
const a = agg.get(key) || { count: 0, total: 0, self: 0 }
|
||||
a.count++
|
||||
a.total += ev.dur
|
||||
agg.set(key, a)
|
||||
}
|
||||
// selfTime:父事件 dur 减去子事件 dur(简化:同一线程内嵌套)
|
||||
const childrenByParent = new Map()
|
||||
for (const ev of events) {
|
||||
if (!ev.dur) continue
|
||||
const key = `${ev.pid}:${ev.tid}`
|
||||
const list = childrenByParent.get(key) || []
|
||||
list.push(ev)
|
||||
childrenByParent.set(key, list)
|
||||
}
|
||||
for (const [key, list] of childrenByParent) {
|
||||
list.sort((a, b) => a.ts - b.ts)
|
||||
// 每个事件查找其直接父(最近的前驱且覆盖其区间)
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const ev = list[i]
|
||||
if (!ev.dur) continue
|
||||
const end = ev.ts + ev.dur
|
||||
let self = ev.dur
|
||||
for (let j = i + 1; j < list.length; j++) {
|
||||
const c = list[j]
|
||||
if (c.ts >= end) break
|
||||
if (c.ts >= ev.ts && c.ts + (c.dur || 0) <= end) self -= c.dur || 0
|
||||
}
|
||||
ev.__self = Math.max(0, self)
|
||||
}
|
||||
}
|
||||
|
||||
const selfAgg = new Map()
|
||||
for (const ev of events) {
|
||||
if (!ev.dur || ev.__self == null) continue
|
||||
const a = selfAgg.get(ev.name) || { count: 0, self: 0 }
|
||||
a.count++
|
||||
a.self += ev.__self
|
||||
selfAgg.set(ev.name, a)
|
||||
}
|
||||
|
||||
const top = [...selfAgg.entries()].sort((a, b) => b[1].self - a[1].self).slice(0, 25)
|
||||
console.log('\n===== 按事件名 selfTime 汇总 (μs) =====')
|
||||
for (const [name, a] of top) {
|
||||
console.log(`${name.padEnd(48)} count=${String(a.count).padStart(6)} self=${(a.self / 1000).toFixed(1)}ms`)
|
||||
}
|
||||
|
||||
// 帧/合成相关事件计数
|
||||
const frameEvents = ['BeginFrame', 'DrawFrame', 'UpdateLayoutTree', 'Layout', 'Paint', 'RasterTask', 'CompositeLayers', 'Commit']
|
||||
console.log('\n===== 渲染相关事件 =====')
|
||||
for (const name of frameEvents) {
|
||||
const a = agg.get(name)
|
||||
if (a) console.log(`${name.padEnd(24)} count=${a.count} total=${(a.total / 1000).toFixed(1)}ms`)
|
||||
}
|
||||
|
||||
await browser.close()
|
||||
Reference in new issue
Block a user