perf: round 15 — grouped-listing O(N²) rebucket, exif/reseed allocs, tantivy zero-hit snippet skip
Benchmark-gated, same rule as rounds 2–14: every change ships with a BEFORE/AFTER benchmark and an equivalence/safety gate; an AFTER that doesn't beat its BEFORE is rolled back. The rule is encoded per harness (GATE FAIL non-zero exit in the Rust examples, threshold expect() in vitest). F1 — Grouped listings (trash / recent / favorites / shared-with-me) re-bucketed the WHOLE accumulated list on every infinite-scroll page. ResourceSectionsBuilder (new, off the reactive graph) re-buckets only the fresh page and hands VirtualList the same rows array reference for untouched buckets. 50×50 (2 500-item) drain: 63 750 → 2 500 bucketOf calls (25.5×), 12.5 → 1.3 ms wall (9.9×); O(N²/page) → O(N). Deep-equal to the full-rebuild reference at every page for both a contiguous (date) and a non-contiguous (trash-by-drive) group-by; reference-stability + fallback gated. B1 — exif Make/Model: the display String was thrown away to allocate the trimmed copy; display_value_trimmed trims in place (drain + truncate), 2 → 1 alloc per field (8 → 4 allocs/op, 1.26×). B2 — content-index worker: text_extractor::supports (lowercases MIME + extension) was called twice per file per drain batch; classify once into a Vec<bool> and thread it through both uses. 256-file batch: 704 → 353 allocs, 34.5 → 16.7 µs (2.07×). B3 — tantivy: skip SnippetGenerator::create on a zero-hit content search (return Ok(vec![]) once top_docs.is_empty()); the per-hit loop was empty. 400-doc index: 1 575.6 → 1 237.2 ns (1.27×), widens with index size. Harnesses: examples/bench_round15_micro.rs, examples/bench_round15_tantivy.rs, frontend resourceSections.bench.test.ts; writeup in benches/ROUND15.md. Also normalizes two round14 bench examples that were committed unformatted (cargo fmt --all). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012o47jSrtL7xuNGTHXmtiYL
This commit is contained in:
@@ -78,6 +78,7 @@
|
||||
import { formatBytes } from '$lib/utils/format';
|
||||
import { formatDate, iconNameFromClass, fileIconKindClass } from '$lib/utils/display';
|
||||
import { gridColumns } from '$lib/utils/grid';
|
||||
import { ResourceSectionsBuilder } from '$lib/utils/resourceSections';
|
||||
import { fileThumbnailUrl, thumbSizeForView } from '$lib/api/endpoints/files';
|
||||
import {
|
||||
canThumbnailClientSide,
|
||||
@@ -369,29 +370,24 @@
|
||||
/**
|
||||
* Partition the visible items into grouped sections when a `bucketOf` is
|
||||
* active. Server order is preserved within and across buckets (first-seen).
|
||||
*
|
||||
* `ResourceSectionsBuilder` re-buckets only the freshly-appended page rather
|
||||
* than the whole accumulated list, and hands `VirtualList` the same rows
|
||||
* array reference for every untouched bucket so it skips re-rendering it. An
|
||||
* infinite-scroll drain of a grouped listing (trash / recent / favorites /
|
||||
* shared-with-me) collapses from Σ O(N²/page) to O(N) bucketing work
|
||||
* (benches/ROUND15.md §F1). Held off the reactive graph — a plain
|
||||
* accumulator keyed by the append cursor, not $state; `sync` is idempotent,
|
||||
* so if the derive re-fires without an actual append it safely full-rebuilds
|
||||
* to the same output the pure `buildResourceSections` reference produces.
|
||||
*/
|
||||
const sections = $derived.by(
|
||||
(): Array<{ key: string; label: string; rows: Array<FileItem | FolderItem> }> => {
|
||||
const bucketOf = activeGroup?.bucketOf;
|
||||
if (!bucketOf) return [{ key: '', label: '', rows: visibleItems }];
|
||||
const order: string[] = [];
|
||||
// Transient bucketing map computed inside $derived.by — not reactive state.
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const map = new Map<string, Array<FileItem | FolderItem>>();
|
||||
for (const item of visibleItems) {
|
||||
const k = bucketOf(item, ctxOf(item.id)) ?? '∅';
|
||||
if (!map.has(k)) {
|
||||
map.set(k, []);
|
||||
order.push(k);
|
||||
}
|
||||
map.get(k)!.push(item);
|
||||
}
|
||||
return order.map((k) => ({
|
||||
key: k,
|
||||
label: activeGroup?.labelOf?.(k) ?? k,
|
||||
rows: map.get(k)!
|
||||
}));
|
||||
}
|
||||
const sectionsBuilder = new ResourceSectionsBuilder<FileItem | FolderItem, ItemContext>();
|
||||
const sections = $derived.by(() =>
|
||||
sectionsBuilder.sync(visibleItems, {
|
||||
bucketOf: activeGroup?.bucketOf,
|
||||
labelOf: activeGroup?.labelOf,
|
||||
ctxOf: (item) => ctxOf(item.id)
|
||||
})
|
||||
);
|
||||
const grouped = $derived(!!activeGroup?.bucketOf);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user