/** * 票 26 消融实验:拦截 `/map-snapshot`(让 freshness tick 的 refresh 拿不到新页), * 其余端点与 GIS 资源照常。若 30s/60s 周期性帧 stall 消失 → 「refresh→重建」因果链成立。 * * 运行:node .scratch/cockpit-real-data-wayfinding/verify/artifacts/t26/probe-jitter-ablate.mjs */ import { cdpLaunch } from '../../../../cockpit-shell-panel-layout/verify/cdp-driver.mjs' const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' const TARGET_URL = process.env.T26_URL || 'http://127.0.0.1:8000/#/cockpit' const WAIT_MS = Number(process.env.T26_WAIT_MS || 70000) const POLL_MS = 250 const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) function stats(values) { if (!values.length) return null const sorted = [...values].sort((a, b) => a - b) const at = (q) => sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))] return { n: sorted.length, p50: at(0.5), p95: at(0.95), max: sorted[sorted.length - 1] } } const browser = await cdpLaunch({ executablePath: CHROME, width: 1280, height: 800 }) console.log(`Chrome: ${await browser.version()} [消融:map-snapshot 全程 500]`) const page = await browser.newPage() await page.goto(TARGET_URL, { timeout: 60000 }) await page.waitForSelector('.cockpit-shell', { timeout: 20000 }) // 采样器 + 消融补丁:map-snapshot 请求直接失败(fetch 与 XHR 两条路径都拦)。 await page.evaluate(() => { window.__t26 = { frames: [], longTasks: [], blocked: 0 } let last = performance.now() const tick = (now) => { window.__t26.frames.push({ t: now, dt: now - last }) last = now requestAnimationFrame(tick) } requestAnimationFrame(tick) try { new PerformanceObserver((list) => { for (const entry of list.getEntries()) window.__t26.longTasks.push({ start: entry.startTime, dur: entry.duration }) }).observe({ entryTypes: ['longtask'] }) } catch { /* 忽略 */ } const blocked = (url) => String(url).includes('map-snapshot') // 只拦 15s 之后的 map-snapshot:首屏加载与 auto-batch(0–8s)照常, // 消融目标是 30s 起的 freshness refresh 轮 —— 这样图上对象仍在,只有"重载"被摘除。 const armAt = performance.now() + 15000 const shouldBlock = () => performance.now() > armAt const nativeFetch = window.fetch.bind(window) window.fetch = async (...args) => { const url = String((args[0] && args[0].url) || args[0]) if (blocked(url) && shouldBlock()) { window.__t26.blocked += 1 return { ok: false, status: 500, json: async () => ({ code: 500, msg: 'ablated' }), headers: { get: () => null } } } return nativeFetch(...args) } const XHR = XMLHttpRequest.prototype const nativeOpen = XHR.open XHR.open = function (method, url, ...rest) { this.__t26blocked = blocked(url) return nativeOpen.call(this, method, url, ...rest) } const nativeSend = XHR.send XHR.send = function (...args) { if (this.__t26blocked && shouldBlock()) { window.__t26.blocked += 1 Object.defineProperty(this, 'readyState', { configurable: true, get: () => 4 }) Object.defineProperty(this, 'status', { configurable: true, get: () => 500 }) Object.defineProperty(this, 'responseText', { configurable: true, get: () => '{"code":500,"msg":"ablated"}' }) Object.defineProperty(this, 'response', { configurable: true, get: () => '{"code":500,"msg":"ablated"}' }) setTimeout(() => { this.dispatchEvent(new Event('readystatechange')) this.dispatchEvent(new Event('load')) this.dispatchEvent(new Event('loadend')) }, 20) return undefined } return nativeSend.apply(this, args) } }) if (await page.$('[data-cockpit-intro]')) { await page.click('[data-cockpit-intro]') await page.waitForSelector('[data-cockpit-intro]', { hidden: true, timeout: 10000 }).catch(() => {}) } await page.waitForFunction( () => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine), { timeout: 40000 }, ).catch(() => {}) await sleep(3000) console.log(`静置采样 ${(WAIT_MS / 1000).toFixed(0)}s…`) const deadline = Date.now() + WAIT_MS while (Date.now() < deadline) await sleep(POLL_MS) const result = await page.evaluate(() => { const frames = window.__t26.frames const start = frames.length ? frames[0].t : 0 const out = [] let bucket = [] let bucketStart = start for (const frame of frames) { if (frame.t - bucketStart >= 5000) { out.push({ from: bucketStart - start, dts: bucket.map((f) => f.dt) }) bucket = [] bucketStart = frame.t } bucket.push(frame) } if (bucket.length) out.push({ from: bucketStart - start, dts: bucket.map((f) => f.dt) }) return { windows: out.map((w) => ({ from: w.from, dts: w.dts })), longTasks: window.__t26.longTasks, blocked: window.__t26.blocked, } }) console.log(`被拦截的 map-snapshot 请求数:${result.blocked}`) console.log('fps 窗口:') for (const w of result.windows) { const s = stats(w.dts) console.log(` ${(w.from / 1000).toFixed(1)}s p50=${s.p50} p95=${s.p95} max=${s.max}`) } const big = result.longTasks.filter((t) => t.dur >= 100) console.log(`长任务(≥100ms):${big.length} 个 ${JSON.stringify(big.slice(0, 8))}`) await browser.close()