This commit is contained in:
oneao committed 2026-10-16 17:30:17 +08:00
1 parent e7c84a82a2
commit 025d3232c4
65 files changed
+9838 -9111

No files matched your search

@@ -0,0 +1,254 @@
import { chromium } from '@playwright/test'
/**
* /table-test 性能探针 v4 —— 归因实验(只读诊断,不改业务代码)。
*
* v3 已确认:静止 61 FPS,纵向滚动 ~13 FPS。本版要回答「74ms/帧花在哪」,
* 用**受控对照**而不是继续猜:
*
* A 现状 基线
* B 去掉 validate/precision → 常驻 customCell 的列少 4 个
* C 去掉全部 editor → 一个 customCell 都不挂(性能上限参考)
* D 数据量 10 / 100 / 1000 → 判断开销是 O(可见格) 还是 O(总行数)
*
* 说明:B/C 是在**探针浏览器里**原地改内存中的列定义,用于归因;不改仓库代码。
*/
const BASE = process.env.PROBE_URL || 'http://127.0.0.1:5090'
const MENU_CODE = 'menu.table_test'
async function makePage(extraQuery = '') {
const browser = await chromium.launch()
const context = await browser.newContext({ viewport: { width: 1600, height: 900 } })
await context.route('**/api/data/loaddata', async (route) => {
let viewName = ''
try {
viewName = JSON.parse(route.request().postData() || '').view_name || ''
} catch {
/* ignore */
}
const response = await route.fetch()
if (viewName !== 's_power' && viewName !== 's_user_power') return route.fulfill({ response })
let json
try {
json = await response.json()
} catch {
return route.fulfill({ response })
}
const rows = Array.isArray(json?.data) ? json.data : []
if (viewName === 's_power') {
if (!rows.some((r) => r.b_id === MENU_CODE)) rows.push({ b_id: MENU_CODE, b_canuse: 1 })
} else if (!rows.some((r) => r.b_power_id === MENU_CODE)) {
rows.push({ b_user_id: 'demo_zhangwei', b_power_id: MENU_CODE, b_canuse: 1 })
}
json.data = rows
await route.fulfill({
response,
body: JSON.stringify(json),
headers: { ...response.headers(), 'content-type': 'application/json' },
})
})
const page = await context.newPage()
page.on('pageerror', (e) => console.log('[pageerror]', String(e).slice(0, 200)))
await page.goto(`${BASE}/table-test${extraQuery}`, { waitUntil: 'domcontentloaded' })
await page.waitForSelector('.stk-table', { timeout: 90000 })
await page.waitForTimeout(2500)
return { browser, context, page }
}
/** 滚动测量:真正驱动 .stk-table 的 scrollTop */
async function scrollFps(page, { axis = 'y', steps = 140, px = 60 } = {}) {
return page.evaluate(
async ({ axis, steps, px }) => {
const cont = document.querySelector('.stk-table')
const frames = []
let last = performance.now()
let stop = false
const tick = () => {
const now = performance.now()
frames.push(now - last)
last = now
if (!stop) requestAnimationFrame(tick)
}
requestAnimationFrame(tick)
const sync = []
for (let i = 0; i < steps; i += 1) {
await new Promise((r) => requestAnimationFrame(r))
const t0 = performance.now()
if (axis === 'y') {
const max = cont.scrollHeight - cont.clientHeight
cont.scrollTop = cont.scrollTop >= max - 1 ? 0 : Math.min(max, cont.scrollTop + px)
} else {
const max = cont.scrollWidth - cont.clientWidth
cont.scrollLeft = cont.scrollLeft >= max - 1 ? 0 : Math.min(max, cont.scrollLeft + px)
}
sync.push(performance.now() - t0)
}
stop = true
const sorted = [...frames].sort((a, b) => a - b)
const avg = frames.reduce((a, b) => a + b, 0) / frames.length
const q = (arr, p) => arr[Math.min(arr.length - 1, Math.floor(arr.length * p))]
return {
fps: +(1000 / avg).toFixed(1),
avgMs: +avg.toFixed(2),
p95Ms: +q(sorted, 0.95).toFixed(2),
maxMs: +sorted[sorted.length - 1].toFixed(2),
syncMs: +(sync.reduce((a, b) => a + b, 0) / sync.length).toFixed(2),
}
},
{ axis, steps, px },
)
}
/** 取 G3Table 实例(.g3-table 的宿主组件) */
const GET_INST = `(() => {
const el = document.querySelector('.g3-table')
let inst = el && (el.__vueParentComponent || el.__vue_vnode?.component)
if (!inst) return null
// 有时拿到的是父布局,向上找带 columns 的那个
let cur = inst, hop = 0
while (cur && hop < 8) {
const ss = cur.setupState
if (ss && (ss.columns || ss.rows)) return cur
cur = cur.parent; hop++
}
return inst
})()`
async function domInfo(page) {
return page.evaluate(() => ({
bodyRows: document.querySelectorAll('.stk-tbody-main tr').length,
tds: document.querySelectorAll('.stk-tbody-main td').length,
cellWrappers: document.querySelectorAll('.stk-tbody-main td > .table-cell-wrapper').length,
displaySpans: document.querySelectorAll('.g3-table__cell-display').length,
editorRoots: document.querySelectorAll('.g3-table__cell-editor-root').length,
}))
}
/* ============================================================
* 实验 A:现状基线
* ============================================================ */
console.log('\n########## A. 现状基线(1000 行)##########')
{
const { browser, page } = await makePage()
console.log('DOM:', JSON.stringify(await domInfo(page)))
console.log('纵向滚动:', JSON.stringify(await scrollFps(page, { axis: 'y' })))
console.log('横向滚动:', JSON.stringify(await scrollFps(page, { axis: 'x', steps: 100, px: 80 })))
await browser.close()
}
/* ============================================================
* 实验 B/C:削减 customCell(同页内原地改列定义)
* ============================================================ */
console.log('\n########## B/C. 削减常驻 customCell 的影响 ##########')
{
const { browser, page } = await makePage()
console.log('基线:', JSON.stringify(await scrollFps(page, { axis: 'y' })))
// B:去掉所有 validate / validateWarning / precision → 少 4 列常驻 customCell
const bRes = await page.evaluate((getInstSrc) => {
const inst = eval(getInstSrc)
if (!inst) return { error: 'no instance' }
const cols = inst.setupState.columns
const arr = cols?.value ?? cols
let n = 0
for (const c of arr) {
if (!c.editor) continue
if (c.editor.validate) { c.editor.validate = undefined; n++ }
if (c.editor.validateWarning) { c.editor.validateWarning = undefined; n++ }
if (c.editor.precision !== undefined) { c.editor.precision = undefined; n++ }
}
return { stripped: n, total: arr.length }
}, GET_INST)
console.log('B 去掉校验/精度:', JSON.stringify(bRes))
await page.waitForTimeout(1500)
console.log('B DOM:', JSON.stringify(await domInfo(page)))
console.log('B 纵向滚动:', JSON.stringify(await scrollFps(page, { axis: 'y' })))
// C:去掉全部 editor → 零 customCell(性能上限)
const cRes = await page.evaluate((getInstSrc) => {
const inst = eval(getInstSrc)
if (!inst) return { error: 'no instance' }
const cols = inst.setupState.columns
const arr = cols?.value ?? cols
let n = 0
for (const c of arr) {
if (c.editor) { delete c.editor; n++ }
}
return { removed: n, total: arr.length }
}, GET_INST)
console.log('C 移除全部 editor:', JSON.stringify(cRes))
await page.waitForTimeout(2000)
console.log('C DOM:', JSON.stringify(await domInfo(page)))
console.log('C 纵向滚动:', JSON.stringify(await scrollFps(page, { axis: 'y' })))
console.log('C 横向滚动:', JSON.stringify(await scrollFps(page, { axis: 'x', steps: 100, px: 80 })))
await browser.close()
}
/* ============================================================
* 实验 D:数据量(用页面自带的下拉)
* ============================================================ */
console.log('\n########## D. 数据量 → 帧率(判断 O(n) 还是 O(可见))##########')
{
const { browser, page } = await makePage()
for (const n of [10, 100, 1000, 5000]) {
await page.selectOption('.booking__select', String(n))
await page.waitForTimeout(2500)
const info = await domInfo(page)
const y = await scrollFps(page, { axis: 'y', steps: 120, px: 60 })
const x = await scrollFps(page, { axis: 'x', steps: 80, px: 80 })
console.log(
`${String(n).padStart(5)} 行 | 行DOM=${String(info.bodyRows).padStart(3)} td=${String(info.tds).padStart(4)} span=${String(info.displaySpans).padStart(4)} | 纵 ${String(y.fps).padStart(5)}fps (${y.avgMs}ms) | 横 ${String(x.fps).padStart(5)}fps (${x.avgMs}ms)`,
)
}
await browser.close()
}
/* ============================================================
* 实验 E:固定列 sticky 补丁 A/B(组件支持的 URL 开关)
* ============================================================ */
console.log('\n########## E. 固定列 sticky 补丁 A/B ##########')
{
for (const q of ['', '?g3-fixed-patch=off']) {
const { browser, page } = await makePage(q)
const y = await scrollFps(page, { axis: 'y', steps: 120, px: 60 })
const x = await scrollFps(page, { axis: 'x', steps: 80, px: 80 })
console.log(`patch${q ? ' OFF' : ' ON '} | 纵 ${y.fps}fps (${y.avgMs}ms) | 横 ${x.fps}fps (${x.avgMs}ms)`)
await browser.close()
}
}
/* ============================================================
* 实验 F:滚动期间的长任务 & 每帧样式重算
* ============================================================ */
console.log('\n########## F. 滚动期间长任务 ##########')
{
const { browser, page } = await makePage()
const res = await page.evaluate(async () => {
const tasks = []
const obs = new PerformanceObserver((list) => {
for (const e of list.getEntries()) tasks.push(+e.duration.toFixed(1))
})
try {
obs.observe({ entryTypes: ['longtask'] })
} catch {
return { unsupported: true }
}
const cont = document.querySelector('.stk-table')
for (let i = 0; i < 90; i += 1) {
await new Promise((r) => requestAnimationFrame(r))
cont.scrollTop = (cont.scrollTop + 60) % (cont.scrollHeight - cont.clientHeight)
}
await new Promise((r) => setTimeout(r, 300))
obs.disconnect()
return {
count: tasks.length,
avg: +(tasks.reduce((a, b) => a + b, 0) / (tasks.length || 1)).toFixed(1),
max: tasks.length ? Math.max(...tasks) : 0,
sample: tasks.slice(0, 12),
}
})
console.log('长任务(>50ms):', JSON.stringify(res))
await browser.close()
}