Files
Oxicloud/frontend/src/lib/composables/selectionBench.svelte.ts
T
Claude 61c9470981 perf(frontend): round 6 — coalesced progressive listing, in-place SvelteSet, batch fan-out, t() value cache
Four SPA hot-path fixes, each shipping with a vitest benchmark gate
(verbatim BEFORE replica + equivalence + perf assertion) so CI
re-verifies the win on every run:

- fetchFolderListing invoked onPage after EVERY 200-row page with the
  whole accumulated listing, and the files view re-sorts everything per
  emission — O(N²/page) main-thread work on large folders. Page one and
  the final page always emit; intermediates coalesce to one per 150 ms.
  25×200 load: 30.9 → 4.0 ms (7.8x), 65 000 → 5 200 sorted elements.

- selected/favoriteIds/sharedIds (files) and favoriteIds (recent) were
  $state<Set>s copied whole on every toggle. Now one SvelteSet each,
  mutated in place (the useSelection pattern): 1 000 toggles @ N=5 000
  771.9 → 1.9 ms (399x); one-toggle fan-out across 40 mounted rows
  40 → 3 re-runs when refining a select-all.

- batchDelete/moveInto awaited one request per item serially and
  probed listing.folders.find per id (O(N·M)). Now an id index built
  once + mapLimit(6) fan-out, failure semantics preserved: 100-item
  delete @ 5 ms RTT 525 → 89 ms (5.9x), 38 825 → 500 probes.

- t() re-split its dotted key and walked the nested dict on every call,
  and interpolate regex-scanned strings without placeholders. Resolved
  values now memoize per (dict, key) in a WeakMap + a {{ guard:
  20k mixed calls 22.7 → 8.6 ms (2.63x).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017aJu9ghvuT8WqC31ZEGTBA
2026-07-18 00:54:38 +00:00

86 lines
2.5 KiB
TypeScript

/**
* Bench harness for the selection/badge-set reactivity patterns compared in
* `selectionPatterns.bench.test.ts` (runes only compile in `.svelte.ts`
* modules, so the models live here; the app never imports this file — it is
* test-only and tree-shaken from the bundle).
*
* `copyReassignModel` is the pre-fix files-view pattern, verbatim: a
* `$state<Set>` where every toggle copies the whole set into a fresh
* `SvelteSet` and reassigns. `inPlaceModel` is the post-fix pattern: one
* `SvelteSet` mutated in place.
*/
import { flushSync } from 'svelte';
import { SvelteSet } from 'svelte/reactivity';
export interface SelectionModel {
has(id: string): boolean;
toggle(id: string): void;
seed(ids: Iterable<string>): void;
readonly size: number;
}
/** Pre-fix pattern (files view `toggleSelected`, verbatim copy-and-reassign). */
export function copyReassignModel(): SelectionModel {
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- BEFORE arm replicates the pre-fix plain-Set pattern verbatim
let selected = $state<Set<string>>(new Set());
return {
has: (id) => selected.has(id),
toggle(id) {
const next = new SvelteSet(selected);
if (next.has(id)) next.delete(id);
else next.add(id);
selected = next;
},
seed(ids) {
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- BEFORE arm replicates the pre-fix plain-Set pattern verbatim
selected = new Set(ids);
},
get size() {
return selected.size;
}
};
}
/** Post-fix pattern: one live `SvelteSet` mutated in place (per-key sources
* for present keys; absent-key reads track the version signal). */
export function inPlaceModel(): SelectionModel {
const selected = new SvelteSet<string>();
return {
has: (id) => selected.has(id),
toggle(id) {
if (selected.has(id)) selected.delete(id);
else selected.add(id);
},
seed(ids) {
selected.clear();
for (const id of ids) selected.add(id);
},
get size() {
return selected.size;
}
};
}
/**
* Mount one effect per row reading `model.has(rowId)` — the shape of a row's
* checkbox/star binding — run `mutate`, and report how many row effects re-ran
* (the invalidation fan-out of the mutation).
*/
export function measureFanout(model: SelectionModel, rowIds: string[], mutate: () => void): number {
let runs = 0;
const destroy = $effect.root(() => {
for (const id of rowIds) {
$effect(() => {
void model.has(id);
runs += 1;
});
}
});
flushSync(); // initial run of every row effect
const baseline = runs;
mutate();
flushSync();
destroy();
return runs - baseline;
}