perf: round 16 — shares-lane & contextMap incremental builders, folder/href/disposition/preview alloc cuts

Finishes the route-level half of the O(N²/page) grouped-listing class ROUND15
fixed inside ResourceList, plus a backend CPU/alloc micro-pack. Every change is
benchmark-gated with a hard rollback rule; no PostgreSQL needed for any arm
(benches/ROUND16.md).

Frontend (vitest):
- F1 "My shares" lanes: the `lanes` $derived.by re-bucketed the whole
  accumulated grant list on every page and every grant edit. SharedLanesBuilder
  re-emits only the fresh page (fan-out + first-appearance header), reusing
  untouched lanes' array refs. 25.5x fewer emit calls, 8.8x wall, O(N²/page)→O(N).
- F2 contextMap (trash/recent/favorites/shared-with-me): each rebuilt a fresh
  N-key Map, re-hashing every accumulated id, per page. primeContextPage holds a
  persistent SvelteMap primed per page (the shipped favoriteIds shape).
  25.5x fewer entry calls, 7.2x wall.
- Extracted the shared O(1) append test (isAppendExtension); F1's gate re-covers it.

Backend (counting-allocator):
- M1 folder display constants Arc::from -> intern_display (3 sites): 3 -> 0 allocs/row.
- M2 build_content_disposition (every download + Range seek): 3 -> 1 alloc, 6x, 2.67x wall.
- M3 nc_href (every NC PROPFIND/REPORT href): Vec+join+format -> one pre-sized
  buffer, keeping urlencoding::encode (byte-identical). 38 -> 27 allocs/op.
- M4 NC preview fileId: collect-then-parse -> borrow-slice parse. 4 -> 0 allocs.

