Spaces:
Configuration error
Configuration error
| import { describe, expect, test } from "vitest"; | |
| import { buildPlan, CUMULATIVE_COMPONENT_SETS, estimateDurationMs, formatSeries, ISOLATED_COMPONENT_SETS, type BenchmarkStep } from "./benchmarkPlan"; | |
| import type { BenchmarkResult, BenchmarkRun, SceneApplied } from "./benchmarkRunner"; | |
| import { FrameSampler, JANK_MS, seriesStats } from "./frameSampler"; | |
| import { | |
| absoluteFitTrustworthy, | |
| formatTable, | |
| gpuTimerTrustworthy, | |
| hasFootprints, | |
| isDrifted, | |
| marginalCosts, | |
| memoryFits, | |
| memoryFitTrustworthy, | |
| propagationCosts, | |
| repeatChecks, | |
| reportRows, | |
| type RepeatCheck, | |
| scalingFits, | |
| thinRows, | |
| toCsv, | |
| } from "./report"; | |
| // A result carrying only what the report reads. `cpuMs` is the figure every | |
| // derived table differences, so it is the one the fixtures set deliberately. | |
| function result(options: { | |
| sats: number; | |
| components: string[]; | |
| clock?: number; | |
| cpuMs: number; | |
| gpuMs?: number; | |
| wallMs?: number; | |
| tickMs?: number; | |
| repeat?: boolean; | |
| buildMs?: number; | |
| visible?: number; | |
| frames?: number; | |
| heapMb?: readonly number[]; | |
| footprintMb?: number; | |
| }): BenchmarkResult { | |
| const clock = options.clock ?? 1; | |
| const components = options.components; | |
| const step: BenchmarkStep = { | |
| index: 0, | |
| satelliteCount: options.sats, | |
| components, | |
| clockMultiplier: clock, | |
| repeat: options.repeat ?? false, | |
| series: formatSeries(components, clock), | |
| label: "", | |
| }; | |
| const applied: SceneApplied = { | |
| satellitesRequested: options.sats, | |
| satellitesVisible: options.visible ?? options.sats, | |
| componentsRequested: components, | |
| componentsDrawn: components, | |
| componentInstances: {}, | |
| clockMultiplier: clock, | |
| entities: 0, | |
| primitives: 0, | |
| clearMs: 0, | |
| buildMs: options.buildMs ?? 0, | |
| }; | |
| const frames = options.frames ?? 100; | |
| return { | |
| step, | |
| applied, | |
| frames: { | |
| frames, | |
| elapsedMs: frames * 10, | |
| fps: 100, | |
| wall: seriesStats([options.wallMs ?? 10]), | |
| cpu: seriesStats([options.cpuMs]), | |
| tick: seriesStats([options.tickMs ?? 0]), | |
| gpu: options.gpuMs === undefined ? undefined : seriesStats([options.gpuMs]), | |
| heap: options.heapMb === undefined ? undefined : seriesStats(options.heapMb), | |
| jankFrames: 0, | |
| jankRatio: 0, | |
| }, | |
| footprint: options.footprintMb === undefined ? undefined : { totalMb: options.footprintMb * 1.4, jsMb: options.footprintMb, workerMb: 0, elapsedMs: 17_000 }, | |
| }; | |
| } | |
| const run = (results: BenchmarkResult[]): BenchmarkRun => ({ | |
| startedAtIso: "2026-01-01T00:00:00.000Z", | |
| spec: { satelliteCounts: [], componentSets: [] }, | |
| environment: {}, | |
| options: { warmupMs: 0, sampleMs: 0 }, | |
| catalogSize: 0, | |
| results, | |
| cancelled: false, | |
| }); | |
| describe("seriesStats", () => { | |
| test("no values means no stats, rather than a row of zeroes", () => { | |
| expect(seriesStats([])).toBeUndefined(); | |
| }); | |
| test("percentiles come off the sorted values, whatever order they arrived in", () => { | |
| const stats = seriesStats([50, 10, 30, 20, 40]); | |
| expect(stats).toMatchObject({ count: 5, min: 10, max: 50, mean: 30, p50: 30 }); | |
| }); | |
| test("the input array is left in its arrival order", () => { | |
| const values = [3, 1, 2]; | |
| seriesStats(values); | |
| expect(values).toEqual([3, 1, 2]); | |
| }); | |
| }); | |
| describe("FrameSampler", () => { | |
| test("the first push only sets the origin, so one timestamp is no frame", () => { | |
| const sampler = new FrameSampler(); | |
| sampler.push(1000); | |
| expect(sampler.frames).toBe(0); | |
| expect(sampler.snapshot().wall).toBeUndefined(); | |
| }); | |
| test("deltas between timestamps become the wall series", () => { | |
| const sampler = new FrameSampler(); | |
| for (const [index, time] of [0, 10, 20, 30].entries()) { | |
| sampler.push(1000 + time, index); | |
| } | |
| const snapshot = sampler.snapshot(); | |
| expect(snapshot.frames).toBe(3); | |
| expect(snapshot.elapsedMs).toBe(30); | |
| expect(snapshot.fps).toBeCloseTo(100); | |
| expect(snapshot.wall?.mean).toBe(10); | |
| }); | |
| test("reset bumps the epoch, so a reading in flight can tell it is stale", () => { | |
| // The GPU clock's only defence against attributing a warmup frame to the | |
| // sample, or the tail of one step to the next: a query records the epoch it | |
| // started in and the delivery is dropped when the sampler has moved on. | |
| const sampler = new FrameSampler(); | |
| const issued = sampler.epoch; | |
| sampler.pushGpu(5); | |
| expect(sampler.snapshot().gpu?.mean).toBe(5); | |
| sampler.reset(); | |
| expect(sampler.epoch).not.toBe(issued); | |
| // What the collector does with a stale reading: nothing. | |
| if (sampler.epoch === issued) { | |
| sampler.pushGpu(250); | |
| } | |
| expect(sampler.snapshot().gpu).toBeUndefined(); | |
| // A reading issued after the reset still lands. | |
| sampler.pushGpu(7); | |
| expect(sampler.snapshot().gpu?.mean).toBe(7); | |
| }); | |
| test("frames slower than 30 fps count as jank", () => { | |
| const sampler = new FrameSampler(); | |
| sampler.push(0); | |
| sampler.push(10); | |
| sampler.push(10 + JANK_MS + 1); | |
| const snapshot = sampler.snapshot(); | |
| expect(snapshot.jankFrames).toBe(1); | |
| expect(snapshot.jankRatio).toBeCloseTo(0.5); | |
| }); | |
| test("reset drops the samples but keeps the origin, so no frame is lost at the seam", () => { | |
| const sampler = new FrameSampler(); | |
| sampler.push(0); | |
| sampler.push(100); | |
| sampler.reset(); | |
| sampler.push(110); | |
| expect(sampler.frames).toBe(1); | |
| expect(sampler.snapshot().wall?.mean).toBe(10); | |
| }); | |
| test("a limit makes the window roll rather than grow", () => { | |
| const sampler = new FrameSampler(2); | |
| for (let i = 0; i <= 10; i += 1) { | |
| sampler.push(i * 10); | |
| } | |
| expect(sampler.frames).toBe(2); | |
| }); | |
| }); | |
| describe("buildPlan", () => { | |
| test("counts are deduplicated, sorted and cleared of nonsense", () => { | |
| const steps = buildPlan({ satelliteCounts: [100, 0, 100, -5, 10.5], componentSets: [["Point"]] }); | |
| expect(steps.filter((step) => !step.repeat).map((step) => step.satelliteCount)).toEqual([0, 100]); | |
| }); | |
| test("component sets are outermost so a cancelled sweep leaves whole series", () => { | |
| const steps = buildPlan({ satelliteCounts: [1, 2], componentSets: [["Point"], ["Label"]], repeatFirstStep: false }); | |
| expect(steps.map((step) => `${step.components.join()}@${step.satelliteCount}`)).toEqual(["Point@1", "Point@2", "Label@1", "Label@2"]); | |
| }); | |
| test("the clock is a real axis, and one value unless asked for", () => { | |
| const plain = buildPlan({ satelliteCounts: [1], componentSets: [["Point"]], repeatFirstStep: false }); | |
| expect(plain.map((step) => step.clockMultiplier)).toEqual([1]); | |
| const swept = buildPlan({ satelliteCounts: [1, 2], componentSets: [["Point"]], clockMultipliers: [1, 100], repeatFirstStep: false }); | |
| expect(swept.map((step) => `${step.clockMultiplier}@${step.satelliteCount}`)).toEqual(["1@1", "1@2", "100@1", "100@2"]); | |
| }); | |
| test("the series names the clock only when it is not real time", () => { | |
| expect(formatSeries(["Point"], 1)).toBe("Point"); | |
| expect(formatSeries(["Point"], 100)).toBe("Point @ ×100"); | |
| }); | |
| test("the first step is re-run last, and marked", () => { | |
| const steps = buildPlan({ satelliteCounts: [10, 20], componentSets: [["Point"]] }); | |
| const last = steps[steps.length - 1] as BenchmarkStep; | |
| expect(steps).toHaveLength(3); | |
| expect(last.repeat).toBe(true); | |
| expect(last.satelliteCount).toBe(10); | |
| expect(steps.filter((step) => step.repeat)).toHaveLength(1); | |
| }); | |
| test("a lone step is not repeated — there would be nothing to compare it against", () => { | |
| expect(buildPlan({ satelliteCounts: [10], componentSets: [["Point"]] })).toHaveLength(1); | |
| }); | |
| test("the cumulative sets grow by one component and start from nothing", () => { | |
| expect(CUMULATIVE_COMPONENT_SETS[0]).toEqual([]); | |
| expect(CUMULATIVE_COMPONENT_SETS[1]).toEqual(["Point"]); | |
| for (const [index, set] of CUMULATIVE_COMPONENT_SETS.entries()) { | |
| expect(set).toHaveLength(index); | |
| } | |
| }); | |
| test("the isolated sets are Point plus exactly one other", () => { | |
| expect(ISOLATED_COMPONENT_SETS[0]).toEqual(["Point"]); | |
| for (const set of ISOLATED_COMPONENT_SETS.slice(1)) { | |
| expect(set[0]).toBe("Point"); | |
| expect(set).toHaveLength(2); | |
| } | |
| }); | |
| }); | |
| describe("scalingFits", () => { | |
| test("a linear cost is reported as its slope per 1,000 satellites", () => { | |
| const fits = scalingFits( | |
| run([ | |
| result({ sats: 0, components: ["Point"], cpuMs: 5 }), | |
| result({ sats: 500, components: ["Point"], cpuMs: 10 }), | |
| result({ sats: 1000, components: ["Point"], cpuMs: 15 }), | |
| ]), | |
| ); | |
| expect(fits).toHaveLength(1); | |
| expect(fits[0]).toMatchObject({ series: "Point", mainMsPer1000: 10, baseMainMs: 5, r2: 1 }); | |
| }); | |
| test("clock rates are separate series, never one averaged fit", () => { | |
| const fits = scalingFits( | |
| run([ | |
| result({ sats: 0, components: ["Point"], cpuMs: 5 }), | |
| result({ sats: 1000, components: ["Point"], cpuMs: 10 }), | |
| result({ sats: 0, components: ["Point"], clock: 100, cpuMs: 20 }), | |
| result({ sats: 1000, components: ["Point"], clock: 100, cpuMs: 60 }), | |
| ]), | |
| ); | |
| expect(fits.map((fit) => fit.series)).toEqual(["Point", "Point @ ×100"]); | |
| expect(fits[1]?.mainMsPer1000).toBe(40); | |
| }); | |
| test("the repeat step is left out, so it cannot weight one point twice", () => { | |
| const fits = scalingFits( | |
| run([ | |
| result({ sats: 0, components: ["Point"], cpuMs: 5 }), | |
| result({ sats: 1000, components: ["Point"], cpuMs: 15 }), | |
| result({ sats: 0, components: ["Point"], cpuMs: 500, repeat: true }), | |
| ]), | |
| ); | |
| expect(fits[0]).toMatchObject({ points: 2, mainMsPer1000: 10, baseMainMs: 5 }); | |
| }); | |
| // The defect: cpuMs excludes the clock tick, where most per-satellite work | |
| // happens, so fitting it alone had the Point series holding 60 fps into the | |
| // millions. The fit is over cpuMs + tickMs. | |
| test("the fit counts the clock tick, not just the render", () => { | |
| const fits = scalingFits(run([result({ sats: 0, components: ["Point"], cpuMs: 1, tickMs: 0 }), result({ sats: 1000, components: ["Point"], cpuMs: 1, tickMs: 10 })])); | |
| // cpuMs is flat across these two rows; all the growth is in the tick. | |
| expect(fits[0]?.mainMsPer1000).toBe(10); | |
| expect(fits[0]?.baseMainMs).toBe(1); | |
| }); | |
| test("satsAt60fps is blank when the floor alone has eaten the budget", () => { | |
| const fits = scalingFits(run([result({ sats: 0, components: ["Point"], cpuMs: 1, wallMs: 40 }), result({ sats: 1000, components: ["Point"], cpuMs: 2, wallMs: 42 })])); | |
| // Main-thread work is trivial and its slope would extrapolate to a huge | |
| // count, but every frame took 40 ms regardless — GPU or vsync bound. No | |
| // satellite count is the reason 60 fps is unavailable. | |
| expect(fits[0]?.floorMs).toBe(40); | |
| expect(fits[0]?.satsAt60fps).toBe(""); | |
| }); | |
| test("satsAt60fps is blank when the fixed cost already blows the budget", () => { | |
| const fits = scalingFits(run([result({ sats: 0, components: ["Point"], cpuMs: 30 }), result({ sats: 1000, components: ["Point"], cpuMs: 40 })])); | |
| expect(fits[0]?.satsAt60fps).toBe(""); | |
| }); | |
| }); | |
| describe("marginalCosts", () => { | |
| test("a set is differenced against the largest subset measured beside it", () => { | |
| const costs = marginalCosts( | |
| run([ | |
| result({ sats: 100, components: ["Point"], cpuMs: 10 }), | |
| result({ sats: 100, components: ["Point", "Label"], cpuMs: 14 }), | |
| result({ sats: 100, components: ["Point", "Label", "Orbit"], cpuMs: 20 }), | |
| ]), | |
| ); | |
| expect(costs).toEqual([ | |
| { sats: 100, clock: 1, added: "Label", over: "Point", deltaMainMs: 4, usPerSatellite: 40 }, | |
| { sats: 100, clock: 1, added: "Orbit", over: "Point + Label", deltaMainMs: 6, usPerSatellite: 60 }, | |
| ]); | |
| }); | |
| test("rows under different clocks are never differenced against each other", () => { | |
| const costs = marginalCosts(run([result({ sats: 100, components: ["Point"], cpuMs: 10 }), result({ sats: 100, components: ["Point", "Label"], clock: 100, cpuMs: 90 })])); | |
| expect(costs).toEqual([]); | |
| }); | |
| test("a set with no subset beside it yields no row rather than a bogus baseline", () => { | |
| const costs = marginalCosts(run([result({ sats: 100, components: ["Label"], cpuMs: 10 }), result({ sats: 100, components: ["Orbit"], cpuMs: 20 })])); | |
| expect(costs).toEqual([]); | |
| }); | |
| }); | |
| describe("propagationCosts", () => { | |
| test("each rate is differenced against x1 for the same scene", () => { | |
| const costs = propagationCosts( | |
| run([ | |
| result({ sats: 200, components: ["Point"], cpuMs: 1, tickMs: 10 }), | |
| result({ sats: 200, components: ["Point"], clock: 100, cpuMs: 1, tickMs: 30 }), | |
| result({ sats: 200, components: ["Point"], clock: 1000, cpuMs: 1, tickMs: 110 }), | |
| ]), | |
| ); | |
| expect(costs.map((cost) => [cost.clock, cost.deltaTickMs, cost.usPerSatellite])).toEqual([ | |
| [100, 20, 100], | |
| [1000, 100, 500], | |
| ]); | |
| }); | |
| // The defect this table was rewritten for: propagation happens in clock.onTick, | |
| // which runs before preUpdate and so is outside cpuMs entirely. Differencing | |
| // cpuMs reported nothing for a step that had ground to a halt propagating. | |
| test("sees a cost that cpuMs cannot", () => { | |
| const costs = propagationCosts( | |
| run([result({ sats: 5000, components: ["Point"], cpuMs: 1.28, tickMs: 0.2 }), result({ sats: 5000, components: ["Point"], clock: 10000, cpuMs: 1.2, tickMs: 460 })]), | |
| ); | |
| // cpuMs went *down* between these two rows; the tick went up by 460 ms. | |
| expect(costs).toHaveLength(1); | |
| expect(costs[0]?.deltaTickMs).toBeCloseTo(459.8, 1); | |
| expect(costs[0]?.usPerSatellite).toBeGreaterThan(90); | |
| expect(costs[0]?.cpuMs).toBeLessThan(costs[0]?.tickMs ?? 0); | |
| }); | |
| test("without a x1 row there is nothing to difference against", () => { | |
| const costs = propagationCosts( | |
| run([result({ sats: 200, components: ["Point"], clock: 100, cpuMs: 1, tickMs: 30 }), result({ sats: 200, components: ["Point"], clock: 1000, cpuMs: 1, tickMs: 110 })]), | |
| ); | |
| expect(costs).toEqual([]); | |
| }); | |
| test("a run that never swept the clock reports nothing at all", () => { | |
| expect(propagationCosts(run([result({ sats: 200, components: ["Point"], cpuMs: 10 })]))).toEqual([]); | |
| }); | |
| }); | |
| describe("repeatChecks", () => { | |
| test("the repeat is matched to its original and reported as drift", () => { | |
| const checks = repeatChecks( | |
| run([ | |
| result({ sats: 100, components: ["Point"], cpuMs: 10, buildMs: 800 }), | |
| result({ sats: 500, components: ["Point"], cpuMs: 20, buildMs: 100 }), | |
| result({ sats: 100, components: ["Point"], cpuMs: 11, buildMs: 200, repeat: true }), | |
| ]), | |
| ); | |
| expect(checks).toHaveLength(1); | |
| expect(checks[0]).toMatchObject({ sats: 100, firstMainMs: 10, repeatMainMs: 11, mainDriftPct: 10, firstBuildMs: 800, repeatBuildMs: 200, buildDriftPct: -75 }); | |
| }); | |
| test("drift is only untrustworthy when it is both proportional and material", () => { | |
| const check = (firstMainMs: number, repeatMainMs: number): RepeatCheck => ({ | |
| sats: 0, | |
| components: "Point", | |
| clock: 1, | |
| firstMainMs, | |
| repeatMainMs, | |
| mainDriftPct: ((repeatMainMs - firstMainMs) / firstMainMs) * 100, | |
| firstBuildMs: 0, | |
| repeatBuildMs: 0, | |
| buildDriftPct: 0, | |
| }); | |
| // A fifth of a millisecond on a two-millisecond control step. This is what a | |
| // clean run looks like at the bottom of the range, and it used to warn. | |
| expect(isDrifted(check(2.2, 1.79))).toBe(false); | |
| // The same proportion where it is worth milliseconds. | |
| expect(isDrifted(check(11, 18))).toBe(true); | |
| // Material but proportionally small: a big scene that moved a little. | |
| expect(isDrifted(check(200, 202))).toBe(false); | |
| }); | |
| test("a run without a repeat step reports nothing", () => { | |
| expect(repeatChecks(run([result({ sats: 100, components: ["Point"], cpuMs: 10 })]))).toEqual([]); | |
| }); | |
| }); | |
| describe("gpu timing", () => { | |
| test("a plausible GPU time is reported", () => { | |
| const rows = reportRows(run([result({ sats: 0, components: ["Point"], cpuMs: 1, gpuMs: 9, wallMs: 14 })])); | |
| expect(rows[0]?.gpuMs).toBe(9); | |
| }); | |
| test("a GPU-bound scene, where gpu approaches the frame interval, is still believed", () => { | |
| const rows = reportRows(run([result({ sats: 0, components: ["Point"], cpuMs: 1, gpuMs: 13.5, wallMs: 14 })])); | |
| expect(rows[0]?.gpuMs).toBe(13.5); | |
| }); | |
| test("a timer claiming more GPU than the frame it presented in is withheld", () => { | |
| // The ANGLE/Metal case: 49 ms of "GPU" against frames arriving every 14 ms. | |
| // A frame that presented cannot have cost that, so the column goes blank | |
| // rather than print it. | |
| const bad = run([result({ sats: 0, components: ["Point"], cpuMs: 1, gpuMs: 49, wallMs: 14 })]); | |
| expect(gpuTimerTrustworthy(bad)).toBe(false); | |
| expect(reportRows(bad)[0]?.gpuMs).toBe(""); | |
| }); | |
| test("one odd step does not discredit a run, a majority does", () => { | |
| const mostlyFine = run([ | |
| result({ sats: 0, components: ["Point"], cpuMs: 1, gpuMs: 9, wallMs: 14 }), | |
| result({ sats: 1, components: ["Point"], cpuMs: 1, gpuMs: 9, wallMs: 14 }), | |
| result({ sats: 2, components: ["Point"], cpuMs: 1, gpuMs: 60, wallMs: 14 }), | |
| ]); | |
| expect(gpuTimerTrustworthy(mostlyFine)).toBe(true); | |
| const mostlyBad = run([ | |
| result({ sats: 0, components: ["Point"], cpuMs: 1, gpuMs: 60, wallMs: 14 }), | |
| result({ sats: 1, components: ["Point"], cpuMs: 1, gpuMs: 60, wallMs: 14 }), | |
| result({ sats: 2, components: ["Point"], cpuMs: 1, gpuMs: 9, wallMs: 14 }), | |
| ]); | |
| expect(gpuTimerTrustworthy(mostlyBad)).toBe(false); | |
| }); | |
| test("no GPU samples at all is blank, not zero", () => { | |
| const none = run([result({ sats: 0, components: ["Point"], cpuMs: 1 })]); | |
| expect(gpuTimerTrustworthy(none)).toBe(false); | |
| expect(reportRows(none)[0]?.gpuMs).toBe(""); | |
| }); | |
| test("the sampler keeps GPU timings as their own population", () => { | |
| const sampler = new FrameSampler(); | |
| sampler.push(0); | |
| sampler.push(10, 1); | |
| sampler.push(20, 1); | |
| // Two frames, one GPU result — they arrive out of step and are not paired. | |
| sampler.pushGpu(8); | |
| const snap = sampler.snapshot(); | |
| expect(snap.frames).toBe(2); | |
| expect(snap.gpu?.count).toBe(1); | |
| expect(snap.gpu?.mean).toBe(8); | |
| }); | |
| test("reset clears GPU timings with everything else", () => { | |
| const sampler = new FrameSampler(); | |
| sampler.pushGpu(8); | |
| sampler.reset(); | |
| expect(sampler.snapshot().gpu).toBeUndefined(); | |
| }); | |
| }); | |
| describe("thinRows", () => { | |
| test("rows sampled over too few frames are the ones flagged", () => { | |
| const thin = thinRows(run([result({ sats: 0, components: ["Point"], cpuMs: 1, frames: 3 }), result({ sats: 1, components: ["Point"], cpuMs: 1, frames: 300 })])); | |
| expect(thin.map((row) => row.frames)).toEqual([3]); | |
| }); | |
| }); | |
| describe("toCsv", () => { | |
| test("the header names every column of the row beneath it", () => { | |
| const csv = toCsv(run([result({ sats: 1, components: ["Point", "Label"], cpuMs: 1 })])); | |
| const [header, row] = csv.split("\n"); | |
| expect(header?.split(",")).toContain("components"); | |
| expect(row?.split(",")).toHaveLength(header?.split(",").length ?? 0); | |
| }); | |
| test("a value carrying a comma is quoted rather than left to split the row", () => { | |
| // Component names must not contain commas (see CONTEXT.md), so this is the | |
| // quoting being defensive rather than a case the app can reach — and the row | |
| // staying one field is the whole point of it. | |
| const csv = toCsv(run([result({ sats: 1, components: ["Point", "A,B"], cpuMs: 1 })])); | |
| const [header, row] = csv.split("\n"); | |
| expect(row).toContain('"Point + A,B"'); | |
| expect(row?.split(",")).toHaveLength((header?.split(",").length ?? 0) + 1); | |
| }); | |
| test("an empty run is an empty string rather than a lone header", () => { | |
| expect(toCsv(run([]))).toBe(""); | |
| }); | |
| }); | |
| describe("memoryFits", () => { | |
| // The whole reason this is a slope and not a column: the garbage offset is | |
| // common to the rows of one series, so it lands in the intercept. These two | |
| // series carry wildly different offsets (+400 MB apart) over the same | |
| // per-satellite cost, and must produce the same slope. | |
| test("the garbage offset lands in the intercept, not the slope", () => { | |
| const clean = run([ | |
| result({ sats: 0, components: ["Point"], cpuMs: 1, visible: 0, heapMb: [40] }), | |
| result({ sats: 1000, components: ["Point"], cpuMs: 1, visible: 1000, heapMb: [93] }), | |
| result({ sats: 5000, components: ["Point"], cpuMs: 1, visible: 5000, heapMb: [310] }), | |
| ]); | |
| const dirty = run([ | |
| result({ sats: 0, components: ["Point"], cpuMs: 1, visible: 0, heapMb: [440] }), | |
| result({ sats: 1000, components: ["Point"], cpuMs: 1, visible: 1000, heapMb: [493] }), | |
| result({ sats: 5000, components: ["Point"], cpuMs: 1, visible: 5000, heapMb: [710] }), | |
| ]); | |
| expect(memoryFits(clean)[0]?.mbPer1000Sats).toBe(memoryFits(dirty)[0]?.mbPer1000Sats); | |
| expect(memoryFits(clean)[0]?.baseMb).not.toBe(memoryFits(dirty)[0]?.baseMb); | |
| }); | |
| test("reports the slope per 1,000 and per satellite", () => { | |
| const fits = memoryFits( | |
| run([ | |
| result({ sats: 0, components: ["Point"], cpuMs: 1, visible: 0, heapMb: [40] }), | |
| result({ sats: 5000, components: ["Point"], cpuMs: 1, visible: 5000, heapMb: [40 + 250] }), | |
| ]), | |
| ); | |
| expect(fits[0]?.mbPer1000Sats).toBe(50); | |
| // 50 MB per 1,000 is 51.2 KB each, not 50 — the unit change is binary. | |
| expect(fits[0]?.kbPerSatellite).toBe(51.2); | |
| expect(fits[0]?.baseMb).toBe(40); | |
| }); | |
| test("fits against satellites drawn, not the count asked for", () => { | |
| const fits = memoryFits( | |
| run([ | |
| result({ sats: 0, components: ["Point"], cpuMs: 1, visible: 0, heapMb: [10] }), | |
| // Only half applied, so the same heap growth is twice the cost each. | |
| result({ sats: 1000, components: ["Point"], cpuMs: 1, visible: 500, heapMb: [60] }), | |
| ]), | |
| ); | |
| expect(fits[0]?.mbPer1000Sats).toBe(100); | |
| }); | |
| test("clock rates are separate series, as everywhere else", () => { | |
| const fits = memoryFits( | |
| run([ | |
| result({ sats: 0, components: ["Point"], cpuMs: 1, visible: 0, heapMb: [10], clock: 1 }), | |
| result({ sats: 1000, components: ["Point"], cpuMs: 1, visible: 1000, heapMb: [60], clock: 1 }), | |
| result({ sats: 0, components: ["Point"], cpuMs: 1, visible: 0, heapMb: [10], clock: 100 }), | |
| result({ sats: 1000, components: ["Point"], cpuMs: 1, visible: 1000, heapMb: [110], clock: 100 }), | |
| ]), | |
| ); | |
| expect(fits.map((fit) => fit.mbPer1000Sats)).toEqual([50, 100]); | |
| }); | |
| // The failure this fit actually has, caught in the wild: a major collection fell | |
| // between the zero row and the next, so the offset stopped being common and the | |
| // slope came out negative — memory apparently freed by drawing satellites. r² is | |
| // the only thing standing between that and being read as a finding. | |
| test("a collection landing mid-series shows up as scatter, not a plausible slope", () => { | |
| const fits = memoryFits( | |
| run([ | |
| result({ sats: 0, components: ["Point"], cpuMs: 1, visible: 0, heapMb: [419.2] }), | |
| result({ sats: 1000, components: ["Point"], cpuMs: 1, visible: 1001, heapMb: [100.8] }), | |
| result({ sats: 5000, components: ["Point"], cpuMs: 1, visible: 5001, heapMb: [299.9] }), | |
| ]), | |
| ); | |
| expect(fits[0]?.mbPer1000Sats).toBeLessThan(0); | |
| expect(memoryFitTrustworthy(fits[0]!)).toBe(false); | |
| }); | |
| test("a clean series clears the r² guard comfortably", () => { | |
| const fits = memoryFits( | |
| run([ | |
| result({ sats: 0, components: ["Point"], cpuMs: 1, visible: 0, heapMb: [50.7] }), | |
| result({ sats: 1000, components: ["Point"], cpuMs: 1, visible: 1001, heapMb: [95.4] }), | |
| result({ sats: 5000, components: ["Point"], cpuMs: 1, visible: 5001, heapMb: [310.5] }), | |
| ]), | |
| ); | |
| // The real measurement this is taken from: 53.7 KB per satellite against a | |
| // forced-collection truth of 52.5. | |
| expect(fits[0]?.kbPerSatellite).toBeCloseTo(53.7, 0); | |
| expect(memoryFitTrustworthy(fits[0]!)).toBe(true); | |
| }); | |
| // Two points always lie on their own line, so r² alone would wave this through | |
| // at 1.000 — which is precisely where the offset assumption is least tested. | |
| test("a two-count sweep is refused however well it fits", () => { | |
| const fits = memoryFits( | |
| run([result({ sats: 0, components: ["Point"], cpuMs: 1, visible: 0, heapMb: [53] }), result({ sats: 100, components: ["Point"], cpuMs: 1, visible: 100, heapMb: [73.4] })]), | |
| ); | |
| expect(fits[0]?.r2).toBe(1); | |
| expect(memoryFitTrustworthy(fits[0]!)).toBe(false); | |
| }); | |
| test("no heap reading means no rows, rather than a fit through zeroes", () => { | |
| // A fit through absent data would report every satellite as free, which is | |
| // worse than an empty table saying the browser cannot answer. | |
| const fits = memoryFits(run([result({ sats: 0, components: ["Point"], cpuMs: 1, visible: 0 }), result({ sats: 5000, components: ["Point"], cpuMs: 1, visible: 5000 })])); | |
| expect(fits).toEqual([]); | |
| }); | |
| test("the repeat step is excluded, as it is from every other fit", () => { | |
| const fits = memoryFits( | |
| run([ | |
| result({ sats: 0, components: ["Point"], cpuMs: 1, visible: 0, heapMb: [10] }), | |
| result({ sats: 1000, components: ["Point"], cpuMs: 1, visible: 1000, heapMb: [60] }), | |
| result({ sats: 0, components: ["Point"], cpuMs: 1, visible: 0, heapMb: [500], repeat: true }), | |
| ]), | |
| ); | |
| expect(fits[0]?.points).toBe(2); | |
| expect(fits[0]?.mbPer1000Sats).toBe(50); | |
| }); | |
| }); | |
| describe("the footprint capture", () => { | |
| const withFootprints = (points: readonly { visible: number; jsMb: number; floorMb: number }[]) => | |
| run(points.map((point) => result({ sats: point.visible, visible: point.visible, components: ["Point"], cpuMs: 1, heapMb: [point.floorMb], footprintMb: point.jsMb }))); | |
| // The two slopes are independent derivations of one quantity: one differenced | |
| // out of garbage-contaminated floors, one measured after a collection. That is | |
| // the whole reason both are reported. | |
| test("the measured slope sits beside the derived one", () => { | |
| const fits = memoryFits( | |
| withFootprints([ | |
| { visible: 0, jsMb: 30.2, floorMb: 50.7 }, | |
| { visible: 1001, jsMb: 82.8, floorMb: 95.4 }, | |
| { visible: 5001, jsMb: 287.7, floorMb: 310.5 }, | |
| ]), | |
| ); | |
| // Both land near the 52.5 KB per satellite a forced collection reported. | |
| expect(fits[0]?.kbPerSatellite).toBeCloseTo(53.7, 0); | |
| expect(fits[0]?.absoluteKbPerSatellite).toBeCloseTo(52.7, 0); | |
| // Three captures for three counts, so the absolute slope stands on its own. | |
| expect(absoluteFitTrustworthy(fits[0]!)).toBe(true); | |
| }); | |
| test("blank where the run never captured one, rather than zero", () => { | |
| const fits = memoryFits( | |
| run([result({ sats: 0, components: ["Point"], cpuMs: 1, visible: 0, heapMb: [10] }), result({ sats: 1000, components: ["Point"], cpuMs: 1, visible: 1000, heapMb: [60] })]), | |
| ); | |
| expect(fits[0]?.absoluteKbPerSatellite).toBeUndefined(); | |
| expect(reportRows(run([result({ sats: 0, components: ["Point"], cpuMs: 1 })]))[0]?.footprintMb).toBe(""); | |
| }); | |
| // The review finding: `measureFootprint` can be refused for one step, which | |
| // leaves the absolute fit two points wide while the floor fit beside it still has | |
| // three. Borrowing the floor fit's r² would print that under a green guard. | |
| test("a refused capture is judged on its own points, not the floor fit's", () => { | |
| const fits = memoryFits( | |
| run([ | |
| result({ sats: 0, components: ["Point"], cpuMs: 1, visible: 0, heapMb: [50.7], footprintMb: 30.2 }), | |
| // This step's capture was refused: heap floor present, footprint absent. | |
| result({ sats: 1000, components: ["Point"], cpuMs: 1, visible: 1001, heapMb: [95.4] }), | |
| result({ sats: 5000, components: ["Point"], cpuMs: 1, visible: 5001, heapMb: [310.5], footprintMb: 287.7 }), | |
| ]), | |
| ); | |
| // The floor fit is fine — three points, tight. | |
| expect(memoryFitTrustworthy(fits[0]!)).toBe(true); | |
| // The absolute one is not, and says so rather than borrowing that verdict. | |
| expect(fits[0]?.absolutePoints).toBe(2); | |
| expect(fits[0]?.absoluteKbPerSatellite).not.toBeUndefined(); | |
| expect(absoluteFitTrustworthy(fits[0]!)).toBe(false); | |
| }); | |
| test("hasFootprints is the one predicate both readers use", () => { | |
| expect(hasFootprints(run([result({ sats: 0, components: ["Point"], cpuMs: 1, heapMb: [40] })]))).toBe(false); | |
| expect(hasFootprints(run([result({ sats: 0, components: ["Point"], cpuMs: 1, heapMb: [40], footprintMb: 30 })]))).toBe(true); | |
| }); | |
| test("the absolute figures reach the rows, total and js apart", () => { | |
| const rows = reportRows(withFootprints([{ visible: 5001, jsMb: 297.4, floorMb: 310.5 }])); | |
| expect(rows[0]?.footprintMb).toBe(297.4); | |
| // The total counts DOM and worker memory too, so it is the larger number. | |
| expect(Number(rows[0]?.footprintTotalMb)).toBeGreaterThan(297.4); | |
| }); | |
| // Same rule the gpu column follows: a column of dashes says nothing an absent | |
| // column does not. | |
| test("the pasted table grows a memory column only when one was captured", () => { | |
| expect(formatTable(withFootprints([{ visible: 0, jsMb: 30, floorMb: 50 }])).split("\n")[0]).toContain("footprint"); | |
| expect(formatTable(run([result({ sats: 0, components: ["Point"], cpuMs: 1 })])).split("\n")[0]).not.toContain("footprint"); | |
| }); | |
| }); | |
| describe("estimateDurationMs with a footprint capture", () => { | |
| test("the capture is added per step, not amortised", () => { | |
| const steps = buildPlan({ satelliteCounts: [0, 100, 500], componentSets: [["Point"]], repeatFirstStep: false }); | |
| const plain = estimateDurationMs(steps, 6000); | |
| const withCapture = estimateDurationMs(steps, 6000, 17_000); | |
| expect(withCapture - plain).toBe(steps.length * 17_000); | |
| }); | |
| test("no capture leaves the estimate exactly as it was", () => { | |
| const steps = buildPlan({ satelliteCounts: [0, 100], componentSets: [["Point"]], repeatFirstStep: false }); | |
| expect(estimateDurationMs(steps, 6000, 0)).toBe(estimateDurationMs(steps, 6000)); | |
| }); | |
| }); | |
| describe("the raw heap columns", () => { | |
| test("stay out of the pasted table but remain in the csv", () => { | |
| const one = run([result({ sats: 0, components: ["Point"], cpuMs: 1, heapMb: [40, 50] })]); | |
| expect(formatTable(one).split("\n")[0]).not.toContain("heap"); | |
| expect(toCsv(one).split("\n")[0]?.split(",")).toEqual(expect.arrayContaining(["heapMb", "heapPeakMb"])); | |
| }); | |
| test("carry the window's floor and peak, not an average", () => { | |
| const rows = reportRows(run([result({ sats: 5000, components: ["Point"], cpuMs: 1, heapMb: [413, 462.2, 86.3, 300] })])); | |
| expect(rows[0]?.heapMb).toBe(86.3); | |
| expect(rows[0]?.heapPeakMb).toBe(462.2); | |
| }); | |
| test("blank on a browser that offers no reading, rather than zero", () => { | |
| const rows = reportRows(run([result({ sats: 0, components: ["Point"], cpuMs: 1 })])); | |
| expect(rows[0]?.heapMb).toBe(""); | |
| expect(rows[0]?.heapPeakMb).toBe(""); | |
| }); | |
| }); | |
| describe("FrameSampler heap series", () => { | |
| test("a frame without a reading is still a frame", () => { | |
| const sampler = new FrameSampler(); | |
| sampler.push(0); | |
| sampler.push(10); | |
| sampler.push(20); | |
| expect(sampler.snapshot().frames).toBe(2); | |
| expect(sampler.snapshot().heap).toBeUndefined(); | |
| }); | |
| test("the heap is its own population, min and max over the window", () => { | |
| const sampler = new FrameSampler(); | |
| sampler.push(0); | |
| sampler.push(10); | |
| sampler.pushHeap(413); | |
| sampler.pushHeap(86.3); | |
| sampler.pushHeap(462.2); | |
| const heap = sampler.snapshot().heap; | |
| expect(heap?.min).toBe(86.3); | |
| expect(heap?.max).toBe(462.2); | |
| expect(heap?.count).toBe(3); | |
| }); | |
| test("reset drops the heap samples with the rest of the warmup", () => { | |
| const sampler = new FrameSampler(); | |
| sampler.pushHeap(999); | |
| sampler.reset(); | |
| sampler.pushHeap(40); | |
| expect(sampler.snapshot().heap?.max).toBe(40); | |
| }); | |
| test("a limit rolls the heap window too, so the live readout stays recent", () => { | |
| const sampler = new FrameSampler(2); | |
| sampler.pushHeap(900); | |
| sampler.pushHeap(41); | |
| sampler.pushHeap(42); | |
| expect(sampler.snapshot().heap?.max).toBe(42); | |
| }); | |
| }); | |