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'))}`)
|