perf: round 12 — auth write-path narrowing, fused quota gate, moka blob-cache index, media single-read, sized listing JSON
Benchmark-gated round (benches/ROUND12.md; every change ships with a BEFORE/AFTER harness + equivalence gates, one candidate rejected by its own bench): DB / query shapes (bench_round12_queries): - NC sharee search: username-only projection instead of the 21-column row (incl. the <=512 KiB avatar) per match, + gin_trgm_ops indexes on auth.users for the leading-wildcard ILIKE (4.98x; 54.7x with index). - Password login: delete the redundant full-row update_user — create_session already stamps last_login_at in its own txn (4.45x per login). - Email-verified stamp: narrow conditional UPDATE (8.9x); OIDC repeat login now compares profile state in memory and issues ZERO queries when nothing changed (was: full 17-column rewrite per login). - Refresh rotation: revoke+insert+stamp fused into one transaction via new rotate_session port method (1.18x). - WOPI CheckFileInfo / authorize_wopi_access: require(Read) + get_file + check(Update) overlapped with tokio::join!, original result precedence (cold 1.34x). - Upload quota gate: user-envelope + drive-cap checks fused into ONE round-trip (check_upload_quotas) — the NC chunked PUT pays this per chunk (1.81x, 2 -> 1 queries/chunk); shared verdict evaluators keep error shapes byte-identical. CPU / allocs (bench_round12_micro): - sized_json: pre-sized listing serialization replacing axum Json's 128 B seed + doubling-realloc chain on files/folder-resources/photos/search responses (1.40x, 13 -> 2 allocs per 500-row page; byte-identical). - Security headers: 4 SetResponseHeaderLayer folded into the CSP middleware pass (5 layers -> 1; 1.43x per request, -26 allocs; header set gated byte-identical incl. 304s). - Media capture-metadata: single-read extraction — nom-exif now parses the buffer kamadak already read (zero-copy Bytes) and videos open once with a kind() dispatch; per-image opens 2-3 -> 1 (1.44x warm geomean, 1.6-3.2x cold cache; extraction outputs gated identical incl. the MIME-mislabel track fallback). - Chunked-upload session ops: owner gate folded into the operation's own DashMap lookup + stack-encoded uuid compare (5 -> 3 lookups, -2 allocs, 1.28x per chunk). Blob cache (bench_blob_cache_index + round-3 regression guard): - CachedBlobBackend index: tokio::sync::Mutex<LruCache> -> moka::sync::Cache with byte weigher. The mutex serialized every cached chunk read and scaled NEGATIVELY (2.08 -> 1.07 Mops/s from 1 -> 2 readers); moka probes are lock-free (2.17x at K=2). Byte budget now enforced by moka (manual current_size + collect_evictions machinery deleted); eviction listener unlinks size-evicted files only (Replaced entries keep their file — gated). Single-flight miss gate unchanged (16 concurrent misses -> 1 fetch re-verified via the round-3 harness). - put_blob now populates the cache BEFORE the inner backend consumes the source file (the old order failed 100% of the time — local renames, S3/Azure delete the source — so the first read after a whole-file put re-downloaded from the remote); inner-put failure invalidates the entry. Frontend (vitest gates): - List-view thumbnails request the 150px icon rendition instead of 400px preview into a 40px slot (~7.1x fewer pixels, ~4-5x fewer bytes per thumbnail across list views); grid keeps preview. Rejected by its own bench (kept as evidence in bench_round12_micro §2): - Single-pass compression predicate: the monomorphized And-chain already costs ~4.6 ns / 0 allocs total; the fused node measured within noise. New migration: 20260719000000_users_search_trgm.sql (trgm indexes). Deferred with prepared design: grouped file/grid view virtualization (single-VirtualRows flatten, the photos pattern) — next round's headline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BfidAJD5AHw23jtvBUNamB
This commit is contained in:
@@ -163,3 +163,13 @@ export function fileThumbnailUrl(
|
||||
): string {
|
||||
return `/api/files/${fileId}/thumbnail/${size}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thumbnail size matched to the rendering slot. List rows draw thumbnails in
|
||||
* a 40×40 box, so the 150px `icon` rendition is already ≥2× retina density —
|
||||
* fetching the 400px `preview` there moved ~7× more pixels than the slot can
|
||||
* show (benches/ROUND12.md §F1). Grid cards (100×70 slot) keep `preview`.
|
||||
*/
|
||||
export function thumbSizeForView(view: 'grid' | 'list'): 'icon' | 'preview' {
|
||||
return view === 'list' ? 'icon' : 'preview';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Round-12 §F1 — list-view thumbnail rendition (benches/ROUND12.md).
|
||||
//
|
||||
// The list rows draw file thumbnails in a 40×40 CSS-px slot (100×70 in
|
||||
// grid), but both views requested the 400px `preview` rendition. The list
|
||||
// view now requests the 150px `icon` rendition: still ≥2× device-pixel
|
||||
// density for the 40px slot, at ~1/7th of the decoded pixels (and roughly
|
||||
// icon ≈ 4-8 KB vs preview ≈ 20-40 KB encoded WebP per thumbnail).
|
||||
//
|
||||
// Gates: the URL actually switches per view; grid keeps `preview`; the
|
||||
// pixel-area saving is the documented ~7x.
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { fileThumbnailUrl, thumbSizeForView } from './files';
|
||||
|
||||
describe('round12 §F1 — thumbnail rendition per view', () => {
|
||||
it('list view requests the icon rendition, grid keeps preview', () => {
|
||||
expect(thumbSizeForView('list')).toBe('icon');
|
||||
expect(thumbSizeForView('grid')).toBe('preview');
|
||||
expect(fileThumbnailUrl('abc', thumbSizeForView('list'))).toBe('/api/files/abc/thumbnail/icon');
|
||||
expect(fileThumbnailUrl('abc', thumbSizeForView('grid'))).toBe(
|
||||
'/api/files/abc/thumbnail/preview'
|
||||
);
|
||||
});
|
||||
|
||||
it('icon rendition moves ~7x fewer pixels than preview for the 40px slot', () => {
|
||||
// Server renditions: icon = 150px, preview = 400px (see the photos
|
||||
// srcset: `icon 150w, preview 400w, large 800w`).
|
||||
const areaRatio = (400 * 400) / (150 * 150);
|
||||
expect(areaRatio).toBeGreaterThan(7);
|
||||
// The 40×40 slot at 2x DPR needs 80px — icon's 150px still
|
||||
// oversamples it; preview was pure waste.
|
||||
expect(150).toBeGreaterThanOrEqual(80);
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,10 @@ vi.mock('$lib/api/endpoints/people', () => ({
|
||||
fetchPersonPhotos: vi.fn(),
|
||||
renamePerson: vi.fn()
|
||||
}));
|
||||
vi.mock('$lib/api/endpoints/files', () => ({ fileThumbnailUrl: () => '/thumb.png' }));
|
||||
vi.mock('$lib/api/endpoints/files', () => ({
|
||||
fileThumbnailUrl: () => '/thumb.png',
|
||||
thumbSizeForView: () => 'preview' as const
|
||||
}));
|
||||
vi.mock('$lib/stores/dialogs.svelte', () => ({ promptDialog: vi.fn() }));
|
||||
|
||||
import { fetchPeople, fetchPersonPhotos, renamePerson } from '$lib/api/endpoints/people';
|
||||
|
||||
@@ -4,7 +4,8 @@ vi.mock('$lib/api/endpoints/files', () => ({
|
||||
deleteFile: vi.fn(),
|
||||
fileDownloadUrl: () => '/d',
|
||||
fileInlineUrl: () => '/i',
|
||||
fileThumbnailUrl: () => '/t'
|
||||
fileThumbnailUrl: () => '/t',
|
||||
thumbSizeForView: () => 'preview' as const
|
||||
}));
|
||||
vi.mock('$lib/api/endpoints/favorites', () => ({ addFavorite: vi.fn() }));
|
||||
vi.mock('$lib/api/endpoints/photos', () => ({ fetchFileMetadata: vi.fn() }));
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
import { formatBytes } from '$lib/utils/format';
|
||||
import { formatDate, iconNameFromClass, fileIconKindClass } from '$lib/utils/display';
|
||||
import { gridColumns } from '$lib/utils/grid';
|
||||
import { fileThumbnailUrl } from '$lib/api/endpoints/files';
|
||||
import { fileThumbnailUrl, thumbSizeForView } from '$lib/api/endpoints/files';
|
||||
import {
|
||||
canThumbnailClientSide,
|
||||
preloadPdf,
|
||||
@@ -621,7 +621,7 @@
|
||||
{#if enableThumbnails && kind === 'file' && mimeVal && canThumbnailClientSide( { id: item.id, name: item.name, mime_type: mimeVal } )}
|
||||
<img
|
||||
class="file-thumb"
|
||||
src={fileThumbnailUrl(item.id)}
|
||||
src={fileThumbnailUrl(item.id, thumbSizeForView(filesStore.viewMode))}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
onerror={(e) => {
|
||||
|
||||
@@ -18,6 +18,7 @@ vi.mock('$lib/api/endpoints/files', () => ({
|
||||
// src for the fallback path; tests don't render actual thumbnails
|
||||
// but the module import needs to succeed.
|
||||
fileThumbnailUrl: () => '/thumb.png',
|
||||
thumbSizeForView: () => 'preview' as const,
|
||||
renameFile: vi.fn(),
|
||||
deleteFile: vi.fn()
|
||||
}));
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
fileThumbnailUrl,
|
||||
moveFile,
|
||||
renameFile,
|
||||
thumbSizeForView,
|
||||
uploadFileWithProgress
|
||||
} from '$lib/api/endpoints/files';
|
||||
import { folderZipUrl } from '$lib/api/endpoints/folders';
|
||||
@@ -2137,7 +2138,7 @@
|
||||
{#if canThumbnail(file)}
|
||||
<img
|
||||
class="file-thumb"
|
||||
src={fileThumbnailUrl(file.id)}
|
||||
src={fileThumbnailUrl(file.id, thumbSizeForView(filesStore.viewMode))}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
onerror={(e) => {
|
||||
|
||||
@@ -39,6 +39,7 @@ vi.mock('$lib/api/endpoints/files', () => ({
|
||||
deleteFile: vi.fn(),
|
||||
fileDownloadUrl: () => '/dl',
|
||||
fileThumbnailUrl: () => '/thumb',
|
||||
thumbSizeForView: () => 'preview' as const,
|
||||
moveFile: vi.fn(),
|
||||
renameFile: vi.fn(),
|
||||
uploadFile: vi.fn(),
|
||||
|
||||
@@ -15,7 +15,8 @@ vi.mock('$lib/api/endpoints/photos', () => ({
|
||||
vi.mock('$lib/api/endpoints/people', () => ({ peopleEnabled: vi.fn() }));
|
||||
vi.mock('$lib/api/endpoints/files', () => ({
|
||||
fileDownloadUrl: () => '/dl',
|
||||
fileThumbnailUrl: () => '/thumb'
|
||||
fileThumbnailUrl: () => '/thumb',
|
||||
thumbSizeForView: () => 'preview' as const
|
||||
}));
|
||||
|
||||
import { fetchPhotos } from '$lib/api/endpoints/photos';
|
||||
|
||||
Reference in New Issue
Block a user