| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576 |
- // Development-only evidence. Diagnostic reads never force a scene render.
- import { createMotionIntervalWindow } from '../domain/frameMotionWindow.js'
- export function attachModelDiagnostics(scene, host, readState, { now = () => performance.now(), isMoving = () => false, pilotView = null } = {}) {
- const doc = host.ownerDocument
- const node = doc.createElement('details'), summary = doc.createElement('summary'), output = doc.createElement('output')
- node.setAttribute('data-model-diagnostics', '')
- summary.textContent = '模型加载诊断(开发)'
- node.style.cssText = 'position:absolute;bottom:4px;left:4px;z-index:8;max-width:480px;font:11px monospace;background:#061827ee;color:#bad4e0;padding:4px;pointer-events:auto'
- output.style.cssText = 'display:block;white-space:pre-wrap;max-height:180px;overflow:auto'
- output.setAttribute('aria-label', '模型加载诊断读数')
- node.append(summary, output); host.appendChild(node)
- const query = new URLSearchParams(doc.defaultView.location.search)
- const audit = query.get('lodAudit') === '1'
- let start = null, lastPaint = -Infinity, total = 0, label = 'initial', requested = null, viewResult = null, disposed = false
- const durations = [], records = []
- const motion = createMotionIntervalWindow()
- const add = (list, n) => { list.push(n); if (list.length > 240) list.shift() }
- const stats = list => {
- const sorted = [...list].sort((a, b) => a - b)
- return { samples: sorted.length, p50Ms: sorted.length ? +sorted[Math.floor(sorted.length * .5)].toFixed(2) : null, p95Ms: sorted.length ? +sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * .95))].toFixed(2) : null, maxMs: sorted.length ? +sorted[sorted.length - 1].toFixed(2) : null, over50Ms: sorted.filter(n => n > 50).length }
- }
- function paint() {
- const payload = { ...readState(), label, requested, viewResult, renderEvents: total, renderSubmissionDuration: stats(durations), movingFrameIntervals: motion.snapshot(), gpuTimeMeasured: false, fpsDuringIdleMeaningful: false, visibleDocument: !doc.hidden }
- output.textContent = JSON.stringify(payload, null, 2)
- if (audit) {
- records.push(payload); if (records.length > 240) records.shift()
- const regions = (payload.regions || []).map(r => ({ ...r, renderedTiles: undefined, renderedByLevel: (r.renderedTiles || []).reduce((a,t) => { a[t.level] = (a[t.level] || 0) + 1; return a }, {}), renderedExamples: (r.renderedTiles || []).slice(0, 3) }))
- console.log('[LOD-AUDIT] ' + JSON.stringify({ label, requested, viewResult: viewResult && { applied: viewResult.applied, height: viewResult.height, error: viewResult.error }, renderEvents: total, sub: payload.renderSubmissionDuration, moving: payload.movingFrameIntervals, camera: payload.camera && { pivot: payload.camera.shownPivot, hpr: payload.camera.shownHpr, settled: payload.camera.settled }, regions: regions.map(r => ({ id: r.id, show: r.show, masked: r.coarseMasked, loaded: r.loaded, pending: r.pending, processing: r.processing, sse: r.maximumScreenSpaceError, effective: r.memoryAdjustedScreenSpaceError, memoryMB: r.memoryBytes && +(r.memoryBytes / 1048576).toFixed(1), firstContentMs: r.firstContentMs, rendered: r.renderedByLevel })) }))
- }
- return payload
- }
- if (audit && pilotView) {
- node.open = true
- node.style.left = '32%'; node.style.zIndex = '90'
- const controls = doc.createElement('div')
- for (const range of [6000, 3000, 1200, 450, 180, 320]) {
- const button = doc.createElement('button')
- button.textContent = '试点 ' + range + 'm'; button.dataset.lodAuditRange = String(range)
- button.addEventListener('click', async () => {
- label = 'view-' + range; requested = { range, atMs: now() }; viewResult = null
- durations.length = 0; motion.reset()
- button.disabled = true
- try { viewResult = await pilotView(range) } catch (error) { viewResult = { error: String(error.message || error) } }
- if (!disposed) { button.disabled = false; paint() }
- })
- controls.appendChild(button)
- }
- const exportButton = doc.createElement('button')
- exportButton.textContent = '导出本次 LOD 观测'
- exportButton.addEventListener('click', () => {
- paint()
- const a = doc.createElement('a'), url = URL.createObjectURL(new Blob([JSON.stringify(records, null, 2)], { type: 'application/json' }))
- a.href = url; a.download = 'lod-audit.json'; a.click(); URL.revokeObjectURL(url)
- })
- controls.appendChild(exportButton); node.insertBefore(controls, output)
- }
- const pre = scene.preRender.addEventListener(() => { start = now() })
- const post = scene.postRender.addEventListener(() => {
- const t = now(); total++
- if (!doc.hidden) {
- if (start !== null) add(durations, t - start)
- motion.push(t, isMoving())
- } else {
- // Hidden frames do not sample, but they must break "consecutive motion"
- // so the first moving frame after returning cannot smuggle an idle gap in.
- motion.markGap(t)
- }
- start = null
- if (t - lastPaint >= 1000) { lastPaint = t; paint() }
- })
- // Audit mode also observes load progress while requestRenderMode is idle.
- const timer = audit ? setInterval(paint, 2000) : null
- summary.addEventListener('click', paint); paint()
- return () => { disposed = true; if (timer) clearInterval(timer); pre(); post(); node.remove() }
- }
|