174 lines
6.0 KiB
JavaScript
174 lines
6.0 KiB
JavaScript
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`)
|
||
}
|