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()