import { chromium } from '@playwright/test' /** * 探针 v7 —— 排除「测量环境本身」造成的假象(只读诊断)。 * * 必须回答的问题:v3~v6 的 ~12fps 是不是 headless Chromium 走软件光栅化 * (SwiftShader / 无 GPU 合成)导致的?如果是,那这个数字不能当成用户的真实体感。 * * 做法:同一页面、同一套动作,对比 * · headless(默认) * · headless + GPU 相关开关 * · headed(真实窗口,有 GPU) * 并输出 compositor / GPU 信息。 */ const BASE = process.env.PROBE_URL || 'http://127.0.0.1:5090' const MENU_CODE = 'menu.table_test' async function routePerms(context) { 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' }, }) }) } async function fps(page, { axis = 'y', steps = 120, px = 60 } = {}) { return page.evaluate( async ({ axis, steps, px }) => { const cont = document.querySelector('.stk-table') cont.scrollTop = 0 cont.scrollLeft = 0 await new Promise((r) => requestAnimationFrame(r)) 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) for (let i = 0; i < steps; i += 1) { await new Promise((r) => requestAnimationFrame(r)) 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) } } 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), avg: +avg.toFixed(1), p95: +q(sorted, 0.95).toFixed(1) } }, { axis, steps, px }, ) } async function probeRun(label, launchOpts) { let browser try { browser = await chromium.launch(launchOpts) } catch (e) { console.log(`${label}: 启动失败 ${String(e).slice(0, 120)}`) return null } const context = await browser.newContext({ viewport: { width: 1600, height: 900 } }) await routePerms(context) const page = await context.newPage() // GPU / 合成器信息 let gpu = {} try { const client = await context.newCDPSession(page) const info = await client.send('SystemInfo.getInfo') gpu = { devices: (info.gpu?.devices || []).map((d) => ({ vendor: d.vendorId, device: d.deviceId, devString: (d.deviceString || '').slice(0, 60), })), auxAttributes: info.gpu?.auxAttributes ? { softwareRendering: info.gpu.auxAttributes.softwareRendering, glRenderer: info.gpu.auxAttributes.glRenderer, glVendor: info.gpu.auxAttributes.glVendor, gpuCompositing: info.gpu.auxAttributes.gpuCompositing, } : null, modelName: info.modelName, } await client.detach() } catch (e) { gpu = { error: String(e).slice(0, 100) } } await page.goto(`${BASE}/table-test`, { waitUntil: 'domcontentloaded' }) await page.waitForSelector('.stk-table', { timeout: 90000 }) await page.waitForTimeout(3000) const y1 = await fps(page, { axis: 'y' }) const x1 = await fps(page, { axis: 'x', steps: 80, px: 80 }) const y2 = await fps(page, { axis: 'y' }) const webgl = await page.evaluate(() => { try { const c = document.createElement('canvas') const gl = c.getContext('webgl') || c.getContext('experimental-webgl') if (!gl) return { webgl: false } const dbg = gl.getExtension('WEBGL_debug_renderer_info') return { webgl: true, renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER), vendor: dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR), } } catch (e) { return { error: String(e).slice(0, 80) } } }) console.log(`\n===== ${label} =====`) console.log('GPU:', JSON.stringify(gpu)) console.log('WebGL:', JSON.stringify(webgl)) console.log(`纵向: ${JSON.stringify(y1)} / ${JSON.stringify(y2)}`) console.log(`横向: ${JSON.stringify(x1)}`) await browser.close() return { label, y1, x1, y2 } } const results = [] results.push(await probeRun('A. headless(默认,探针前几版用的就是这个)', { headless: true })) results.push( await probeRun('B. headless + 强制 GPU', { headless: true, args: ['--enable-gpu', '--use-gl=angle', '--use-angle=d3d11', '--enable-unsafe-swiftshader'], }), ) results.push(await probeRun('C. headed(真实窗口)', { headless: false })) console.log('\n================ 汇总 ================') for (const r of results) { if (!r) continue console.log(`${r.label}\n 纵 ${r.y1.fps}/${r.y2.fps} fps 横 ${r.x1.fps} fps`) }