197 lines
6.5 KiB
JavaScript
197 lines
6.5 KiB
JavaScript
|
|
#!/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)
|
|||
|
|
}
|