perf: round 13 — grouped-view virtualization, notification/login query narrowing, HTTP dedup, locale precompute
Benchmark-gated (BEFORE/AFTER + equivalence/safety gate per change), same discipline as rounds 2-12. Full write-up in benches/ROUND13.md. Shipped: - V1 Grouped views windowed (files route + ResourceList). The grid arm was the last unwindowed path (trash is grouped-by-default in grid): each swimlane now feeds its own VirtualList, outer container a flex stack. vitest gate: 800-item grouped grid mounts <120 .file-item (was 800). - Q1 get_users_by_ids drops the <=512 KiB avatar image + ui_preferences JSONB (notification path never reads them). 30-member fan-out 8.60 -> 0.25 ms (34.3x), ~7.7 MB off the wire. - Q2 Login provisioning is_empty() -> SELECT EXISTS for calendar + address book (every login). 0.193 -> 0.170 ms, widens with owned-row count. - Q3 Recent-access prunes only when the upsert inserted (RETURNING xmax=0) — a re-access can't grow the set. 0.567 -> 0.324 ms (1.75x). - L1 Locale supported-codes precomputed once vs rebuilt per anonymous request. 616 -> 17.3 ns (35.7x), 18 -> 1 allocs. - H1 Duplicate /api TraceLayer removed (global stack already wraps it). 1.86 -> 1.42 us/request, -6 allocs. - H2 client_ip span field: borrow-only ClientIpDisplay vs owned String. 187 -> 173 ns, -1 alloc. Not shipped (discipline): the "media hooks read the blob 3x" lead was a correctness bug, not a perf dup — the raw-path metadata/faces readers resolve only for local+unencrypted+single-chunk blobs and silently produce nothing otherwise. Flagged for maintainers; routing through read_blob_bytes is a correctness fix (perf-neutral-to-negative), not a benchmark-gated perf change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BfidAJD5AHw23jtvBUNamB
This commit is contained in:
@@ -329,9 +329,6 @@
|
||||
// filter on shows the empty state (the host page's `emptyHint` can
|
||||
// reference `hiddenCount` to say "3 items hidden by the filter").
|
||||
const isEmpty = $derived(visibleItems.length === 0);
|
||||
const viewClass = $derived(
|
||||
filesStore.viewMode === 'grid' ? 'files-grid-view' : 'files-list-view'
|
||||
);
|
||||
/** Content width, for computing the grid's column count to match auto-fill. */
|
||||
let gridWidth = $state(0);
|
||||
const gridCols = $derived(gridColumns(gridWidth));
|
||||
@@ -740,8 +737,8 @@
|
||||
/>
|
||||
{:else}
|
||||
<div class="files-container" bind:clientWidth={gridWidth}>
|
||||
{#if grouped}
|
||||
<div class={viewClass} style="--files-list-columns: {columns}">
|
||||
{#if grouped && filesStore.viewMode === 'list'}
|
||||
<div class="files-list-view" style="--files-list-columns: {columns}">
|
||||
{#if listHeaderOverride}{@render listHeaderOverride()}{:else}{@render listHeader()}{/if}
|
||||
{#each sections as section (section.key)}
|
||||
<div class="rl-swimlane-header" role="rowheader">
|
||||
@@ -752,17 +749,37 @@
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if filesStore.viewMode === 'list'}
|
||||
<!-- Window each section's rows so a large grouped list (e.g. a big
|
||||
trash, grouped by remaining days) doesn't mount every row. The
|
||||
grid-grouped branch stays un-windowed: `files-grid-view` is itself
|
||||
the card grid and can't host the windowing spacer wrapper. -->
|
||||
<VirtualList items={section.rows} rowHeight={56} key={(e) => e.id} {row} />
|
||||
{:else}
|
||||
{#each section.rows as entry (entry.id)}
|
||||
{@render row(entry)}
|
||||
{/each}
|
||||
{/if}
|
||||
<!-- Window each section's rows so a large grouped list (e.g. a big
|
||||
trash, grouped by remaining days) doesn't mount every row. -->
|
||||
<VirtualList items={section.rows} rowHeight={56} key={(e) => e.id} {row} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if grouped}
|
||||
<!-- Grouped GRID: a vertical stack of (header + its own windowed card
|
||||
grid) per section. The outer is a flex column, NOT `.files-grid-view`
|
||||
(which is itself a grid and would place each header/VirtualList into a
|
||||
cell) — the grid lives on each VirtualList's inner window via
|
||||
`windowClass`, exactly like the flat-grid arm. This was the last
|
||||
unwindowed path: a grouped-by-default grid (trash) mounted every card
|
||||
(benches/ROUND13.md §V1). -->
|
||||
<div class="rl-grouped-grid">
|
||||
{#each sections as section (section.key)}
|
||||
<div class="rl-swimlane-header rl-swimlane-header--grid" role="rowheader">
|
||||
<span class="rl-swimlane-header__label">{section.label}</span>
|
||||
{#if bucketAction}
|
||||
<span class="rl-swimlane-header__action">
|
||||
{@render bucketAction(section.key)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<VirtualList
|
||||
items={section.rows}
|
||||
columns={gridCols}
|
||||
rowHeight={240}
|
||||
windowClass="files-grid-view"
|
||||
key={(e) => e.id}
|
||||
{row}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if filesStore.viewMode === 'list'}
|
||||
@@ -987,6 +1004,22 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Grouped-grid container: a vertical stack of (header + its own windowed
|
||||
card grid) per section. Not `.files-grid-view` — the grid is on each
|
||||
VirtualList's inner window, so this outer element just stacks. */
|
||||
.rl-grouped-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
/* In the flex stack the `grid-column: 1 / -1` span (meant for the grid
|
||||
context) is inert; the header spans naturally as a block-level flex
|
||||
child. */
|
||||
.rl-swimlane-header--grid {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
/* Grid view date meta line. */
|
||||
.grid-meta__line {
|
||||
display: flex;
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// Round-13 §V1 — grouped views are windowed (benches/ROUND13.md).
|
||||
//
|
||||
// Before this round, the grouped GRID path mounted EVERY card:
|
||||
// `{#each sections}{#each section.rows}{@render row}` with no windowing
|
||||
// (the grouped-by-default trash grid, and the files route's grouped grid,
|
||||
// were the last unwindowed paths). Now each swimlane feeds its own windowed
|
||||
// <VirtualList> — a flex stack of (header + windowed card grid) per section
|
||||
// — so only a viewport-bounded slice of `.file-item` cards is realized,
|
||||
// regardless of group size.
|
||||
//
|
||||
// Gate: render the real ResourceList in grouped GRID mode with N=800 items
|
||||
// in one bucket and assert the mounted card count is viewport-bounded, not
|
||||
// N. jsdom does no layout, so VirtualList's visible band is a small constant
|
||||
// — the same lever the round-12 files page test documents.
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render } from '@testing-library/svelte';
|
||||
|
||||
vi.mock('$lib/api/endpoints/files', () => ({
|
||||
fileThumbnailUrl: () => '/thumb',
|
||||
thumbSizeForView: () => 'preview' as const
|
||||
}));
|
||||
|
||||
import ResourceList from './ResourceList.svelte';
|
||||
import type { GroupByDef } from './ResourceList.svelte';
|
||||
import { files as filesStore } from '$lib/stores/files.svelte';
|
||||
|
||||
interface TestFile {
|
||||
category: string;
|
||||
created_at: number;
|
||||
icon_class: string;
|
||||
icon_special_class: string;
|
||||
id: string;
|
||||
mime_type: string;
|
||||
modified_at: number;
|
||||
name: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
folder_id: string;
|
||||
path: string;
|
||||
size: number;
|
||||
size_formatted: string;
|
||||
sort_date: number;
|
||||
etag: string;
|
||||
content_hash: string;
|
||||
}
|
||||
|
||||
function fileItem(i: number): TestFile {
|
||||
return {
|
||||
category: 'Document',
|
||||
created_at: 0,
|
||||
icon_class: 'fa-file',
|
||||
icon_special_class: '',
|
||||
id: `f${i}`,
|
||||
mime_type: 'text/plain',
|
||||
modified_at: 0,
|
||||
name: `file-${i}.txt`,
|
||||
created_by: 'me',
|
||||
updated_by: 'me',
|
||||
folder_id: 'home',
|
||||
path: `/file-${i}.txt`,
|
||||
size: 4,
|
||||
size_formatted: '4 B',
|
||||
sort_date: 0,
|
||||
etag: 'e',
|
||||
content_hash: 'h'
|
||||
};
|
||||
}
|
||||
|
||||
// Single bucket → one big swimlane (the worst case the old grid mounted whole).
|
||||
const groupBys: GroupByDef[] = [
|
||||
{
|
||||
key: 'type',
|
||||
label: 'Type',
|
||||
orderBy: 'name',
|
||||
bucketOf: (item) => (item as TestFile).category ?? 'other',
|
||||
labelOf: (k) => k
|
||||
}
|
||||
];
|
||||
|
||||
describe('round13 §V1 — grouped grid is windowed', () => {
|
||||
beforeEach(() => {
|
||||
filesStore.viewMode = 'grid';
|
||||
});
|
||||
|
||||
it('mounts a viewport-bounded slice of cards, not all N, in grouped grid', () => {
|
||||
const N = 800;
|
||||
const items = Array.from({ length: N }, (_, i) => fileItem(i));
|
||||
const { container } = render(ResourceList, {
|
||||
props: {
|
||||
title: 'Round13',
|
||||
items,
|
||||
groupBys,
|
||||
groupBy: 'type',
|
||||
selectable: true,
|
||||
actions: undefined
|
||||
}
|
||||
});
|
||||
|
||||
const mounted = container.querySelectorAll('.file-item').length;
|
||||
// A swimlane header confirms we are on the grouped path.
|
||||
expect(container.querySelectorAll('.rl-swimlane-header').length).toBeGreaterThan(0);
|
||||
// Windowed: the visible band is viewport+overscan bounded, far below N.
|
||||
// (The pre-fix grid-grouped path mounted all 800.)
|
||||
expect(mounted).toBeGreaterThan(0);
|
||||
expect(mounted).toBeLessThan(120);
|
||||
expect(mounted).toBeLessThan(N / 4);
|
||||
});
|
||||
|
||||
it('full scroll height is still reserved (windowing spacer, not truncation)', () => {
|
||||
const N = 800;
|
||||
const items = Array.from({ length: N }, (_, i) => fileItem(i));
|
||||
const { container } = render(ResourceList, {
|
||||
props: { title: 'Round13', items, groupBys, groupBy: 'type', selectable: true }
|
||||
});
|
||||
// The VirtualList reserves total height via its `.vlist` spacer so the
|
||||
// scrollbar / end-of-list sentinel keep working — height must scale with
|
||||
// N, proving cards weren't simply dropped.
|
||||
const vlist = container.querySelector('.vlist') as HTMLElement | null;
|
||||
expect(vlist).not.toBeNull();
|
||||
const reserved = parseFloat(vlist!.style.height || '0');
|
||||
expect(reserved).toBeGreaterThan(1000);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user