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:
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user