compare-baseline.mjs 4.4 KB

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