probe-jitter-ablate.mjs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. /**
  2. * 票 26 消融实验:拦截 `/map-snapshot`(让 freshness tick 的 refresh 拿不到新页),
  3. * 其余端点与 GIS 资源照常。若 30s/60s 周期性帧 stall 消失 → 「refresh→重建」因果链成立。
  4. *
  5. * 运行:node .scratch/cockpit-real-data-wayfinding/verify/artifacts/t26/probe-jitter-ablate.mjs
  6. */
  7. import { cdpLaunch } from '../../../../cockpit-shell-panel-layout/verify/cdp-driver.mjs'
  8. const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
  9. const TARGET_URL = process.env.T26_URL || 'http://127.0.0.1:8000/#/cockpit'
  10. const WAIT_MS = Number(process.env.T26_WAIT_MS || 70000)
  11. const POLL_MS = 250
  12. const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
  13. function stats(values) {
  14. if (!values.length) return null
  15. const sorted = [...values].sort((a, b) => a - b)
  16. const at = (q) => sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))]
  17. return { n: sorted.length, p50: at(0.5), p95: at(0.95), max: sorted[sorted.length - 1] }
  18. }
  19. const browser = await cdpLaunch({ executablePath: CHROME, width: 1280, height: 800 })
  20. console.log(`Chrome: ${await browser.version()} [消融:map-snapshot 全程 500]`)
  21. const page = await browser.newPage()
  22. await page.goto(TARGET_URL, { timeout: 60000 })
  23. await page.waitForSelector('.cockpit-shell', { timeout: 20000 })
  24. // 采样器 + 消融补丁:map-snapshot 请求直接失败(fetch 与 XHR 两条路径都拦)。
  25. await page.evaluate(() => {
  26. window.__t26 = { frames: [], longTasks: [], blocked: 0 }
  27. let last = performance.now()
  28. const tick = (now) => {
  29. window.__t26.frames.push({ t: now, dt: now - last })
  30. last = now
  31. requestAnimationFrame(tick)
  32. }
  33. requestAnimationFrame(tick)
  34. try {
  35. new PerformanceObserver((list) => {
  36. for (const entry of list.getEntries()) window.__t26.longTasks.push({ start: entry.startTime, dur: entry.duration })
  37. }).observe({ entryTypes: ['longtask'] })
  38. } catch { /* 忽略 */ }
  39. const blocked = (url) => String(url).includes('map-snapshot')
  40. // 只拦 15s 之后的 map-snapshot:首屏加载与 auto-batch(0–8s)照常,
  41. // 消融目标是 30s 起的 freshness refresh 轮 —— 这样图上对象仍在,只有"重载"被摘除。
  42. const armAt = performance.now() + 15000
  43. const shouldBlock = () => performance.now() > armAt
  44. const nativeFetch = window.fetch.bind(window)
  45. window.fetch = async (...args) => {
  46. const url = String((args[0] && args[0].url) || args[0])
  47. if (blocked(url) && shouldBlock()) {
  48. window.__t26.blocked += 1
  49. return { ok: false, status: 500, json: async () => ({ code: 500, msg: 'ablated' }), headers: { get: () => null } }
  50. }
  51. return nativeFetch(...args)
  52. }
  53. const XHR = XMLHttpRequest.prototype
  54. const nativeOpen = XHR.open
  55. XHR.open = function (method, url, ...rest) {
  56. this.__t26blocked = blocked(url)
  57. return nativeOpen.call(this, method, url, ...rest)
  58. }
  59. const nativeSend = XHR.send
  60. XHR.send = function (...args) {
  61. if (this.__t26blocked && shouldBlock()) {
  62. window.__t26.blocked += 1
  63. Object.defineProperty(this, 'readyState', { configurable: true, get: () => 4 })
  64. Object.defineProperty(this, 'status', { configurable: true, get: () => 500 })
  65. Object.defineProperty(this, 'responseText', { configurable: true, get: () => '{"code":500,"msg":"ablated"}' })
  66. Object.defineProperty(this, 'response', { configurable: true, get: () => '{"code":500,"msg":"ablated"}' })
  67. setTimeout(() => {
  68. this.dispatchEvent(new Event('readystatechange'))
  69. this.dispatchEvent(new Event('load'))
  70. this.dispatchEvent(new Event('loadend'))
  71. }, 20)
  72. return undefined
  73. }
  74. return nativeSend.apply(this, args)
  75. }
  76. })
  77. if (await page.$('[data-cockpit-intro]')) {
  78. await page.click('[data-cockpit-intro]')
  79. await page.waitForSelector('[data-cockpit-intro]', { hidden: true, timeout: 10000 }).catch(() => {})
  80. }
  81. await page.waitForFunction(
  82. () => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine),
  83. { timeout: 40000 },
  84. ).catch(() => {})
  85. await sleep(3000)
  86. console.log(`静置采样 ${(WAIT_MS / 1000).toFixed(0)}s…`)
  87. const deadline = Date.now() + WAIT_MS
  88. while (Date.now() < deadline) await sleep(POLL_MS)
  89. const result = await page.evaluate(() => {
  90. const frames = window.__t26.frames
  91. const start = frames.length ? frames[0].t : 0
  92. const out = []
  93. let bucket = []
  94. let bucketStart = start
  95. for (const frame of frames) {
  96. if (frame.t - bucketStart >= 5000) {
  97. out.push({ from: bucketStart - start, dts: bucket.map((f) => f.dt) })
  98. bucket = []
  99. bucketStart = frame.t
  100. }
  101. bucket.push(frame)
  102. }
  103. if (bucket.length) out.push({ from: bucketStart - start, dts: bucket.map((f) => f.dt) })
  104. return {
  105. windows: out.map((w) => ({ from: w.from, dts: w.dts })),
  106. longTasks: window.__t26.longTasks,
  107. blocked: window.__t26.blocked,
  108. }
  109. })
  110. console.log(`被拦截的 map-snapshot 请求数:${result.blocked}`)
  111. console.log('fps 窗口:')
  112. for (const w of result.windows) {
  113. const s = stats(w.dts)
  114. console.log(` ${(w.from / 1000).toFixed(1)}s p50=${s.p50} p95=${s.p95} max=${s.max}`)
  115. }
  116. const big = result.longTasks.filter((t) => t.dur >= 100)
  117. console.log(`长任务(≥100ms):${big.length} 个 ${JSON.stringify(big.slice(0, 8))}`)
  118. await browser.close()