PageSourceSearch

https://degenai.dev/js/plugins/chart-plugins/shared/frame-cost.1j0u0izqp8.js

js degenai.dev collected 2026-10-06 14:18:39 UTC 4,271 bytes, 112 lines download raw bytes

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.