| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- /**
- * 票 26 消融 B(缓存回放):15s 后 map-snapshot 全部回放缓存的同 URL 响应。
- *
- * 与消融 A 的区别:refresh 仍然成功、分页照走、对象照在图上 —— 只是数据完全相同。
- * 若 30s/60s 的周期性 stall 仍在 → 抽搐来自「refresh→整层重建」机制本身,
- * 与数据是否变化无关;这是对因果链最严格的检验。
- *
- * 运行:node .scratch/cockpit-real-data-wayfinding/verify/artifacts/t26/probe-jitter-replay.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()} [消融 B:map-snapshot 缓存回放(15s 后)]`)
- const page = await browser.newPage()
- await page.goto(TARGET_URL, { timeout: 60000 })
- await page.waitForSelector('.cockpit-shell', { timeout: 20000 })
- await page.evaluate(() => {
- window.__t26 = { frames: [], longTasks: [], replayed: 0, recorded: 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 isSnapshot = (url) => String(url).includes('map-snapshot')
- const armAt = performance.now() + 15000
- const cache = new Map() // url -> { status, body }
- const nativeFetch = window.fetch.bind(window)
- window.fetch = async (...args) => {
- const url = String((args[0] && args[0].url) || args[0])
- if (!isSnapshot(url)) return nativeFetch(...args)
- if (performance.now() < armAt) {
- const resp = await nativeFetch(...args)
- try {
- const clone = resp.clone()
- cache.set(url, { status: resp.status, body: await clone.json() })
- window.__t26.recorded += 1
- } catch { /* 非 JSON 不缓存 */ }
- return resp
- }
- const hit = cache.get(url)
- if (!hit) return nativeFetch(...args) // 未见过的 URL 照常取
- window.__t26.replayed += 1
- return { ok: hit.status >= 200 && hit.status < 300, status: hit.status, json: async () => hit.body, headers: { get: () => null } }
- }
- // XHR 路径同款(axios 可能走 XHR;real 模式下 cockpitApi 用的是 fetch 还是 XHR 未知,两条都拦)。
- const XHR = XMLHttpRequest.prototype
- const nativeOpen = XHR.open
- XHR.open = function (method, url, ...rest) {
- this.__t26url = String(url)
- return nativeOpen.call(this, method, url, ...rest)
- }
- const nativeSend = XHR.send
- XHR.send = function (...args) {
- const el = this
- if (!isSnapshot(el.__t26url) || performance.now() < armAt) return nativeSend.apply(el, args)
- const hit = cache.get(el.__t26url)
- if (!hit) return nativeSend.apply(el, args)
- window.__t26.replayed += 1
- const body = JSON.stringify(hit.body)
- Object.defineProperty(el, 'readyState', { configurable: true, get: () => 4 })
- Object.defineProperty(el, 'status', { configurable: true, get: () => hit.status })
- Object.defineProperty(el, 'responseText', { configurable: true, get: () => body })
- Object.defineProperty(el, 'response', { configurable: true, get: () => body })
- setTimeout(() => {
- el.dispatchEvent(new Event('readystatechange'))
- el.dispatchEvent(new Event('load'))
- el.dispatchEvent(new Event('loadend'))
- }, 5)
- return undefined
- }
- })
- 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,
- replayed: window.__t26.replayed,
- recorded: window.__t26.recorded,
- }
- })
- console.log(`录制 ${result.recorded} 页 / 回放 ${result.replayed} 页`)
- 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 late = result.longTasks.filter((t) => t.start > 16000 && t.dur >= 100)
- console.log(`15s 后长任务(≥100ms):${late.length} 个 ${JSON.stringify(late.slice(0, 10))}`)
- await browser.close()
|