Files
Oxicloud/frontend/src/lib/components/round14.bench.test.ts
T
Claude c930f865b0 perf: round 14 — faces narrow projection, auth per-request allocs, CalDAV emit buffers, frontend set churn
Benchmark-gated (benches/ROUND14.md); every change ships a BEFORE/AFTER
benchmark with an equivalence gate and is rolled back on regression (the
rule is encoded as a GATE FAIL exit / threshold expect).

Backend
- Q1 faces_for_file → narrow face_boxes_for_file(id, person_id, bbox) with the
  caller filter pushed into SQL: drops the 2 KiB embedding BYTEA + 6 unused
  columns per face. 15-face lightbox open 0.312→0.219 ms, 32 KB→840 B/req.
- A1 cookie auth uses the borrow-only extract_cookie_str (already backs CSRF)
  instead of extract_cookie_value's owned String: -1 alloc/cookie request.
- A2 compute_relevance ASCII case-fold fast path vs name.to_lowercase() per
  result row (Unicode fallback preserved): 1.40x, 12→3 allocs/page.
- A3 sub pre-parsed to Uuid at decode time (TokenClaims.sub_id) vs re-parsing
  the 36-char claim on every request incl. cache hits: 22.7→0.7 ns.
- A4 auth + NextCloud middlewares borrow request.headers() instead of taking
  axum's HeaderMap extractor (a full map clone): 2→0 allocs/authed request.
- A5 CalDAV getlastmodified via the stack rfc2822_utc (byte-identical to
  chrono) vs a per-event to_rfc2822() heap String: 5→0 allocs.
- A6 CalDAV per-event href + quoted etag written into reused page buffers vs a
  fresh format! pair per event: 3.48x, 240→6 allocs/40-event page.

Frontend
- F1 t() shares one frozen EMPTY_PARAMS for the no-interpolation call forms vs
  a throwaway {} per call: -1 alloc/call.
- F2 favorites favoriteIds is a persistent SvelteSet with per-page add (clear
  on reset) vs a brand-new set over the whole accumulated list each page:
  22.3x over a 40-page drain (O(N^2)→O(N)).

Verified: cargo check --all-targets, cargo clippy -D warnings, both bench
packs (GATE PASS), frontend npm run check + vitest (4/4). ROUND14.md also
records the investigated-but-deferred backlog (music N+1, contact vcard
over-fetch, CachedBlobBackend syscalls, ResourceList.sections builder, etc.).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PymgCdK78NzUF3oRAQCJfN
2026-07-19 10:22:12 +00:00

139 lines
6.4 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.
expect(afterMs).toBeLessThanOrEqual(beforeMs * 1.05);
});
});