61c9470981
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
86 lines
2.5 KiB
TypeScript
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;
|
|
}
|