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)
This commit is contained in:
@@ -99,7 +99,7 @@ const kotlinStdlib = fs
|
||||
if (!kotlinStdlib) throw new Error('kotlin-stdlib not found in Gradle lib')
|
||||
|
||||
const appVersion = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version
|
||||
const versionCode = Number(process.env.APP_VERSION_CODE || 7)
|
||||
const versionCode = Number(process.env.APP_VERSION_CODE || 8)
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env node
|
||||
// 端到端检查真实 App(index.html → 打开文件 → 渲染 → 视图面板),输出状态文本与截图。
|
||||
// 用法:node scripts/probe-app.mjs [文件 ...]
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { chromium } from 'playwright-core'
|
||||
|
||||
const root = process.cwd()
|
||||
const ven = path.resolve(root, '..', 'ven')
|
||||
const outDir = path.join(root, 'tools', 'out')
|
||||
const port = Number(process.env.PROBE_PORT || 5199)
|
||||
fs.mkdirSync(outDir, { recursive: true })
|
||||
|
||||
const SUPPORTED = /\.(dwg|dxf|pdf|epub|xps|oxps|cbz|png|jpe?g|gif|bmp)$/i
|
||||
let files = process.argv.slice(2).filter((a) => SUPPORTED.test(a))
|
||||
if (!files.length) {
|
||||
const dir = path.join(root, 'testfile')
|
||||
files = fs.readdirSync(dir).filter((n) => SUPPORTED.test(n)).map((n) => path.join('testfile', n))
|
||||
}
|
||||
|
||||
const chrome = [process.env.CHROME_PATH, path.join(ven, 'chrome-for-testing', 'chrome-linux64', 'chrome')].find(
|
||||
(p) => p && fs.existsSync(p)
|
||||
)
|
||||
|
||||
const vite = spawn(
|
||||
process.execPath,
|
||||
[path.join(root, 'node_modules', 'vite', 'bin', 'vite.js'), '--port', String(port), '--strictPort', '--host', '127.0.0.1'],
|
||||
{ cwd: root, stdio: 'ignore' }
|
||||
)
|
||||
await new Promise((r) => setTimeout(r, 3000))
|
||||
|
||||
const browser = await chromium.launch({
|
||||
executablePath: chrome,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--use-gl=angle',
|
||||
'--use-angle=swiftshader',
|
||||
'--enable-unsafe-swiftshader',
|
||||
'--disable-dev-shm-usage',
|
||||
'--js-flags=--max-old-space-size=4096'
|
||||
]
|
||||
})
|
||||
|
||||
const report = []
|
||||
try {
|
||||
for (const file of files) {
|
||||
const abs = path.resolve(root, file)
|
||||
const label = path.basename(abs)
|
||||
const page = await browser.newPage({ viewport: { width: 1100, height: 860 }, deviceScaleFactor: 1 })
|
||||
const errors = []
|
||||
page.on('pageerror', (e) => errors.push(String(e.message).slice(0, 200)))
|
||||
await page.goto(`http://127.0.0.1:${port}/index.html`, { waitUntil: 'load' })
|
||||
if (process.env.PROBE_NO_ANIM === '1') {
|
||||
await page.addStyleTag({ content: '*{animation:none!important;transition:none!important;backdrop-filter:none!important}' })
|
||||
}
|
||||
const t0 = Date.now()
|
||||
|
||||
await page.setInputFiles('input[type=file]', abs)
|
||||
let status = ''
|
||||
try {
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const el = document.querySelector('.viewer-status')
|
||||
const text = el?.textContent || ''
|
||||
return text.includes('实体')
|
||||
},
|
||||
null,
|
||||
{ timeout: 180000 }
|
||||
)
|
||||
status = (await page.textContent('.viewer-status')) || ''
|
||||
} catch {
|
||||
status = `(超时) ${(await page.textContent('.viewer-status').catch(() => '')) || ''}`
|
||||
}
|
||||
const elapsed = Date.now() - t0
|
||||
await page.waitForTimeout(800)
|
||||
|
||||
const progress = await page.evaluate(() => window.__progress || null)
|
||||
if (progress) console.log(' worker 进度:', progress.map((p) => `${p.ms}ms/${p.phase}`).join(' '))
|
||||
const timings = await page.evaluate(() => window.__loadTimings || null)
|
||||
if (timings) console.log(' 阶段耗时:', JSON.stringify(timings))
|
||||
const shot = path.join(outDir, `app-${label.replace(/[^\w.\u4e00-\u9fa5-]/g, '_')}.png`)
|
||||
await page.screenshot({ path: shot })
|
||||
|
||||
// 打开“视图与图层”面板,检查视图列表与诊断信息
|
||||
let panel = ''
|
||||
try {
|
||||
await page.click('[data-action="layers"]', { timeout: 5000 })
|
||||
await page.waitForTimeout(500)
|
||||
const sheetShot = shot.replace(/\.png$/, '-panel.png')
|
||||
await page.screenshot({ path: sheetShot })
|
||||
panel = (await page.textContent('.sheet').catch(() => '')) || ''
|
||||
} catch (error) {
|
||||
panel = `面板打开失败: ${error.message.slice(0, 80)}`
|
||||
}
|
||||
|
||||
// 点第一个“游离块”视图,确认它确实是一张能看的图纸
|
||||
let viewShot = ''
|
||||
try {
|
||||
const rows = await page.$$('.view-row')
|
||||
if (rows.length > 1) {
|
||||
await rows[1].click()
|
||||
await page.waitForTimeout(1200)
|
||||
viewShot = shot.replace(/\.png$/, '-view1.png')
|
||||
await page.screenshot({ path: viewShot })
|
||||
}
|
||||
} catch {}
|
||||
|
||||
console.log(`\n=== ${label} ===`)
|
||||
console.log(` 打开+渲染墙钟 ${elapsed}ms`)
|
||||
console.log(` 状态栏: ${status.trim()}`)
|
||||
const views = panel.match(/[MLB][^\n]{0,60}实体/g)?.slice(0, 6) ?? []
|
||||
if (views.length) console.log(` 视图列表: ${views.map((v) => v.replace(/\s+/g, ' ')).join(' | ')}`)
|
||||
const diag = panel.match(/渲染提示[^\n]{0,160}/)?.[0]
|
||||
if (diag) console.log(` 诊断: ${diag.replace(/\s+/g, ' ')}`)
|
||||
if (errors.length) console.log(` 页面错误: ${errors.slice(0, 2).join(' | ')}`)
|
||||
report.push({
|
||||
file: label,
|
||||
elapsed,
|
||||
status: status.trim(),
|
||||
views,
|
||||
diag,
|
||||
errors,
|
||||
screenshot: path.relative(root, shot),
|
||||
viewScreenshot: viewShot ? path.relative(root, viewShot) : undefined
|
||||
})
|
||||
await page.close()
|
||||
}
|
||||
} finally {
|
||||
await browser.close()
|
||||
vite.kill('SIGTERM')
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(outDir, 'app-report.json'), JSON.stringify(report, null, 2))
|
||||
setTimeout(() => process.exit(0), 200)
|
||||
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env node
|
||||
// DWG 渲染基准 / 漏渲染诊断探针
|
||||
//
|
||||
// 用法:node scripts/probe-dwg.mjs [--out tools/out] [文件1.dwg 文件2.dwg ...]
|
||||
// - 默认扫描项目根目录下的 *.dwg / *.DWG
|
||||
// - 启动 vite 开发服务器 + 无头 Chromium(WebGL/SwiftShader)
|
||||
// - 输出解析耗时、首帧构建耗时、实体/图层/块/布局/代理图形统计、缺失引用、
|
||||
// 平移缩放耗时,并把每个图纸的截图写到 tools/out/
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { chromium } from 'playwright-core'
|
||||
|
||||
const root = process.cwd()
|
||||
const ven = path.resolve(root, '..', 'ven')
|
||||
const outDir = path.join(root, 'tools', 'out')
|
||||
const port = Number(process.env.PROBE_PORT || 5199)
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const outFlag = args.indexOf('--out')
|
||||
if (outFlag >= 0) args.splice(outFlag, 2)
|
||||
let files = args.filter((a) => a.toLowerCase().endsWith('.dwg'))
|
||||
if (!files.length) {
|
||||
const dirs = ['testfile', '.'].map((d) => path.join(root, d)).filter((d) => fs.existsSync(d))
|
||||
for (const dir of dirs) {
|
||||
for (const name of fs.readdirSync(dir)) {
|
||||
if (name.toLowerCase().endsWith('.dwg')) files.push(path.join(path.basename(dir), name))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!files.length) {
|
||||
console.error('没有找到 DWG 文件')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
fs.mkdirSync(outDir, { recursive: true })
|
||||
|
||||
function chromePath() {
|
||||
const candidates = [
|
||||
process.env.CHROME_PATH,
|
||||
path.join(ven, 'chrome-for-testing', 'chrome-linux64', 'chrome'),
|
||||
'/usr/bin/google-chrome',
|
||||
'/usr/bin/chromium'
|
||||
].filter(Boolean)
|
||||
const hit = candidates.find((p) => {
|
||||
try {
|
||||
return fs.statSync(p).isFile()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
if (!hit) throw new Error('找不到 Chrome/Chromium,可设置 CHROME_PATH')
|
||||
return hit
|
||||
}
|
||||
|
||||
function startVite() {
|
||||
const vite = path.join(root, 'node_modules', 'vite', 'bin', 'vite.js')
|
||||
const child = spawn(process.execPath, [vite, '--port', String(port), '--strictPort', '--host', '127.0.0.1'], {
|
||||
cwd: root,
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
})
|
||||
const ready = new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error('vite 启动超时')), 30000)
|
||||
const onData = (buf) => {
|
||||
if (/Local:.*http/.test(String(buf))) {
|
||||
clearTimeout(timer)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
child.stdout.on('data', onData)
|
||||
child.stderr.on('data', (b) => process.env.PROBE_DEBUG && process.stderr.write(b))
|
||||
child.on('exit', (code) => reject(new Error(`vite 退出:${code}`)))
|
||||
})
|
||||
return { child, ready }
|
||||
}
|
||||
|
||||
const bytes = (n) => `${(n / 1048576).toFixed(1)} MB`
|
||||
|
||||
const { child: viteChild, ready } = startVite()
|
||||
await ready
|
||||
|
||||
const browser = await chromium.launch({
|
||||
executablePath: chromePath(),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--use-gl=angle',
|
||||
'--use-angle=swiftshader',
|
||||
'--enable-unsafe-swiftshader',
|
||||
'--disable-dev-shm-usage',
|
||||
'--js-flags=--max-old-space-size=4096'
|
||||
]
|
||||
})
|
||||
|
||||
const results = []
|
||||
try {
|
||||
for (const file of files) {
|
||||
const abs = path.resolve(root, file)
|
||||
const label = path.basename(abs)
|
||||
if (!fs.existsSync(abs)) {
|
||||
console.error(`跳过(不存在):${label}`)
|
||||
continue
|
||||
}
|
||||
const page = await browser.newPage({ viewport: { width: 1280, height: 900 }, deviceScaleFactor: 1 })
|
||||
const logs = []
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error' || msg.type() === 'warning') logs.push(`[${msg.type()}] ${msg.text()}`)
|
||||
})
|
||||
page.on('pageerror', (err) => logs.push(`[pageerror] ${err.message}`))
|
||||
page.on('worker', (w) => {
|
||||
w.on('console', (msg) => logs.push(`[worker:${msg.type()}] ${msg.text()}`))
|
||||
})
|
||||
|
||||
// 直接把本地图纸喂给页面,避免复制到大目录
|
||||
await page.route('**/samples/**', async (route) => {
|
||||
const url = new URL(route.request().url())
|
||||
const name = decodeURIComponent(url.pathname.split('/').pop())
|
||||
const target = [
|
||||
path.join(root, 'samples', name),
|
||||
path.join(root, 'testfile', name),
|
||||
path.join(root, name)
|
||||
].find((p) => fs.existsSync(p))
|
||||
if (target) {
|
||||
await route.fulfill({ status: 200, body: fs.readFileSync(target), contentType: 'application/octet-stream' })
|
||||
} else {
|
||||
await route.fulfill({ status: 404, body: 'not found' })
|
||||
}
|
||||
})
|
||||
|
||||
await page.goto(`http://127.0.0.1:${port}/tools/render-probe.html`, { waitUntil: 'load' })
|
||||
await page.waitForFunction(() => window.__ready === true, null, { timeout: 30000 })
|
||||
|
||||
console.log(`\n=== ${label} (${bytes(fs.statSync(abs).size)}) ===`)
|
||||
const url = `http://127.0.0.1:${port}/samples/${encodeURIComponent(label)}`
|
||||
let data
|
||||
try {
|
||||
const probeOpts = {
|
||||
preload: process.env.PROBE_PRELOAD === '1',
|
||||
viewerOptions: process.env.PROBE_PAPER === '1' ? { includePaperSpace: true } : {}
|
||||
}
|
||||
data = await page.evaluate(
|
||||
([u, o]) => window.__probe(u, o),
|
||||
[url, probeOpts]
|
||||
)
|
||||
} catch (error) {
|
||||
data = { file: label, fatal: String(error) }
|
||||
}
|
||||
|
||||
if (!data.fatal) {
|
||||
const shot = path.join(outDir, `${label.replace(/[^\w.\u4e00-\u9fa5-]/g, '_')}.png`)
|
||||
await page.screenshot({ path: shot })
|
||||
data.screenshot = path.relative(root, shot)
|
||||
data.consoleErrors = logs.slice(0, 20)
|
||||
const r = data.render
|
||||
console.log(
|
||||
`解析 ${data.parseElapsedMs}ms · 首帧构建 ${r.buildElapsedMs ?? '-'}ms · 后端 ${data.backend} · ` +
|
||||
`实体 ${data.entityCount} · 图层 ${data.layerCount} · 块 ${data.blockCount} · 布局 ${data.pageCount} · ` +
|
||||
`代理 ${data.proxies.total}`
|
||||
)
|
||||
console.log(
|
||||
`绘制 ${r.drawn}/${r.total} · 跳过 ${r.skipped} · 视口裁剪 ${r.culledPrimitiveCount} · ` +
|
||||
`GPU ${r.gpuMemoryMB ?? '-'}MB · 平移帧 ${data.interaction.panFrameMs.p50}ms(p95 ${data.interaction.panFrameMs.p95}ms)`
|
||||
)
|
||||
console.log(
|
||||
`预编译 ${data.preloadMs}ms · 布局空间实体 ${data.paperSpaceEntities} · 元数据键 ${(data.metadataKeys || []).join(',')}`
|
||||
)
|
||||
if (data.missingReferences.length) console.log(`缺失引用(${data.missingReferences.length}): ${data.missingReferences.slice(0, 8).join(', ')}`)
|
||||
if (Object.keys(r.unsupported || {}).length) console.log(`不支持的实体类型: ${JSON.stringify(r.unsupported).slice(0, 300)}`)
|
||||
if (data.warnings.length) console.log(`警告: ${data.warnings.slice(0, 3).join(' | ')}`)
|
||||
if (data.consoleErrors?.length) console.log(`控制台错误: ${data.consoleErrors.slice(0, 3).join(' | ')}`)
|
||||
console.log(`截图: ${data.screenshot}`)
|
||||
} else {
|
||||
console.log('失败:', data.fatal)
|
||||
if (logs.length) console.log(logs.slice(0, 5).join('\n'))
|
||||
}
|
||||
results.push(data)
|
||||
await page.close()
|
||||
}
|
||||
} finally {
|
||||
await browser.close()
|
||||
viteChild.kill('SIGTERM')
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(outDir, 'probe-report.json'), JSON.stringify(results, null, 2))
|
||||
console.log(`\n报告:${path.relative(root, path.join(outDir, 'probe-report.json'))}`)
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/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)
|
||||
}
|
||||
Reference in New Issue
Block a user