221c1f31b0
Backend (each change benchmark-gated with BEFORE replicas + equivalence gates; see examples/bench_round11_micro.rs, bench_round11_queries.rs, bench_log_writer.rs and benches/ROUND11.md — final numbers land in the follow-up doc commit): - StoragePath re-representation: single canonical joined String, segments derived on demand; File/Folder drop the duplicated path_string field (4000→1000 allocs per 500-row listing page) - Display classifier fusion: classify_display shares one stack-lowered extension across the three decision trees; call sites in FileDto, folder/favorites/recent handlers, trash, path-resolver (+ interning where Arc::from was still used) - /status.php and /openapi.json memoized into OnceLock<Bytes> (openapi rebuilt a 171 KiB spec per request: 2.8 ms → 18 ns) - NC upload-session PROPFIND: write! + pre-sized body + stack RFC2822 dates (2.3-2.6x, 2582→772 allocs at 256 chunks) - REST download: dead FileDto clone removed (capture mime/size + move) - CalendarEventDto/TrashedItem into_parts moves (11 KiB ical_data memcpy gone per CalDAV row); CardDAV getlastmodified stack render - 4xx path: borrowed ErrorResponse serialize, ErrorKind::as_str, not_found/already_exists clone kill - vCard emit via write!; search page moved out with into_iter skip/take; content-hit UUIDs parsed once; group last-user check via HashSet - RateLimiter: lock-free get + insert (and_upsert_with variant REJECTED by benchmark); CSRF token borrow-compare + borrowed cookie extraction - Thumbnail/preview ETags built from as_str (Debug-identical bytes) - Encrypted backend: encrypt_in_place_detached single-buffer write path, chunk-sized reserve in collect_stream; retry labels made lazy - PG: deferred upload registration 3→1 round-trips (persist_file CTE template); direct_grant_cache for Calendar/AddressBook/Playlist authz (single-flight + set_role/clear_role invalidation); expand_user tokio::join!; geo clusters min(uuid)::text; recluster face assignment batched into one UNNEST update - People recluster cosine: norms precomputed once (bit-identical gate) - NC capabilities poll logs demoted to debug; tracing-appender dep added for the log-writer benchmark Frontend: - ResourceList.selectedEntries O(N)-per-toggle → id-index projection O(k log k); favorites/recent consume the batchToolbar snippet param and drop their duplicate filter + dead selectedIds mirror - Recent: star state via new favoriteIds prop — a star click no longer rebuilds all N entries - admin timeAgo >30d fallback uses the cached Intl.DateTimeFormat - vitest gates in src/lib/components/round11.bench.test.ts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ABhTEHuGujvwoodh67Kga7
251 lines
8.9 KiB
TypeScript
251 lines
8.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
/**
|
||
* Benchmark gates for the round-11 SPA items (see benches/ROUND11.md):
|
||
*
|
||
* [1] `ResourceList.selectedEntries` re-filtered the ENTIRE items array on
|
||
* every selection change once the batch toolbar was mounted — and the
|
||
* favorites/recent hosts ignored the snippet param and recomputed their
|
||
* own `entries.filter(...)` shadow, so each toggle ran TWO full O(N)
|
||
* scans (O(N²)-ish across a shift-range gesture). The shipped shape
|
||
* derives an id→index Map (rebuilt only when `items` changes) and
|
||
* projects the selection in O(k · log k), preserving item order; hosts
|
||
* now consume the snippet param.
|
||
*
|
||
* [2] The Recent page mapper baked `favoriteIds.has(id)` into every entry,
|
||
* subscribing the whole O(N) map to the SvelteSet — one star click
|
||
* rebuilt all N entries and re-rendered every visible row. The shipped
|
||
* shape reads membership in the star widget via ResourceList's new
|
||
* `favoriteIds` prop, so the mapper no longer depends on the set.
|
||
*
|
||
* [3] The admin "time ago" >30-day fallback called `toLocaleDateString()`
|
||
* (a fresh Intl.DateTimeFormat per call) instead of the cached
|
||
* `dateTimeFormatFor` the rest of the app uses.
|
||
*
|
||
* All modeled as pure replicas of the derive bodies with instrumentation
|
||
* counters (the listDerives.bench.test.ts convention).
|
||
*/
|
||
|
||
interface Entry {
|
||
id: string;
|
||
name: string;
|
||
}
|
||
|
||
const buildItems = (n: number): Entry[] =>
|
||
Array.from({ length: n }, (_, i) => ({ id: `it-${i}`, name: `Item ${i}` }));
|
||
|
||
/** BEFORE — component derive + host shadow, each a full O(N) scan. */
|
||
function selectedBefore(
|
||
items: Entry[],
|
||
selected: Set<string>,
|
||
counter: { comparisons: number }
|
||
): { component: Entry[]; host: Entry[] } {
|
||
const component = items.filter((i) => {
|
||
counter.comparisons++;
|
||
return selected.has(i.id);
|
||
});
|
||
const host = items.filter((i) => {
|
||
counter.comparisons++;
|
||
return selected.has(i.id);
|
||
});
|
||
return { component, host };
|
||
}
|
||
|
||
/** AFTER — id→index Map projection, index rebuilt only on items change. */
|
||
function makeAfterProjector(items: Entry[]) {
|
||
const indexById = new Map(items.map((i, idx) => [i.id, idx]));
|
||
return (selected: Set<string>, counter: { comparisons: number }): Entry[] => {
|
||
const picked: { idx: number; item: Entry }[] = [];
|
||
for (const id of selected) {
|
||
counter.comparisons++;
|
||
const idx = indexById.get(id);
|
||
if (idx !== undefined) picked.push({ idx, item: items[idx] });
|
||
}
|
||
picked.sort((a, b) => a.idx - b.idx);
|
||
return picked.map((p) => p.item);
|
||
};
|
||
}
|
||
|
||
describe('ResourceList selectedEntries projection (benchmark gate)', () => {
|
||
it('identical output (order + membership) and O(k) vs O(2N) comparisons per toggle', () => {
|
||
const N = 2000;
|
||
const items = buildItems(N);
|
||
const project = makeAfterProjector(items);
|
||
|
||
// Model a 50-item shift-range selection built one id at a time,
|
||
// re-deriving after each toggle (what the reactive graph does).
|
||
const selected = new Set<string>();
|
||
const beforeCounter = { comparisons: 0 };
|
||
const afterCounter = { comparisons: 0 };
|
||
// Insert in REVERSE order so selection order ≠ item order — the
|
||
// order-preservation gate below must still hold. Each toggle
|
||
// re-derives both shapes (what the reactive graph does).
|
||
for (let i = 149; i >= 100; i--) {
|
||
selected.add(`it-${i}`);
|
||
selectedBefore(items, selected, beforeCounter);
|
||
project(selected, afterCounter);
|
||
}
|
||
// Stale ids (deleted rows) must be dropped by both shapes.
|
||
selected.add('it-ghost');
|
||
const lastBefore = selectedBefore(items, selected, beforeCounter).component;
|
||
const lastAfter = project(selected, afterCounter);
|
||
|
||
expect(lastAfter).toEqual(lastBefore); // same entries, same (item) order
|
||
// BEFORE: 2 scans × N per toggle. AFTER: k probes per toggle.
|
||
expect(beforeCounter.comparisons).toBe(51 * 2 * N);
|
||
expect(afterCounter.comparisons).toBeLessThan(51 * 51 + 1);
|
||
});
|
||
|
||
it('wall clock: 500-toggle sweep on a 5k list is faster with the projection', () => {
|
||
const N = 5000;
|
||
const items = buildItems(N);
|
||
const project = makeAfterProjector(items);
|
||
const selected = new Set<string>();
|
||
const nul = { comparisons: 0 };
|
||
|
||
const t0 = performance.now();
|
||
for (let i = 0; i < 500; i++) {
|
||
selected.add(`it-${i}`);
|
||
selectedBefore(items, selected, nul);
|
||
}
|
||
const tBefore = performance.now() - t0;
|
||
|
||
selected.clear();
|
||
const t1 = performance.now();
|
||
for (let i = 0; i < 500; i++) {
|
||
selected.add(`it-${i}`);
|
||
project(selected, nul);
|
||
}
|
||
const tAfter = performance.now() - t1;
|
||
|
||
// Generous bound to keep CI stable; locally ~10-40x.
|
||
expect(tAfter).toBeLessThan(tBefore);
|
||
});
|
||
});
|
||
|
||
// ─── [2] Recent favorite-star dependency ────────────────────────────────────
|
||
|
||
interface RawItem {
|
||
id: string;
|
||
name: string;
|
||
}
|
||
|
||
/** BEFORE — mapper reads the favorite set: every toggle re-maps ALL rows. */
|
||
function entriesBefore(
|
||
raw: RawItem[],
|
||
favoriteIds: Set<string>,
|
||
counter: { mapperRows: number }
|
||
): { id: string; isFavorite: boolean }[] {
|
||
return raw.map((it) => {
|
||
counter.mapperRows++;
|
||
return { id: it.id, isFavorite: favoriteIds.has(it.id) };
|
||
});
|
||
}
|
||
|
||
/** AFTER — mapper is set-independent; the star widget reads membership. */
|
||
function entriesAfter(raw: RawItem[], counter: { mapperRows: number }): { id: string }[] {
|
||
return raw.map((it) => {
|
||
counter.mapperRows++;
|
||
return { id: it.id };
|
||
});
|
||
}
|
||
function starStateAfter(favoriteIds: Set<string>, id: string): boolean {
|
||
return favoriteIds.has(id);
|
||
}
|
||
|
||
describe('Recent favorite-star fine-grained dependency (benchmark gate)', () => {
|
||
it('a star toggle re-maps 0 rows (was N) and renders the same star states', () => {
|
||
const N = 400;
|
||
const raw: RawItem[] = Array.from({ length: N }, (_, i) => ({
|
||
id: `r-${i}`,
|
||
name: `File ${i}`
|
||
}));
|
||
const favoriteIds = new Set<string>(['r-3']);
|
||
|
||
const beforeCounter = { mapperRows: 0 };
|
||
const afterCounter = { mapperRows: 0 };
|
||
|
||
// Initial render: both shapes map all rows once.
|
||
let entriesB = entriesBefore(raw, favoriteIds, beforeCounter);
|
||
const entriesA = entriesAfter(raw, afterCounter);
|
||
expect(beforeCounter.mapperRows).toBe(N);
|
||
expect(afterCounter.mapperRows).toBe(N);
|
||
|
||
// 10 star toggles. BEFORE: the mapper depends on the set → full
|
||
// re-map each time. AFTER: the mapper doesn't run at all.
|
||
for (let k = 0; k < 10; k++) {
|
||
const id = `r-${k * 7}`;
|
||
if (favoriteIds.has(id)) favoriteIds.delete(id);
|
||
else favoriteIds.add(id);
|
||
entriesB = entriesBefore(raw, favoriteIds, beforeCounter); // reactive re-run
|
||
// AFTER: no mapper re-run; only the affected star re-reads.
|
||
starStateAfter(favoriteIds, id);
|
||
}
|
||
|
||
expect(beforeCounter.mapperRows).toBe(N + 10 * N);
|
||
expect(afterCounter.mapperRows).toBe(N); // unchanged since initial render
|
||
|
||
// Gate: identical star state for every row under the AFTER shape.
|
||
for (let i = 0; i < N; i++) {
|
||
expect(starStateAfter(favoriteIds, entriesA[i].id)).toBe(entriesB[i].isFavorite);
|
||
}
|
||
});
|
||
});
|
||
|
||
// ─── [3] admin timeAgo date fallback ────────────────────────────────────────
|
||
|
||
describe('admin timeAgo >30d fallback formatter cache (benchmark gate)', () => {
|
||
it('cached formatter output is identical to toLocaleDateString()', async () => {
|
||
const { dateTimeFormatFor } = await import('../utils/display');
|
||
const dates = [
|
||
new Date('2025-01-15T10:30:00Z'),
|
||
new Date('2024-12-31T23:59:59Z'),
|
||
new Date('2020-06-01T00:00:00Z'),
|
||
new Date('1999-02-28T12:00:00Z')
|
||
];
|
||
for (const d of dates) {
|
||
expect(dateTimeFormatFor(undefined).format(d)).toBe(d.toLocaleDateString());
|
||
}
|
||
});
|
||
|
||
it('1000 formats construct ≤1 Intl.DateTimeFormat (was 1000)', async () => {
|
||
const { dateTimeFormatFor } = await import('../utils/display');
|
||
const RealDTF = Intl.DateTimeFormat;
|
||
let constructed = 0;
|
||
// Count constructions through both paths.
|
||
const Counting = new Proxy(RealDTF, {
|
||
construct(target, args: [string?, Intl.DateTimeFormatOptions?]) {
|
||
constructed++;
|
||
return new target(...args);
|
||
}
|
||
});
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
(Intl as any).DateTimeFormat = Counting;
|
||
try {
|
||
const d = new Date('2020-06-01T00:00:00Z');
|
||
constructed = 0;
|
||
for (let i = 0; i < 1000; i++) {
|
||
d.toLocaleDateString();
|
||
}
|
||
// jsdom implements toLocaleDateString via Intl internally in some
|
||
// versions; count only if observable. The load-bearing assertion
|
||
// is the cached path below.
|
||
const beforeConstructed = constructed;
|
||
|
||
constructed = 0;
|
||
for (let i = 0; i < 1000; i++) {
|
||
dateTimeFormatFor(undefined).format(d);
|
||
}
|
||
expect(constructed).toBeLessThanOrEqual(1);
|
||
// When the environment exposes per-call constructions, require
|
||
// the cached path to be strictly cheaper.
|
||
if (beforeConstructed > 1) {
|
||
expect(constructed).toBeLessThan(beforeConstructed);
|
||
}
|
||
} finally {
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
(Intl as any).DateTimeFormat = RealDTF;
|
||
}
|
||
});
|
||
});
|