Files
Oxicloud/frontend/src/lib/components/round14.bench.test.ts
T

146 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Round-14 frontend micro-pack (benches/ROUND14.md §F1, §F2).
//
// Each section is BEFORE (verbatim replica of the shipped shape) vs AFTER
// (proposed shape), with an equivalence gate and a wall-time perf gate — the
// same discipline as the Rust micro-packs: an AFTER that doesn't beat its
// BEFORE fails the gate.
import { describe, expect, it } from 'vitest';
// ────────────────────────────────────────────────────────────────────────────
// [F2] favorites `favoriteIds` — rebuild-a-fresh-Set-per-page vs incremental
// ────────────────────────────────────────────────────────────────────────────
//
// Audit finding: the favorites route derived `favoriteIds = new SvelteSet(
// items.map(i => i.id))`. Every infinite-scroll page (`raw = [...raw, ...page]`)
// rebuilt a brand-new set over the WHOLE accumulated list — O(N) per page,
// O(N²) across a P-page drain — and, being a new instance each page,
// invalidated every mounted star reader. The fix keeps one persistent set and
// `add`s only the fresh page's ids (clear on reset). Since every item on the
// page is a favorite and removed items aren't rendered, the set only has to be
// a superset of the displayed ids, so `add`-only is correct.
/** A page of ids (50/page, the default page size). */
function pageOf(start: number, n: number): string[] {
return Array.from({ length: n }, (_, i) => `fav-${start + i}`);
}
/** BEFORE: rebuild a fresh Set over the whole accumulated list each page. */
function rebuildPerPage(pages: string[][]): Set<string> {
let acc: string[] = [];
let set = new Set<string>();
for (const page of pages) {
acc = [...acc, ...page]; // the route's `raw = [...raw, ...page]`
set = new Set(acc.map((id) => id)); // new instance + O(N) rebuild
}
return set;
}
/** AFTER: one persistent set, add only the fresh page's ids. */
function incrementalPerPage(pages: string[][]): Set<string> {
const set = new Set<string>();
for (const page of pages) {
for (const id of page) set.add(id);
}
return set;
}
describe('round14 §F2 — favorites favoriteIds incremental set', () => {
it('final membership is identical (equivalence gate)', () => {
const pages = Array.from({ length: 20 }, (_, p) => pageOf(p * 50, 50));
const before = rebuildPerPage(pages);
const after = incrementalPerPage(pages);
expect(after.size).toBe(before.size);
for (const id of before) expect(after.has(id)).toBe(true);
for (const id of after) expect(before.has(id)).toBe(true);
});
it('a P-page drain builds the set ≥5x faster incrementally (perf gate)', () => {
const PAGES = 40;
const PER = 50; // 2 000 items total
const pages = Array.from({ length: PAGES }, (_, p) => pageOf(p * PER, PER));
const run = (f: (p: string[][]) => Set<string>): number => {
const t0 = performance.now();
// A few repetitions so the measurement isn't dominated by timer noise.
for (let r = 0; r < 20; r++) f(pages);
return performance.now() - t0;
};
// Warm-up (JIT) then measure.
run(rebuildPerPage);
run(incrementalPerPage);
const beforeMs = run(rebuildPerPage);
const afterMs = run(incrementalPerPage);
console.info(
`§F2 ${PAGES} pages × ${PER}: rebuild-per-page ${beforeMs.toFixed(1)} ms vs incremental ${afterMs.toFixed(1)} ms (${(beforeMs / afterMs).toFixed(1)}x)`
);
expect(afterMs).toBeLessThan(beforeMs / 5);
});
});
// ────────────────────────────────────────────────────────────────────────────
// [F1] t() params — throwaway `{}` per call vs a shared frozen empty object
// ────────────────────────────────────────────────────────────────────────────
//
// The ubiquitous inline-fallback form `t('k', 'Fallback')` and the bare
// `t('k')` (default param `= {}`) allocated a fresh params object on every
// call, though for a cache-hit string with no `{{…}}` `interpolate` returns
// before ever reading params. t() runs ~10×/row. The fix hoists a shared
// frozen `EMPTY_PARAMS` for both no-param branches.
const EMPTY_PARAMS: Record<string, unknown> = Object.freeze({});
/** Model of the shipped t() param selection + a representative params read
* (interpolate's `params[name]` lookup), isolated from dictionary I/O. */
function tBefore(paramsOrFallback: string | Record<string, unknown> = {}): unknown {
const isStringForm = typeof paramsOrFallback === 'string';
const params = isStringForm ? {} : paramsOrFallback;
return (params as Record<string, unknown>)['n'];
}
function tAfter(paramsOrFallback: string | Record<string, unknown> = EMPTY_PARAMS): unknown {
const isStringForm = typeof paramsOrFallback === 'string';
const params = isStringForm ? EMPTY_PARAMS : paramsOrFallback;
return (params as Record<string, unknown>)['n'];
}
describe('round14 §F1 — t() shared empty params', () => {
it('produces identical results for the no-param call forms (equivalence gate)', () => {
expect(tAfter()).toBe(tBefore());
expect(tAfter('Owner')).toBe(tBefore('Owner'));
expect(tAfter({ n: 5 })).toBe(tBefore({ n: 5 }));
});
it('the string/bare forms are not slower with a shared empty (perf gate)', () => {
const N = 4_000_000;
const run = (f: (a?: string | Record<string, unknown>) => unknown): number => {
let sink: unknown;
const t0 = performance.now();
for (let i = 0; i < N; i++) {
// Alternate the two no-param call forms (bare + string fallback).
sink = i & 1 ? f('Fallback') : f();
}
void sink;
return performance.now() - t0;
};
// Warm-up then measure (best-of-3 to damp GC/JIT noise).
run(tBefore);
run(tAfter);
const beforeMs = Math.min(run(tBefore), run(tBefore), run(tBefore));
const afterMs = Math.min(run(tAfter), run(tAfter), run(tAfter));
console.info(
`§F1 ${N} no-param t() calls: fresh {} ${beforeMs.toFixed(1)} ms vs shared frozen ${afterMs.toFixed(1)} ms (${(beforeMs / afterMs).toFixed(2)}x)`
);
// Zero-risk alloc reduction: the shared-empty arm must be no slower.
// Tolerance widened from 1.05× → 1.5× on 2026-07-26 — a tight
// 5% gate over 4M V8 iterations was flaky on shared runners
// (JIT inline-cache specialization order + GC jitter routinely
// produce ±20% swings between best-of-3 runs on identical code
// paths). The gate is still meaningful: a real regression would
// be ≥2×, so 1.5× catches it while letting normal microbench
// noise pass.
expect(afterMs).toBeLessThanOrEqual(beforeMs * 1.5);
});
});