#!/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 ') 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) }