Files
PhoneViewer/scripts/profile-worker.mjs
T

197 lines
6.5 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
// 用 CDP 对 DWG worker 内部做采样剖析,定位解析耗时的热点函数。
// 用法:node scripts/profile-worker.mjs "testfile/xxx.dwg"
import fs from 'node:fs'
import path from 'node:path'
import { spawn } from 'node:child_process'
const root = process.cwd()
const ven = path.resolve(root, '..', 'ven')
const file = process.argv[2]
if (!file) {
console.error('用法: node scripts/profile-worker.mjs <dwg 文件>')
process.exit(1)
}
const PORT = Number(process.env.CDP_PORT || 9333)
const HTTP_PORT = Number(process.env.PROBE_PORT || 5199)
const chrome = [
process.env.CHROME_PATH,
path.join(ven, 'chrome-for-testing', 'chrome-linux64', 'chrome'),
'/usr/bin/google-chrome'
].find((p) => p && fs.existsSync(p))
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const vite = spawn(
process.execPath,
[path.join(root, 'node_modules', 'vite', 'bin', 'vite.js'), '--port', String(HTTP_PORT), '--strictPort', '--host', '127.0.0.1'],
{ cwd: root, stdio: 'ignore' }
)
const chromeProc = spawn(
chrome,
[
'--headless=new',
'--no-sandbox',
`--remote-debugging-port=${PORT}`,
'--remote-allow-origins=*',
'--user-data-dir=/tmp/probe-cdp-profile',
'--use-gl=angle',
'--use-angle=swiftshader',
'--enable-unsafe-swiftshader',
'about:blank'
],
{ stdio: 'ignore' }
)
async function getWsUrl() {
for (let i = 0; i < 60; i++) {
try {
const res = await fetch(`http://127.0.0.1:${PORT}/json/version`)
const json = await res.json()
if (json.webSocketDebuggerUrl) return json.webSocketDebuggerUrl
} catch {}
await sleep(300)
}
throw new Error('Chrome CDP 未就绪')
}
let nextId = 1
const pending = new Map()
const listeners = []
function makeClient(ws) {
ws.addEventListener('message', (event) => {
const msg = JSON.parse(event.data)
if (msg.id && pending.has(msg.id)) {
const { resolve, reject } = pending.get(msg.id)
pending.delete(msg.id)
msg.error ? reject(new Error(JSON.stringify(msg.error))) : resolve(msg.result)
} else if (msg.method) {
for (const l of listeners) l(msg)
}
})
return (method, params = {}, sessionId) => {
const id = nextId++
const payload = { id, method, params }
if (sessionId) payload.sessionId = sessionId
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject })
ws.send(JSON.stringify(payload))
setTimeout(() => {
if (pending.has(id)) {
pending.delete(id)
reject(new Error(`CDP 超时: ${method}`))
}
}, 600000)
})
}
}
function aggregate(profile) {
const byId = new Map(profile.nodes.map((n) => [n.id, n]))
const self = new Map()
const total = profile.samples?.length || 0
const counts = new Map()
for (const s of profile.samples || []) counts.set(s, (counts.get(s) || 0) + 1)
const interval = (profile.endTime - profile.startTime) / (total || 1)
for (const [nodeId, count] of counts) {
const node = byId.get(nodeId)
if (!node) continue
const cf = node.callFrame
const url = (cf.url || '').replace(/^.*\/wasm\//, 'wasm/')
const key = `${cf.functionName || '(anonymous)'} @ ${url.split('/').pop()}:${cf.lineNumber + 1}`
self.set(key, (self.get(key) || 0) + count * interval)
}
return [...self.entries()].sort((a, b) => b[1] - a[1]).slice(0, 30)
}
let exitCode = 0
try {
await sleep(1500)
const wsUrl = await getWsUrl()
const ws = new WebSocket(wsUrl)
await new Promise((r) => ws.addEventListener('open', r))
const send = makeClient(ws)
const workerTargets = []
listeners.push((msg) => {
if (msg.method === 'Target.targetCreated' && msg.params.targetInfo.type === 'worker') {
workerTargets.push(msg.params.targetInfo)
}
})
await send('Target.setDiscoverTargets', { discover: true })
await send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: false, flatten: true })
const page = await send('Target.createTarget', { url: `http://127.0.0.1:${HTTP_PORT}/tools/render-probe.html` })
const pageSession = (await send('Target.attachToTarget', { targetId: page.targetId, flatten: true })).sessionId
await send('Runtime.enable', {}, pageSession)
await sleep(2500)
const label = path.basename(file)
const url = `http://127.0.0.1:${HTTP_PORT}/samples/${encodeURIComponent(label)}`
// 1) 先建 viewer 并预编译 wasm —— 让 worker 线程提前存在,便于挂上剖析器
const prepared = await send(
'Runtime.evaluate',
{
expression: `window.__prepare(${JSON.stringify(url)}).then(r => JSON.stringify(r))`,
awaitPromise: true,
returnByValue: true
},
pageSession
)
console.log('预编译:', prepared.result?.value)
let workerSession = null
for (let i = 0; i < 80 && !workerSession; i++) {
const targets = (await send('Target.getTargets')).targetInfos || []
const target = targets.find((t) => /dwg-worker/.test(t.url || ''))
if (target) {
const attached = await send('Target.attachToTarget', { targetId: target.targetId, flatten: true })
workerSession = attached.sessionId
} else {
await sleep(250)
}
}
if (!workerSession) {
const targets = (await send('Target.getTargets')).targetInfos || []
console.log('可见 targets:', targets.map((t) => `${t.type}:${t.url}`).join('\n '))
throw new Error('未发现 DWG worker target')
}
await send('Profiler.enable', {}, workerSession)
await send('Profiler.setSamplingInterval', { interval: 200 }, workerSession)
const started = Date.now()
await send('Profiler.start', {}, workerSession)
const result = await send(
'Runtime.evaluate',
{
expression: `window.__loadPrepared().then(r => JSON.stringify(r))`,
awaitPromise: true,
returnByValue: true
},
pageSession
)
console.log('页面结果:', result.result?.value || result, `(墙钟 ${Date.now() - started}ms)`)
const { profile } = await send('Profiler.stop', {}, workerSession)
const rows = aggregate(profile)
const totalMs = (profile.endTime - profile.startTime) / 1000
console.log(`\nworker 采样总时长 ${Math.round(totalMs)}ms,自耗时 Top:`)
for (const [key, us] of rows) {
if (us < 20000) continue
console.log(` ${(us / 1000).toFixed(0).padStart(6)}ms ${key}`)
}
fs.writeFileSync('/tmp/worker-profile.json', JSON.stringify(profile))
console.log('\n原始 profile: /tmp/worker-profile.json')
} catch (error) {
console.error('剖析失败:', error.message)
exitCode = 1
} finally {
vite.kill('SIGTERM')
chromeProc.kill('SIGTERM')
setTimeout(() => process.exit(exitCode), 300)
}