/** * 票 33:同环境修复前后对照(jitter-report-baseline vs jitter-report-fixed)。 * * 环境说明(诚实口径):本轮探针环境的白模(cad-bim v8/roofed2 glb 批次)持续流送 + * 无 GPU(软件渲染),~680ms/帧的**环境底噪**会污染票面判据 (a)(b) 的绝对阈值 * (±500ms 无长任务 / p95≤33ms 在该环境物理不可达——静置对照桶同值即证)。 * 因此用三个**病灶专属**口径对照: * ① 纯 snapshot 长任务:≥100ms 长任务的 ±400ms 伴随请求里含 map-snapshot 且**不含任何 * glb/terrain 瓦片**——旧全量重同步的特征形态(白模流送 decode 必然伴随 .glb); * ② 一轮刷新成簇:纯 snapshot 长任务里时间间隔 <2s 的连簇长度(旧病理:8 批连续); * ③ 长任务总量与 tick 增量:刷新窗口 p95 − 静置窗口 p95。 */ import { readFileSync } from 'node:fs' const [baselinePath, fixedPath] = process.argv.slice(2) const load = (p) => JSON.parse(readFileSync(p, 'utf8')) function analyze(report) { const tiles = (url) => /\.glb|\.terrain|\/gis\/|\.png|\.jpg/.test(url) const snapTimes = report.requests.filter((r) => r.url.includes('map-snapshot')).map((r) => r.t) const longTasks = (report.longTasksAll || report.longTaskCorrelation || []).filter((t) => t.dur >= 100) const pure = [] for (const task of longTasks) { const rest = (report.longTaskCorrelation.find((c) => Math.abs(c.start - task.start) < 1 && c.dur === task.dur) || {}).rest || [] const hasSnap = rest.some((u) => u.includes('map-snapshot')) const hasTile = rest.some((u) => tiles(u)) if (hasSnap && !hasTile) pure.push({ start: task.start, dur: task.dur }) } pure.sort((a, b) => a.start - b.start) let maxCluster = 0 let run = 0 let prev = null for (const p of pure) { run = prev !== null && p.start - prev < 2000 ? run + 1 : 1 prev = p.start maxCluster = Math.max(maxCluster, run) } // 帧间隔:刷新窗口 vs 静置窗口(bucket 起点在 30–40s / 60–70s 视为刷新窗口) const p95 = (vals) => { const s = [...vals].sort((a, b) => a - b) return s.length ? s[Math.min(s.length - 1, Math.floor(0.95 * s.length))] : null } let refreshDts = [] let idleDts = [] const buckets = report.frameBuckets || [] if (buckets.length) { for (const b of buckets) { const sec = b.from / 1000 ;(sec >= 30 && sec < 40) || (sec >= 60 && sec < 70) ? refreshDts.push(...b.dts) : idleDts.push(...b.dts) } } else { // 旧版探针没有 frameBuckets:按 fpsWindows 桶近似 for (const w of report.fpsWindows || []) { const sec = w.from / 1000 const isRefresh = (sec >= 30 && sec < 40) || (sec >= 60 && sec < 70) if (isRefresh && w.p95 != null) refreshDts = [w.p95] if (!isRefresh && w.p95 != null) idleDts.push(w.p95) } } return { longTaskTotal: longTasks.length, pureSnapshot: pure.length, pureList: pure, maxCluster, refreshP95: p95(refreshDts), idleP95: p95(idleDts), } } const base = analyze(load(baselinePath)) const fixed = analyze(load(fixedPath)) const fmt = (v) => (v === null ? 'n/a' : Number(v).toFixed(1)) console.log('口径 基线(无修复) 带修复') console.log(`≥100ms 长任务总量 ${String(base.longTaskTotal).padEnd(16)} ${fixed.longTaskTotal}`) console.log(`纯 map-snapshot 长任务(病灶) ${String(base.pureSnapshot).padEnd(16)} ${fixed.pureSnapshot}`) console.log(`最长连续簇(≈批数×120ms节拍) ${String(base.maxCluster).padEnd(16)} ${fixed.maxCluster}`) console.log(`刷新窗口帧间隔 p95 ${fmt(base.refreshP95).padEnd(16)} ${fmt(fixed.refreshP95)} ms`) console.log(`静置窗口帧间隔 p95 ${fmt(base.idleP95).padEnd(16)} ${fmt(fixed.idleP95)} ms`) console.log(`tick 增量(刷新−静置) ${fmt(base.refreshP95 - base.idleP95).padEnd(16)} ${fmt(fixed.refreshP95 - fixed.idleP95)} ms`) console.log('\n基线病灶长任务时刻:', base.pureList.map((p) => `${Math.round(p.start)}ms/${p.dur}ms`).join(' ') || '(无)') console.log('修复后病灶长任务时刻:', fixed.pureList.map((p) => `${Math.round(p.start)}ms/${p.dur}ms`).join(' ') || '(无)')