Files
PhoneViewer/scripts/probe-dwg.mjs
T

185 lines
6.9 KiB
JavaScript
Raw Normal View History

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