Files
Oxicloud/frontend/src/lib/components/round11.bench.test.ts
T
Claude 221c1f31b0 perf: round 11 — StoragePath joined-only, classifier fusion, memoized bodies, query-shape pack, SPA fine-grained stars
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
2026-07-18 22:02:00 +00:00

251 lines
8.9 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';
/**
* 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;
}
});
});