Files
Oxicloud/frontend/src/lib/utils/listContext.bench.test.ts
T
Claude 955f4a7b9f perf: round 16 — shares-lane & contextMap incremental builders, folder/href/disposition/preview alloc cuts
Finishes the route-level half of the O(N²/page) grouped-listing class ROUND15
fixed inside ResourceList, plus a backend CPU/alloc micro-pack. Every change is
benchmark-gated with a hard rollback rule; no PostgreSQL needed for any arm
(benches/ROUND16.md).

Frontend (vitest):
- F1 "My shares" lanes: the `lanes` $derived.by re-bucketed the whole
  accumulated grant list on every page and every grant edit. SharedLanesBuilder
  re-emits only the fresh page (fan-out + first-appearance header), reusing
  untouched lanes' array refs. 25.5x fewer emit calls, 8.8x wall, O(N²/page)→O(N).
- F2 contextMap (trash/recent/favorites/shared-with-me): each rebuilt a fresh
  N-key Map, re-hashing every accumulated id, per page. primeContextPage holds a
  persistent SvelteMap primed per page (the shipped favoriteIds shape).
  25.5x fewer entry calls, 7.2x wall.
- Extracted the shared O(1) append test (isAppendExtension); F1's gate re-covers it.

Backend (counting-allocator):
- M1 folder display constants Arc::from -> intern_display (3 sites): 3 -> 0 allocs/row.
- M2 build_content_disposition (every download + Range seek): 3 -> 1 alloc, 6x, 2.67x wall.
- M3 nc_href (every NC PROPFIND/REPORT href): Vec+join+format -> one pre-sized
  buffer, keeping urlencoding::encode (byte-identical). 38 -> 27 allocs/op.
- M4 NC preview fileId: collect-then-parse -> borrow-slice parse. 4 -> 0 allocs.

Gates: sharedLanes/listContext.bench.test.ts, examples/bench_round16_micro.rs
(GATE PASS all sections). Frontend: vitest 331 pass, svelte-check clean.
Backend: clippy -D warnings clean, 524 lib tests pass.

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