Gates: sharedLanes/listContext.bench.test.ts, examples/bench_round16_micro.rs
(GATE PASS all sections). Frontend: vitest 331 pass, svelte-check clean.
Backend: clippy -D warnings clean, 524 lib tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193NjactJVqfU32gxeJDj8m
This commit is contained in:
Claude
2026-07-19 17:30:22 +00:00
parent 8537b5949a
commit 955f4a7b9f
20 changed files with 1388 additions and 123 deletions
+10 -6
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { SvelteSet } from 'svelte/reactivity';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import { primeContextPage } from '$lib/utils/listContext';
import Button from '$lib/components/Button.svelte';
import { useOwnerCache } from '$lib/composables/useOwnerCache.svelte';
import { errorToast } from '$lib/utils/errors';
@@ -52,11 +53,10 @@
// items on this page are favorites — pass every id in `favoriteIds`
// so the star widget lights up universally.
const items = $derived(raw.map((it) => it.resource as FileItem | FolderItem));
const contextMap = $derived(
new Map<string, ItemContext>(
raw.map((it) => [it.resource.id, { date: it.favorited_at } satisfies ItemContext])
)
);
// Persistent reactive map, primed per page in `load()` (benches/ROUND16.md §F2)
// instead of rebuilding a fresh Map that re-hashes the whole accumulated list
// on every infinite-scroll page. Mirrors the sibling `favoriteIds` SvelteSet.
const contextMap = new SvelteMap<string, ItemContext>();
// Persistent reactive set, updated in place per page (add the fresh page's
// ids; clear on reset) instead of rebuilding a brand-new SvelteSet over the
// whole accumulated list on every infinite-scroll page — that was O(N²)
@@ -117,6 +117,10 @@
// reset, then add only this page's ids (benches/ROUND14.md §F2).
if (reset) favoriteIds.clear();
for (const it of page.items) favoriteIds.add(it.resource.id);
primeContextPage(contextMap, reset, page.items, (it) => [
it.resource.id,
{ date: it.favorited_at }
]);
cursor = page.next_cursor;
void owners.resolve(page.items.map((i) => i.resource.created_by));
} catch (e) {
+10 -9
View File
@@ -5,7 +5,8 @@
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { onMount } from 'svelte';
import { SvelteSet } from 'svelte/reactivity';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import { primeContextPage } from '$lib/utils/listContext';
import { clearRecent, fetchRecentPage, type RecentResourceItem } from '$lib/api/endpoints/recent';
import {
addFavorite,
@@ -59,14 +60,10 @@
// shared `isDotfile` predicate purely for the empty-state message
// below (distinguishes "genuinely empty" from "everything filtered").
const items = $derived(raw.map((it) => it.resource as FileItem | FolderItem));
const contextMap = $derived(
new Map<string, ItemContext>(
raw.map((it) => [
it.resource.id,
{ date: it.accessed_at, ownerId: it.resource.updated_by ?? null } satisfies ItemContext
])
)
);
// Persistent reactive map, primed per page in `load()` (benches/ROUND16.md §F2)
// instead of rebuilding a fresh Map that re-hashes the whole accumulated list
// on every infinite-scroll page. Mirrors the sibling `favoriteIds` SvelteSet.
const contextMap = new SvelteMap<string, ItemContext>();
const hiddenCount = $derived(
preferences.hideDotfiles ? items.filter((i) => isDotfile(i.name)).length : 0
);
@@ -131,6 +128,10 @@
resourceTypes: ['file', 'folder']
});
raw = reset ? page.items : [...raw, ...page.items];
primeContextPage(contextMap, reset, page.items, (it) => [
it.resource.id,
{ date: it.accessed_at, ownerId: it.resource.updated_by ?? null }
]);
cursor = page.next_cursor;
void owners.resolve(page.items.map((i) => i.resource.updated_by));
} catch (e) {
@@ -1,5 +1,7 @@
<script lang="ts">
import { errorMessage } from '$lib/utils/errors';
import { SvelteMap } from 'svelte/reactivity';
import { primeContextPage } from '$lib/utils/listContext';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { onMount } from 'svelte';
@@ -43,14 +45,11 @@
// so the sharer shows up in the vignette (rather than the resource's
// intrinsic `created_by`, which is a stranger for grantees).
const items = $derived(fileFolderGrants.map((it) => it.resource as FileItem | FolderItem));
const contextMap = $derived(
new Map<string, ItemContext>(
fileFolderGrants.map((it) => [
it.resource.id,
{ date: it.granted_at, ownerId: it.granted_by ?? null } satisfies ItemContext
])
)
);
// Persistent reactive map, primed per page in `load()` (benches/ROUND16.md §F2)
// instead of rebuilding a fresh Map that re-hashes the whole accumulated list
// on every infinite-scroll page. Drives are skipped (they never reach the
// row UI), so the map covers exactly the displayed `fileFolderGrants`.
const contextMap = new SvelteMap<string, ItemContext>();
// Server-supported sort_by values (see grant_handler.rs:615):
// granted_at, granted_by, name, type
@@ -96,6 +95,11 @@
reverse: rev
});
raw = reset ? page.items : [...raw, ...page.items];
primeContextPage(contextMap, reset, page.items, (it) =>
it.resource_type === 'drive'
? null
: [it.resource.id, { date: it.granted_at, ownerId: it.granted_by ?? null }]
);
cursor = page.next_cursor;
// Warm the sharer-name cache so the "Shared by" group headers
// show real names instead of UUIDs.
+52 -40
View File
@@ -28,6 +28,7 @@
import { t } from '$lib/i18n/index.svelte';
import { ui } from '$lib/stores/ui.svelte';
import { formatDate, iconNameFromClass } from '$lib/utils/display';
import { SharedLanesBuilder, type LaneGrouping } from '$lib/utils/sharedLanes';
type GroupBy = 'items' | 'sharedWith';
@@ -178,47 +179,58 @@
rows: { grant: OutgoingResourceGrant; item: OutgoingGrantItem }[];
}
const lanes = $derived.by((): Lane[] => {
const out: Lane[] = [];
// Transient scratch map built inside $derived.by and discarded — not reactive state.
// eslint-disable-next-line svelte/prefer-svelte-reactivity
const byKey = new Map<string, Lane>();
const ensure = (key: string, header: Lane['header']): Lane => {
let lane = byKey.get(key);
if (!lane) {
lane = { key, header, rows: [] };
byKey.set(key, lane);
out.push(lane);
}
return lane;
};
for (const item of filteredRaw) {
if (groupBy === 'items') {
const lane = ensure(`resource:${item.resource.id}`, { kind: 'resource', item });
for (const grant of item.grants) lane.rows.push({ grant, item });
} else {
for (const grant of item.grants) {
let key: string;
let header: Lane['header'];
if (grant.subject_type === 'user') {
key = `user:${grant.subject_id}`;
header = { kind: 'user', id: grant.subject_id };
} else if (grant.subject_type === 'group') {
key = `group:${grant.subject_id}`;
header = { kind: 'group', id: grant.subject_id };
} else if (grant.has_password) {
key = 'links:password';
header = { kind: 'linkPassword' };
} else {
key = 'links:public';
header = { kind: 'linkPublic' };
type LaneRow = Lane['rows'][number];
// The active grouping as a stable-identity descriptor: `groupKey` changes
// only when the user switches group-by, so an infinite-scroll page (or a
// grant edit that reassigns `raw`) takes the builder's O(N) incremental path
// instead of re-bucketing the whole accumulated list. `emit` reproduces the
// old derive exactly — `open` is the old unconditional `ensure` (a by-files
// lane exists even with zero grants); `push` is `ensure(...).rows.push`.
const laneGrouping = $derived.by(
(): LaneGrouping<OutgoingGrantItem, Lane['header'], LaneRow> =>
groupBy === 'items'
? {
groupKey: 'items',
emit: (item, sink) => {
const key = `resource:${item.resource.id}`;
const header: Lane['header'] = { kind: 'resource', item };
sink.open(key, header);
for (const grant of item.grants) sink.push(key, header, { grant, item });
}
}
ensure(key, header).rows.push({ grant, item });
}
}
}
return out;
});
: {
groupKey: 'sharedWith',
emit: (item, sink) => {
for (const grant of item.grants) {
let key: string;
let header: Lane['header'];
if (grant.subject_type === 'user') {
key = `user:${grant.subject_id}`;
header = { kind: 'user', id: grant.subject_id };
} else if (grant.subject_type === 'group') {
key = `group:${grant.subject_id}`;
header = { kind: 'group', id: grant.subject_id };
} else if (grant.has_password) {
key = 'links:password';
header = { kind: 'linkPassword' };
} else {
key = 'links:public';
header = { kind: 'linkPublic' };
}
sink.push(key, header, { grant, item });
}
}
}
);
// Persistent across reactive ticks: re-buckets only the freshly-appended page
// and hands back the same rows-array reference for untouched lanes, falling
// back to a full rebuild (deep-equal to the pure `buildLanes` reference) on a
// group-by switch, grant edit or kind-filter toggle. Mirrors ResourceList's
// `sectionsBuilder` (benches/ROUND16.md §F1).
const lanesBuilder = new SharedLanesBuilder<OutgoingGrantItem, Lane['header'], LaneRow>();
const lanes = $derived.by(() => lanesBuilder.sync(filteredRaw, laneGrouping));
function laneTitle(header: Lane['header']): string {
switch (header.kind) {
+10 -14
View File
@@ -1,5 +1,7 @@
<script lang="ts">
import { errorToast } from '$lib/utils/errors';
import { SvelteMap } from 'svelte/reactivity';
import { primeContextPage } from '$lib/utils/listContext';
import { onMount } from 'svelte';
import {
deleteTrashItem,
@@ -44,20 +46,10 @@
// travel through `contextMap`, which page-provided group-by / render
// callbacks read via the `ctx` parameter.
const items = $derived(raw.map((it) => it.resource as FileItem | FolderItem));
const contextMap = $derived(
new Map<string, ItemContext>(
raw.map((it) => [
it.resource.id,
{
date: it.deletion_date,
extras: {
driveId: it.drive_id,
trashedAt: it.trashed_at
}
}
])
)
);
// Persistent reactive map, primed per page in `load()` (benches/ROUND16.md §F2)
// instead of rebuilding a fresh Map that re-hashes the whole accumulated list
// on every infinite-scroll page. Mirrors the shipped `favoriteIds` SvelteSet.
const contextMap = new SvelteMap<string, ItemContext>();
// "Drive" group rank: default-personal first, then secondary personal, then
// shared — matches `DrivePicker.svelte::sortedDrives` so the sidebar and
@@ -140,6 +132,10 @@
resourceTypes: ['file', 'folder']
});
raw = reset ? page.items : [...raw, ...page.items];
primeContextPage(contextMap, reset, page.items, (it) => [
it.resource.id,
{ date: it.deletion_date, extras: { driveId: it.drive_id, trashedAt: it.trashed_at } }
]);
cursor = page.next_cursor;
} catch (e) {
console.error('trash: load error', e);