Files
PhoneViewer/scripts/profile-worker.mjs
T
admin be34c630bb Fix blank drawings, drop parse-time animation cost, add render diagnostics
Accuracy / completeness
- docViews: split parsed scene into model space / paper space / free-standing
  blocks (geometry hidden in uninserted blocks no longer shows as a blank
  canvas); auto-select the first block view when model space is empty
- viewer: view switcher panel listing every detected view with expanded
  entity counts; paper space is now parsed (includePaperSpace) and kept apart
- SHX: missing .shx references are surfaced with an import control
  (addReferenceFile), linetypes/text redraw afterwards
- unsupported/proxy entities: markers on canvas + diagnostics block showing
  proxy / wipeout counts and missing SHX names

Speed
- the loading overlay used an infinite CSS spinner + backdrop blur while the
  worker parsed; on software rendering that starved the parse thread
  (TELECOM 18MB: 71s -> 30s wall clock, normalize 33s -> 14s). Replaced with a
  determinate progress bar fed by loader progress events
- transferInputBuffer for worker loads, maxInsertDepth 32, longer worker timeout

Tooling
- scripts/probe-dwg.mjs / probe-app.mjs: headless render + end-to-end probes
  (entity/layer/block/layout/proxy stats, timings, screenshots)
- tools/render-probe.html, scripts/profile-worker.mjs
- version 0.6.0 (versionCode 8)
2026-10-26 20:28:37 +08:00

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