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