be34c630bb
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)
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)
|
||
}
|