f58d72a780
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
204 lines
12 KiB
Markdown
204 lines
12 KiB
Markdown
# 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
|
||
`<Icon>`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,
|
||
`<N/4`), a swimlane header confirms the grouped path, and the `.vlist`
|
||
spacer still reserves the full scroll height (cards windowed, not dropped).
|
||
Preserved (unchanged, verified by the existing files/trash/recent tests):
|
||
selection (`SvelteSet.has` reads stay inside the row), the ROUND11 §S2
|
||
fine-grained favorite star (the grouping derive reads only item identity +
|
||
order, never `favoriteIds`/`selected`), drag-drop, keyboard, and the
|
||
ROUND12 §F1 `thumbSizeForView` icon/preview switch.
|
||
|
||
Scope note: this windows the grouped arms (the actual defect) while leaving
|
||
the already-windowed flat arms on `VirtualList`. Unifying all four arms onto
|
||
one `VirtualRows` (the photos-timeline single-pass model) is a clean
|
||
follow-up that also removes the per-section scroll listeners — deferred so a
|
||
pitch-measurement change can't regress the flat views that are fine today.
|
||
|
||
## [Q1] Group-notification recipient expansion
|
||
|
||
```
|
||
cargo run --release --features bench --example bench_round13_queries # §1
|
||
```
|
||
|
||
`RecipientNotificationService` fans a group share out to its members via
|
||
`get_users_by_ids`, whose 21-column projection dragged the ≤512 KiB avatar
|
||
`image` (TOAST-detoasted per row) and the `ui_preferences` JSONB — of which
|
||
the notification path reads *neither* (only email/eligibility fields). It is
|
||
the ROUND12 §Q1 sharee-avatar pattern on the group-notify path, ×M members.
|
||
`get_users_by_ids` has exactly one production caller, so it is narrowed
|
||
in-place (image + ui_preferences dropped; doc updated: notification-recipient
|
||
projection). 30-member fan-out: 8.60 → 0.25 ms, ~7.7 MB of avatar/JSONB kept
|
||
off the wire. Gate: identical `(id, email, notify_on_share)` set.
|
||
|
||
## [Q2] Login provisioning EXISTS probes
|
||
|
||
The Personal-Drive / Default-Calendar / Default-Address-Book provisioning
|
||
hooks fire on EVERY login; the calendar and address-book hooks tested
|
||
"already provisioned?" by `list_*_by_owner(..).is_empty()` — hydrating every
|
||
owned row (calendars carry description/color TEXT) just to look at
|
||
emptiness. New `has_owned_calendar` / `has_owned_address_book` back it with
|
||
`SELECT EXISTS(...)` (the ROUND9 §7 `Drive::is_empty` COUNT→EXISTS pattern),
|
||
short-circuiting at the first row. 4 owned calendars: 0.193 → 0.170 ms; the
|
||
margin widens with the owned-row count. Gate: EXISTS agrees with
|
||
hydrate-all, present and absent. (The drive hook's unconditional `set_role`
|
||
re-emit on every login — an authz write — is flagged, not shipped: it's a
|
||
deliberate self-heal and touches authz semantics, the ROUND12 class that
|
||
awaits maintainer sign-off.)
|
||
|
||
## [Q3] Recent-access prune only on insert
|
||
|
||
`RecentService::record_access` ran `upsert_access` then `prune`
|
||
unconditionally — but a re-access is an `ON CONFLICT DO UPDATE` that only
|
||
refreshes a timestamp and can never push the user over the cap, so the prune
|
||
(a DELETE over an `OFFSET` self-subquery) was a wasted round-trip on that
|
||
common path. `upsert_access` now `RETURNING (xmax = 0)` reports whether it
|
||
inserted; the service prunes only then. A single fused CTE was rejected: a
|
||
data-modifying CTE's outer DELETE sees the pre-insert snapshot, so it would
|
||
under-prune by one on the boundary insert — the two-statement,
|
||
prune-on-insert shape is the correct one. Re-access: 0.567 → 0.324 ms. Gate:
|
||
`xmax` flags insert vs update correctly and the row count stays at the cap.
|
||
|
||
## [L1] Locale supported-codes precompute
|
||
|
||
```
|
||
cargo run --release --features bench --example bench_round13_micro # §L1
|
||
```
|
||
|
||
The `Accept-Language` extractor rebuilt the supported-locale list — N fresh
|
||
heap `String`s + two `Vec`s — on every anonymous request, though the set is
|
||
fixed at startup (the ROUND10 §15 "process-invariant rebuilt per request"
|
||
class). `LocaleRegistry` now materializes `supported_codes: Arc<Vec<String>>`
|
||
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`.
|