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
This commit is contained in:
Claude
2026-07-19 10:22:12 +00:00
parent 3d578e4fc2
commit c930f865b0
19 changed files with 1345 additions and 73 deletions
@@ -0,0 +1,138 @@
// 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);
});
});
+8 -2
View File
@@ -201,6 +201,12 @@ async function loadDict(locale: string): Promise<Dict> {
return dicts[locale];
}
/** Shared frozen empty params for the no-interpolation call forms, so the
* ubiquitous `t(key)` / `t(key, 'fallback')` don't each allocate a throwaway
* `{}` (t() is the hottest UI function — ~10× per row). Never mutated, so a
* single shared instance is safe. See benches/ROUND14.md §F1. */
const EMPTY_PARAMS: Record<string, unknown> = Object.freeze({});
/**
* Translate a key.
* - `t(key)` / `t(key, params)` — interpolation params object.
@@ -209,11 +215,11 @@ async function loadDict(locale: string): Promise<Dict> {
*/
export function t(
key: string,
paramsOrFallback: string | Record<string, unknown> = {},
paramsOrFallback: string | Record<string, unknown> = EMPTY_PARAMS,
fallbackArg?: string
): string {
const isStringForm = typeof paramsOrFallback === 'string';
const params = isStringForm ? {} : paramsOrFallback;
const params = isStringForm ? EMPTY_PARAMS : paramsOrFallback;
const fallback = isStringForm ? paramsOrFallback : (fallbackArg ?? null);
const localeData = dicts[store.locale];
+13 -1
View File
@@ -57,7 +57,14 @@
raw.map((it) => [it.resource.id, { date: it.favorited_at } satisfies ItemContext])
)
);
const favoriteIds = $derived(new SvelteSet(items.map((i) => i.id)));
// Persistent reactive set, updated in place per page (add the fresh page's
// ids; clear on reset) instead of rebuilding a brand-new SvelteSet over the
// whole accumulated list on every infinite-scroll page — that was O(N²)
// across a drain and, being a new instance each page, invalidated every
// mounted star reader. Every item on this page is a favorite, and removed
// items are no longer rendered, so the set only needs to be a superset of
// the displayed ids (benches/ROUND14.md §F2, mirrors recent's shipped shape).
const favoriteIds = new SvelteSet<string>();
const groupBys: GroupByDef[] = [
{ key: '', label: t('files.name', 'Name'), orderBy: 'name', icon: 'arrow-up-a-z' },
@@ -106,6 +113,10 @@
resourceTypes: ['file', 'folder']
});
raw = reset ? page.items : [...raw, ...page.items];
// Keep the persistent favoriteIds set in sync incrementally: clear on
// reset, then add only this page's ids (benches/ROUND14.md §F2).
if (reset) favoriteIds.clear();
for (const it of page.items) favoriteIds.add(it.resource.id);
cursor = page.next_cursor;
void owners.resolve(page.items.map((i) => i.resource.created_by));
} catch (e) {
@@ -148,6 +159,7 @@
try {
await removeFavorite(kind, item.id);
raw = raw.filter((i) => i.resource.id !== item.id);
favoriteIds.delete(item.id);
} catch (e) {
errorToast(e);
}