Files
workspace/code/g3soft-libs/packages/ui/probe-table-gpu.mjs
T
2026-10-16 17:30:17 +08:00

174 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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`)
}