# Round 13 — grouped-view virtualization, notification/login query narrowing, HTTP dedup, locale precompute Benchmark-gated, same rule as ROUND2-12: every change ships with a BEFORE/AFTER benchmark and an equivalence/safety gate; an AFTER that doesn't beat its BEFORE gets rolled back or redesigned. This round's discipline story is a *correctness* finding the sweep surfaced under a perf banner: the "media hooks read the same blob 3×" lead turned out to be "1 real read + 2 *broken* reads" (the raw-path readers resolve only for local + unencrypted + single-chunk blobs), so it is flagged for maintainers as a correctness bug, NOT shipped as a perf change (§Not shipped). Measured on 4 cores / 15 GiB, local PostgreSQL 16 (fsync off), release profile; frontend on Node 22 / vitest 4 (jsdom). Reproduce any row with the command in its section. ## Summary | # | change | key metric | before → after | |--:|---|---|---| | V1 | Grouped views windowed (files route + ResourceList; grid was the last unwindowed path — trash is grouped-by-default in grid) | `.file-item` mounted, 800-item group | **800 → <120** (viewport-bounded) | | Q1 | Group-notification recipient expansion: drop the ≤512 KiB avatar `image` + `ui_preferences` JSONB from `get_users_by_ids` (email path never reads them) | 30-member fan-out | 8.60 → 0.25 ms (**34.3x**) · ~7.7 MB off the wire | | Q2 | Login provisioning idempotency: `list_*_by_owner().is_empty()` → `SELECT EXISTS` (×2: calendar + address book, on EVERY login) | 4 owned calendars | 0.193 → 0.170 ms (**1.13x**, widens with owned-row count) | | Q3 | Recent-access: prune only when the upsert actually inserted (`RETURNING xmax=0`) — a re-access can't grow the set | per re-access | 0.567 → 0.324 ms (**1.75x**) · prune round-trip skipped | | L1 | Locale `Accept-Language`: precomputed supported-codes list vs rebuilding N heap Strings per anonymous request | 16 locales | 616 → 17.3 ns (**35.7x**) · 18 → 1 allocs | | H1 | Duplicate `TraceLayer` on `/api` removed (the global stack already wraps it) | per `/api` request | 1.86 → 1.42 µs (**1.31x**) · −6 allocs | | H2 | `client_ip` span field: borrow-only `ClientIpDisplay` vs an owned `String` per request | per request | 187 → 173 ns · −1 alloc | ## [V1] Grouped views are windowed (the ROUND10-deferred headline) ``` cd frontend && npx vitest run src/lib/components/round13.bench.test.ts ``` The moment any group-by was active, both the files route (`routes/files/[...path]/+page.svelte`) and `ResourceList` left their windowed `VirtualList` paths and rendered `{#each groups}{#each rows}` — the GRID arm mounted **every** card, and the accumulated listing is the whole folder, so a big grouped grid mounted thousands of `.file-item`s (~8-10 ``s + ~8 buttons each), a multi-second main-thread block. `/trash` is grouped-by-default, so a grid-view trash page hit this on first load. The fix is the symmetric one the grouped-LIST arm already used and the flat-GRID arm already proved: **window each swimlane with its own `VirtualList`** (`windowClass="files-grid-view"` puts the card grid on the list's inner window). The outer grouped-grid container is a flex column (`.files-grouped-grid` / `.rl-grouped-grid`), NOT `.files-grid-view` — that class is itself a grid and would place each header/VirtualList into a cell; the grid now lives per-section. The files route additionally folds each group's separate `folders`/`files` into one ordered `Entry` stream (`groupedEntries`, folders-then-files — the exact old render order) so a section feeds one `VirtualList`. The prior claim in a code comment that "`files-grid-view` … can't host the windowing spacer" was simply wrong (the flat grid disproves it). Gate: render the real `ResourceList` in grouped GRID mode at N=800 in one bucket — mounted `.file-item` count is **<120** (viewport+overscan bounded, `>` once in `discover()`; the extractor borrows it and builds only the `&[&str]` view the crate needs. 16 locales: 616 → 17.3 ns, 18 → 1 allocs per anonymous request. Gate: precomputed and rebuilt code SETS identical (order is irrelevant — `accept_language::intersection` ranks by header q-values). ## [H1][H2] HTTP micro-pack ``` cargo run --release --features bench --example bench_round13_micro # §H1, §H2 ``` - **Duplicate `TraceLayer` on `/api`** — `routes.rs` layered its own `TraceLayer::new_for_http()`, but the global `TraceLayer + ClientIpMakeSpan` stack in `main.rs` wraps the whole app (the `/api` router is nested into it), so every `/api` request paid TWO span + response-future layers. Removed; end-to-end 1.86 → 1.42 µs/request, −6 allocs. Gate: response status identical with 1 vs 2 layers. - **`client_ip` span field** — `ClientIpMakeSpan::make_span` allocated an owned `String` per request purely to feed `%client_ip` (Display). New borrow-only `ClientIpDisplay` renders straight into the span's field storage (forwarded header borrowed, peer rendered in place): 187 → 173 ns, −1 alloc. Gate: byte-identical to the owned resolver across all four resolution cases. ## Not shipped — correctness finding surfaced by the perf sweep - **Media hooks' raw blob reads are broken, not merely duplicated.** The round-12 deferred "media metadata + faces + thumbnail each read the blob" lead was investigated for a shared-read refactor. The investigation found the premise was wrong: `MediaMetadataService` and `FaceIndexingService` read `.blobs/{file_hash}.blob` **directly**, but that path exists only for **local + unencrypted + single-chunk** blobs — for a normal multi-MB (multi-chunk) photo it does not exist, on S3/Azure there is no local `.blobs` tree, and on encrypted backends it is ciphertext. So today those two hooks silently produce **no capture date / no GPS / no faces** for the common case, while only the thumbnail hook (which goes through `dedup.read_blob_bytes`, honoring chunk-reassembly + decryption) works. The fix is to route both through `read_blob_bytes` — but that is a **correctness fix that is perf-neutral-to-negative** (it makes reads that currently fail actually run), so it does not belong in a benchmark-gated perf round. Flagged for maintainers as a correctness bug with the exact call sites; a shared-`Bytes` provider (single decode-plaintext read fanned to the hooks) is the perf follow-up once the correctness fix lands. ## Deferred / flagged (not shipped this round) - **Unify all four listing arms onto one `VirtualRows`** (flat + grouped × list + grid), the photos-timeline single-pass model — removes the per-section scroll listeners the grouped paths now carry and the four-branch render in both files route and ResourceList. Wants a pitch-measurement pass so it can't drift the flat views that work today (V1 scope note). - **Drive-provisioning `set_role` re-emit on every login** (authz write; a self-heal for a historical partial-provision case) — needs maintainer sign-off, same class as the ROUND12 auth-write deferrals. - **NC per-session quota budget cache** (0 queries/chunk instead of the ROUND12 fused 1) — needs a staleness/invalidation story (ROUND12 flag stands). - **`mp3_duration` full-file scan when the ID3 `TLEN` tag is present** (ingest path) — preferring TLEN is a speed/accuracy tradeoff on VBR files; maintainer call. - **Thumbnail orientation re-parses EXIF** that capture-metadata already parsed — reusing the persisted `orientation` is ordering-dependent (hooks run concurrently). - **`CachedBlobBackend::local_blob_path` sync `stat`** (ROUND10-12 flag stands; needs an async port variant). - **`admin_settings_service` ~7 sequential autocommit upserts on OIDC save** — admin-only, fired a handful of times per deployment; confirmed still present, judged not worth entangling the hot-reload logic (same verdict as ROUND12's skipped REST quota-pair fusion). ## Environment / methodology - `cargo run --release --features bench --example bench_round13_queries` — needs Postgres; seeds + sweeps its own fixtures (`BENCH_PASSES`, `BENCH_GROUP`, `BENCH_CALS`, `BENCH_RECENT_CAP`). - `cargo run --release --features bench --example bench_round13_micro` — counting allocator; §L1 reads the shipped `frontend/static/locales`. - `cd frontend && npx vitest run src/lib/components/round13.bench.test.ts`.