1/** 2 * Per-layer frame-cost sampler. 3 * 4 * Interaction jank is a distribution problem, not an average problem: a layer that costs 0.3 ms 5 * on a typical frame but 40 ms on the frame where it rebuilds its model still feels broken, 6 * because that one frame is the one the user's finger is on. So this records a rolling window of 7 * per-frame durations per layer and reports p50/p95/max, plus a counter breakdown of WHICH path 8 * each frame took (for the heatmap: "compute" = model rebuilt, "remap" = cached model reprojected 9 * to new pixels). The compute-to-remap ratio is the direct measure of whether viewport decoupling 10 * is actually working -- during a pan every frame should be a remap. 11 * 12 * Sampling is opt-in per layer and off by default: no chart code pays for it unless something 13 * calls enableFrameCost(). The window is bounded, so leaving it on does not grow memory. 14 * 15 * Read it from the browser console during a drag: 16 * window.__chartFrameCost.enable(); // pan around; then: 17 * window.__chartFrameCost.report(); 18 */ 19 20const MAX_SAMPLES = 240; // ~4s of samples at 60fps; enough for a full drag gesture. 21 22let enabled = false; 23const layers = new Map(); // layer name -> { samples: number[], paths: (string|null)[], next: int } 24 25function layerFor(name) { 26 let entry = layers.get(name); 27 if (!entry) { 28 entry = { samples: [], paths: [], next: 0 }; 29 layers.set(name, entry); 30 } 31 return entry; 32} 33 34export function enableFrameCost(on = true) { 35 enabled = !!on; 36 if (enabled) return; 37 layers.clear(); 38} 39 40export function isFrameCostEnabled() { 41 return enabled; 42} 43 44export function resetFrameCost() { 45 layers.clear(); 46} 47 48/** 49 * Record one frame's cost for a layer, and optionally which render path it took. 50 * Cheap enough to call unconditionally -- it returns immediately when disabled. 51 * 52 * The path travels IN the ring alongside its duration rather than into a running tally, so the 53 * path counts describe the same window the timings do. A lifetime tally next to a windowed p95 54 * would read as "2 computes / 118 remaps" long after those 2 computes had aged out of the window 55 * -- and the compute-to-remap ratio for the frames you just dragged is the entire point here. 56 */ 57export function sampleFrame(layer, durationMs, path) { 58 if (!enabled || !Number.isFinite(durationMs)) return; 59 const entry = layerFor(layer); 60 if (entry.samples.length < MAX_SAMPLES) { 61 entry.samples.push(durationMs); 62 entry.paths.push(path || null); 63 } else { 64 entry.samples[entry.next] = durationMs; // ring buffer once full 65 entry.paths[entry.next] = path || null; 66 entry.next = (entry.next + 1) % MAX_SAMPLES; 67 } 68} 69 70function percentile(sorted, p) { 71 if (sorted.length === 0) return 0; 72 const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil(p * sorted.length) - 1)); 73 return sorted[idx]; 74} 75 76const round2 = (n) => Math.round(n * 100) / 100; 77 78/** 79 * Per-layer { frames, p50, p95, max, mean, paths } with millisecond figures. 80 * `paths` is a plain object of path-name -> frame count (e.g. { remap: 118, compute: 2 }), 81 * counted over the SAME window as the timings -- so the counts sum to `frames`, minus any 82 * frames that were sampled without naming a path. 83 */ 84export function getFrameCostReport() { 85 const out = {}; 86 for (const [name, entry] of layers) { 87 const sorted = entry.samples.slice().sort((a, b) => a - b); 88 const sum = sorted.reduce((acc, v) => acc + v, 0); 89 const paths = {}; 90 for (const p of entry.paths) { 91 if (p) paths[p] = (paths[p] || 0) + 1; 92 } 93 out[name] = { 94 frames: sorted.length, 95 p50: round2(percentile(sorted, 0.5)), 96 p95: round2(percentile(sorted, 0.95)), 97 max: round2(sorted.length > 0 ? sorted[sorted.length - 1] : 0), 98 mean: round2(sorted.length > 0 ? sum / sorted.length : 0), 99 paths, 100 }; 101 } 102 return out; 103} 104 105// Console handle. Guarded so the module stays importable under jsdom/node test runs. 106if (typeof window !== 'undefined') { 107 window.__chartFrameCost = { 108 enable: (on = true) => enableFrameCost(on), 109 reset: resetFrameCost, 110 report: getFrameCostReport, 111 }; 112}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.