142 lines
5.6 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.
import { describe, expect, it } from 'vitest';
import { primeContextPage } from './listContext';
/**
* Benchmark gate for the incremental route-level `contextMap` maintenance
* (primeContextPage) that replaced `contextMap = $derived(new Map(raw.map(...)))`
* on the trash / recent / favorites / shared-with-me routes.
*
* Audit finding (ROUND16 §F2, the route-level half of the class ROUND15 §F1
* fixed inside ResourceList): each route paged its rows in via
* `raw = [...raw, ...page.items]` and rebuilt a brand-new Map — hashing every
* accumulated id — on EVERY page. O(N) per page ⇒ Σ O(N²/page) across a drain,
* plus a fresh Map instance each page. The fix holds one persistent map and
* sets only the fresh page's entries (mirrors the shipped `favoriteIds`
* SvelteSet, ROUND14 §F2).
*
* Gates (rollback rule: an AFTER that fails to beat its BEFORE fails CI):
* 1. Equivalence — at EVERY page, the incrementally-primed map is deep-equal
* to a full `new Map(cumulative.map(entry))` rebuild, including skipped
* entries (drives → null) and the reset path.
* 2. Perf — `entry` work collapses from Σ O(N²/page) to O(N) across the drain
* (deterministic call count) and wall drops ≥3x.
*/
interface Ctx {
date: string | null;
ownerId: string | null;
}
interface Raw {
resource: { id: string; updated_by: string | null };
resource_type: 'file' | 'folder' | 'drive';
accessed_at: string;
}
const pad = (i: number) => i.toString().padStart(6, '0');
/** Item `i`; every 10th is a `drive` (skipped by the shared-with-me-style entry). */
function raw(i: number): Raw {
return {
resource: { id: `res-${pad(i)}`, updated_by: `user-${i % 8}` },
resource_type: i % 10 === 0 ? 'drive' : i % 3 === 0 ? 'folder' : 'file',
accessed_at: `2026-07-${pad((i % 27) + 1).slice(-2)}`
};
}
/** Maps a raw item to its `[id, ctx]`, skipping drives (returns null) — counts calls. */
function makeEntry(counter?: { n: number }): (it: Raw) => readonly [string, Ctx] | null {
return (it) => {
if (counter) counter.n++;
if (it.resource_type === 'drive') return null;
return [it.resource.id, { date: it.accessed_at, ownerId: it.resource.updated_by }];
};
}
/** Verbatim BEFORE: the old derive — a fresh Map hashing the whole cumulative list. */
function rebuild(
cumulative: Raw[],
entry: (it: Raw) => readonly [string, Ctx] | null
): Map<string, Ctx> {
const m = new Map<string, Ctx>();
for (const it of cumulative) {
const e = entry(it);
if (e !== null) m.set(e[0], e[1]);
}
return m;
}
const PAGE = 50;
const PAGES = 50; // 2 500-item drain
describe('incremental contextMap (benchmark gate)', () => {
it('stays deep-equal to the full rebuild at every page (incl. skipped drives)', () => {
const all = Array.from({ length: PAGE * PAGES }, (_, i) => raw(i));
const entry = makeEntry();
const map = new Map<string, Ctx>();
for (let p = 1; p <= PAGES; p++) {
const page = all.slice((p - 1) * PAGE, p * PAGE);
primeContextPage(map, p === 1, page, entry);
const reference = rebuild(all.slice(0, p * PAGE), entry);
expect(new Map(map), `page ${p}`).toEqual(reference);
}
});
it('clears on reset and re-primes to the reset page only', () => {
const all = Array.from({ length: 200 }, (_, i) => raw(i));
const entry = makeEntry();
const map = new Map<string, Ctx>();
primeContextPage(map, true, all.slice(0, 100), entry);
primeContextPage(map, false, all.slice(100, 150), entry);
// Reset with a disjoint page: prior ids must be gone.
const resetPage = all.slice(150, 200);
primeContextPage(map, true, resetPage, entry);
expect(new Map(map)).toEqual(rebuild(resetPage, entry));
});
it('collapses entry work from Σ O(N²/page) to O(N) and runs ≥3x faster', () => {
const N = PAGE * PAGES;
const all = Array.from({ length: N }, (_, i) => raw(i));
// Deterministic call-count gate (the hard rollback gate): incremental
// computes each item's entry exactly once; the rebuild is quadratic. This
// holds regardless of machine load.
const afterCounter = { n: 0 };
const afterEntry = makeEntry(afterCounter);
const countMap = new Map<string, Ctx>();
for (let p = 1; p <= PAGES; p++) {
primeContextPage(countMap, p === 1, all.slice((p - 1) * PAGE, p * PAGE), afterEntry);
}
const beforeCounter = { n: 0 };
const beforeEntry = makeEntry(beforeCounter);
for (let p = 1; p <= PAGES; p++) rebuild(all.slice(0, p * PAGE), beforeEntry);
expect(afterCounter.n).toBe(N);
expect(beforeCounter.n).toBe((PAGES * (PAGES + 1) * PAGE) / 2);
expect(afterCounter.n).toBeLessThan(beforeCounter.n / 5);
// Wall gate — best-of-3 (min) per arm to shrug off scheduler / GC noise
// under a saturated test runner (mirrors round14 §F1's `Math.min` pattern).
const entry = makeEntry();
const runAfter = () => {
const m = new Map<string, Ctx>();
const t = performance.now();
for (let p = 1; p <= PAGES; p++) {
primeContextPage(m, p === 1, all.slice((p - 1) * PAGE, p * PAGE), entry);
}
return performance.now() - t;
};
const runBefore = () => {
const t = performance.now();
for (let p = 1; p <= PAGES; p++) rebuild(all.slice(0, p * PAGE), entry);
return performance.now() - t;
};
const afterMs = Math.min(runAfter(), runAfter(), runAfter());
const beforeMs = Math.min(runBefore(), runBefore(), runBefore());
console.info(
`contextMap ${PAGES}×${PAGE}: before ${beforeCounter.n} entry calls / ${beforeMs.toFixed(1)} ms — after ${afterCounter.n} calls / ${afterMs.toFixed(1)} ms (${(beforeCounter.n / afterCounter.n).toFixed(1)}x fewer calls, ${(beforeMs / afterMs).toFixed(1)}x wall)`
);
expect(afterMs).toBeLessThan(beforeMs / 3);
});
});