Files
PhoneViewer/scripts/probe-dwg.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

185 lines
6.9 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
// 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'))}`)