955f4a7b9f
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
142 lines
5.6 KiB
TypeScript
142 lines
5.6 KiB
TypeScript
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);
|
||
});
|
||
});
|