8e55caa8a9
Seven behaviour-preserving allocation / copy / bandwidth cuts, each behind a counting-allocator BEFORE/AFTER gate that exit(1)s unless AFTER allocates strictly fewer than BEFORE (benches/ROUND29.md, examples/bench_round29_micro.rs). - [B] Content-cache serve fast path (optimized_inner Tier 1 + get_file_range_preloaded — the video-scrub hot path): probe the cache with a borrow first and build the owned get_or_load args (quoted-etag / key / id Strings) only on a miss, instead of allocating them before every probe and discarding them on a hit. 6 -> 0 allocs per cache hit. Splits get_or_load into get + load_and_cache so the miss path is not re-probed and the hit/miss stat counters stay byte-identical. Also drops the unconditional content_hash/name clones that ran for the >=10 MB streaming tier that used neither. - [A] NextCloud REPORT emit loops: per-row href String (and format! per folder row) -> one reused href_buf via nc_href_into / nc_collection_href_into with the URL-encoded user computed once per page. 1497 fewer allocs on a 500-row page. - [C] read_full: a single-frame blob is returned zero-copy instead of a second whole-payload memcpy into a fresh BytesMut; multi-frame path unchanged. - [D] login-lockout key: to_lowercase()+format! -> one pre-sized ASCII buffer (non-ASCII keeps str::to_lowercase). 3 -> 1 alloc/req, byte-identical key. - [E] NC composite-username parse: owned clone/to_string -> &str borrow of the already-owned raw_username. 1 -> 0 alloc on the common no-marker path. - [F] get_contacts_in_group: stop SELECTing the discarded multi-KB vcard column (the live method ROUND25 §Q2 missed; ContactDto has no vcard field). - [G] count_admin_users: add count_users_by_role -> scalar COUNT(*) instead of hydrating every admin's full row (incl. up-to-512 KiB avatar + ui_preferences JSONB) only to .len() it, on a bootstrap-polled status endpoint. All seven gates pass; cargo fmt --check and cargo clippy --all-features --all-targets -D warnings clean. §F/§G additionally validated against a live PostgreSQL 16 with the full migration set (query validity, result equivalence, 600000 -> 8 byte wire delta on the admin count). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhpDZxSQTAGnAqCHUdtG5N
245 lines
13 KiB
Markdown
245 lines
13 KiB
Markdown
# Round 29 — read-path cache-serve allocs, NC REPORT href buffer, auth per-request allocs, DB over-fetch
|
||
|
||
Seven behaviour-preserving cuts, each behind a counting-allocator BEFORE/AFTER gate
|
||
that `exit(1)`s (`GATE FAIL … rollback`) unless AFTER allocates strictly fewer than
|
||
BEFORE. Sections span the four hot paths a deep re-audit surfaced that the prior 28
|
||
rounds had not reached: the content-cache serve fast path (video scrubbing), the
|
||
NextCloud REPORT emit loops, the NextCloud Basic-Auth request path, and two
|
||
Postgres over-fetch sites.
|
||
|
||
Reproduce:
|
||
|
||
```bash
|
||
RUSTFLAGS="-C target-cpu=x86-64-v3" \
|
||
cargo run --release --features bench --example bench_round29_micro
|
||
```
|
||
|
||
| § | site | allocs/op BEFORE→AFTER | wall |
|
||
|----|------|-----------------------:|-----:|
|
||
| A | NC REPORT href reused buffer | 3500 → 2003 /500-row page | 1.59× |
|
||
| B | cache-serve borrow-probe (video scrub) | 6 → 0 /cache hit | 5.85× |
|
||
| C | `read_full` single-frame zero-copy | 1 → 0 | 194× |
|
||
| D | login-lockout single-alloc key | 3 → 1 | 2.10× |
|
||
| E | NC composite-username parse borrow | 1 → 0 | 2.86× |
|
||
| F | contact-group `vcard` over-fetch | 200 → 0 /200-row page | decode-shape |
|
||
| G | admin-count `COUNT(*)` vs hydrate | 25 → 0 /poll | decode-shape |
|
||
|
||
---
|
||
|
||
## [B] Content-cache serve fast path: eager owned args built before the borrow-probe (HIGHEST — the hottest read path)
|
||
|
||
`file_retrieval_service::optimized_inner` (Tier 1) and `get_file_range_preloaded`
|
||
built the owned `get_or_load` arguments — `format!("\"{}\"", hash)` (the quoted
|
||
etag), `hash.to_string()` (the cache key), `id.to_string()` — **before** the cache
|
||
was probed. But `FileContentCache::get_or_load`'s first line is a lock-free
|
||
`self.get(&cache_key)` that returns on a hit and never touches `etag` / `ct` / the
|
||
load closure. So every cache **hit** — the steady state of a repeat download and of
|
||
a *range-seek storm* (video scrubbing hits `get_file_range_preloaded` on every
|
||
seek) — allocated ~3–6 Strings and immediately dropped them. The returned etag/ct
|
||
are discarded by both callers (`let (bytes, ..)`), and the response etag is built
|
||
independently from `file_dto.etag`, so the eager etag was dead on the miss path too.
|
||
|
||
AFTER probes `cache.get(&hash)` (a borrow, zero owned allocs) first and slices on a
|
||
hit; only a miss builds the owned args and calls the new `load_and_cache`.
|
||
`get_or_load` is split into `get` + `load_and_cache` (it now composes them), so the
|
||
miss path is **not** re-probed — the hit/miss stat counters stay byte-identical to a
|
||
single `get_or_load` call. Also folds in the removal of the unconditional
|
||
`content_hash.clone()` + `name.clone()` that ran for every request including the
|
||
≥10 MB streaming tier that used neither.
|
||
|
||
| arm | ns/op | allocs/op |
|
||
|--------|------:|----------:|
|
||
| BEFORE | 241.1 | 6.00 |
|
||
| AFTER | 41.2 | 0.00 |
|
||
|
||
**6 → 0 allocs per cache hit, 5.85× wall.** On a 200-seek video scrub this removes
|
||
~1200 throwaway allocations. Equivalence: same cached `Bytes` returned; the split
|
||
preserves the exact single-`get` stat accounting.
|
||
|
||
## [A] NextCloud REPORT emit loops: per-row href String → one reused buffer + once-encoded user
|
||
|
||
The two REPORT handlers (`report_handler`: favorites `filter-files` + `search`)
|
||
each emit a file loop and a folder loop that built `<d:href>` per row with
|
||
`nc_href(url_user, subpath)` — a fresh `String` per file row — and
|
||
`format!("{}/", nc_href(...))` — **two** Strings per folder row — while re-encoding
|
||
the constant `url_user` on every row. The hotter PROPFIND child loop was already
|
||
hoisted to a reused buffer + once-encoded prefix (ROUND19/27); the REPORT loops were
|
||
the last per-row href allocation on the NC emit surface (the ROUND20/27/28 deferred
|
||
item). AFTER adds `nc_href_into` / `nc_collection_href_into` (the 0-alloc,
|
||
write-into-a-buffer form; `nc_href`/`nc_collection_href` now delegate to them, no
|
||
duplication) and computes into one `href_buf` reused across both loops with the
|
||
`encoded_user` computed once per page.
|
||
|
||
| arm | ns/op | allocs/op |
|
||
|--------|----------:|----------:|
|
||
| BEFORE | 115 849.2 | 3500.00 |
|
||
| AFTER | 72 835.0 | 2003.00 |
|
||
|
||
**1497 fewer allocs on a 500-row page, 1.59× wall.** The 2003 residual is the
|
||
per-segment `urlencoding::encode` (4 path segments/row) that AFTER keeps to stay
|
||
byte-identical; the win is the removed per-row href `String`, the folder `format!`,
|
||
and the per-row user encode. Equivalence: AFTER href bytes match BEFORE
|
||
(file + folder) across a matrix of paths.
|
||
|
||
## [C] `read_full`: single-frame blob no longer double-copied
|
||
|
||
`read_full` reassembled the blob stream with `BytesMut::with_capacity(cap)` +
|
||
`extend_from_slice` per frame. The local backend yields owned contiguous `Bytes`
|
||
frames, and a sub-`CACHE_THRESHOLD` blob arrives as exactly **one** frame — yet the
|
||
old code copied that whole payload a second time into a fresh buffer (a full-payload
|
||
memcpy + a `BytesMut` alloc) for every small cacheable download and every
|
||
uncacheable small read. AFTER returns the sole frame directly; only a multi-frame
|
||
read pays the pre-sized concat (byte-identical).
|
||
|
||
| arm | ns/op | allocs/op |
|
||
|--------|------:|----------:|
|
||
| BEFORE | 3325.9 | 1.00 |
|
||
| AFTER | 17.2 | 0.00 |
|
||
|
||
**1 → 0 allocs and one 200 KB memcpy removed (194× wall on the isolated copy).**
|
||
Equivalence: identical `Bytes` out; multi-frame path unchanged.
|
||
|
||
## [D] NextCloud login-lockout key: `to_lowercase()` + `format!` → one ASCII buffer
|
||
|
||
`LoginLockoutService::key` built the composite `(account, IP)` cache key with
|
||
`format!("{}|{}", username.to_lowercase(), client_ip)` — two heap allocations — on
|
||
**every** NC request (the check on the way in; a hit on the happy path is a lockout
|
||
miss). App passwords authenticate with an already-lowercase ASCII username in ~all
|
||
traffic, so AFTER renders the lowercased key into one pre-sized buffer for the ASCII
|
||
case and keeps `str::to_lowercase` only on the rare non-ASCII branch (exact Unicode,
|
||
e.g. final-sigma, semantics).
|
||
|
||
| arm | ns/op | allocs/op |
|
||
|--------|------:|----------:|
|
||
| BEFORE | 95.7 | 3.00 |
|
||
| AFTER | 45.7 | 1.00 |
|
||
|
||
**3 → 1 alloc, 2.10× wall.** Byte-identical key verified across {ASCII lower,
|
||
mixed-case, composite `~` marker, IPv4, IPv6, non-ASCII, `unknown`}. The lockout
|
||
decision (same key bytes, threshold, TTL) is unchanged; failed verifications still
|
||
bypass the cache and pay full Argon2.
|
||
|
||
## [E] NextCloud composite-username parse: owned clone → borrow
|
||
|
||
The `{username}~{drive_marker}` split allocated the prefix per request —
|
||
`raw_username.clone()` on the common no-marker path (a full duplicate),
|
||
`u.to_string()` + `m.to_string()` on the marker path — even though `username` is
|
||
only ever passed by reference and `raw_username` outlives every use before it moves
|
||
into `NcSession`. AFTER borrows `&str` slices out of the already-owned
|
||
`raw_username`.
|
||
|
||
| arm | ns/op | allocs/op |
|
||
|--------|------:|----------:|
|
||
| BEFORE | 22.5 | 1.00 |
|
||
| AFTER | 7.9 | 0.00 |
|
||
|
||
**1 → 0 allocs on the common DAV path, 2.86× wall.** Stacks with §D on the same
|
||
per-request surface. Byte-identical inputs reach every downstream call.
|
||
|
||
## [F] Contact-group listing: stop fetching the multi-KB `vcard` only to drop it
|
||
|
||
`contact_group_pg_repository::get_contacts_in_group` SELECTed `c.vcard` — the full
|
||
serialized vCard TEXT with an embedded base64 `PHOTO`, the largest column — and
|
||
decoded it into a `String` per contact, but its sole live caller
|
||
(`list_contacts_in_group`) maps every row to `ContactDto`, which has **no vcard
|
||
field**, so it was fetched, shipped, decoded, and dropped. This is the ROUND25 §Q2
|
||
`row_to_contact_lite` treatment applied to the **live** group method this time (Q2
|
||
shipped it to `get_contacts_by_group`, which has zero call sites). AFTER omits the
|
||
column and passes `String::new()`.
|
||
|
||
| arm | ns/op | allocs/op |
|
||
|--------|---------:|----------:|
|
||
| BEFORE | 70 121.1 | 200.00 |
|
||
| AFTER | 1.4 | 0.00 |
|
||
|
||
The micro isolates the discarded-`String` decode (200 rows × 8 KiB): **200 → 0
|
||
per-row allocs**. The new SQL was run against a live schema (all columns resolve,
|
||
join valid, empty and populated results correct); `ContactDto` output is
|
||
byte-identical. Same unit economics ROUND25 §Q2 *measured* (6.4× wall on 1000 ×
|
||
8 KiB vCards). Bandwidth win scales with the embedded-photo size.
|
||
|
||
## [G] admin-user count: hydrate every full row → scalar `COUNT(*)`
|
||
|
||
`count_admin_users` (the system-status / initialization endpoint, polled at
|
||
bootstrap / login-page render) called `list_users_by_role("admin").len()`, fetching
|
||
every admin's full 21-column row — including the up-to-512 KiB avatar `image` data
|
||
URI and the `ui_preferences` JSONB (decoded into a discarded `serde_json::Value`
|
||
DOM) — only to take the length. AFTER adds `count_users_by_role` →
|
||
`SELECT COUNT(*) … WHERE role::text = $1` through the existing domain-trait / port
|
||
delegation pattern.
|
||
|
||
| arm | ns/op | allocs/op |
|
||
|--------|---------:|----------:|
|
||
| BEFORE | 12 634.9 | 25.00 |
|
||
| AFTER | 0.7 | 0.00 |
|
||
|
||
The micro isolates the hydrate-N-rows-then-`len` cost (3 admins × a 64 KiB avatar +
|
||
JSONB DOM): **25 → 0 allocs**. Validated on a live DB with 3 seeded admins carrying
|
||
200 KiB avatars: the `COUNT(*)` returns the correct `3` while the wire payload drops
|
||
from **600 000 bytes** (the three avatars) + JSONB to **8 bytes**, and the app
|
||
hydrates zero `User` structs. Win scales with admin count, avatar size, and
|
||
PG-connection distance.
|
||
|
||
---
|
||
|
||
## Not shipped — carried forward
|
||
|
||
Concrete, still-valuable items surfaced by the same re-audit, deferred here because
|
||
they need a fixture this round can't drive, a structural change wider than an
|
||
allocation cut, or a live-DB validation harness:
|
||
|
||
- **Delta `store_loose_chunks` check-then-write (highest-value dedup item).** The
|
||
delta upload path writes every received chunk to the backend unconditionally, then
|
||
registers with `ON CONFLICT DO NOTHING` — unlike the main `settle_batch` ingest,
|
||
which runs one `WHERE hash = ANY($1)` existence probe per batch and writes only
|
||
absent chunks (the discipline the S3 backend's dropped-HEAD comment already
|
||
assumes). Bringing the delta path to parity eliminates redundant disk writes /
|
||
object-store PUTs for content the server already has (multi-tenant overlap,
|
||
abandoned-upload orphan re-sends). Deferred: near-zero on a single-tenant local
|
||
server (the highest win is on S3/Azure), it restructures the ingest, and its gate
|
||
is a backend-write-count harness (not the allocator), so it wants its own pass. The
|
||
frontend already negotiates a Dropbox-style batched have/need exchange, so the
|
||
client does **not** re-upload content the server has — this is purely the
|
||
server-side write.
|
||
- **`ingest_chunks_from_stream` end-of-stream reshape move.** The final chunk
|
||
registration clones every newly-written 64-byte hash to reshape for `sync_blobs` +
|
||
the UNNEST bind (`~4000 String allocs on a 1 GB upload`); the sibling sites were
|
||
converted to `into_iter().unzip()` moves in ROUND23/25 but this one wasn't.
|
||
Deferred: `st.written` must be restored on the two fallible error paths before
|
||
`guard.rollback()` (which itself `mem::take`s it), so the move needs a
|
||
`rollback_with_written` variant — error-path surgery on the ingest correctness path
|
||
for a once-per-upload (not per-frame) alloc cut.
|
||
- **NC `parse_basic_auth` credential borrow.** The shared helper returns
|
||
`(String, String)` via two `to_string()`s; the native Basic path already hands
|
||
`credentials.split_once(':')` `&str` borrows to `verify_basic_auth`. Bringing NC to
|
||
parity removes 2 allocs/request but touches a unit-tested shared helper and wants a
|
||
`decode_basic_credentials` extraction to avoid a third copy of the base64 logic.
|
||
- **DB `create_folder` 2 round-trips → 1 `INSERT … SELECT … RETURNING`** (the drive_id
|
||
is a pure function of the parent; `move_folder` already folds this). Needs the
|
||
`RowNotFound → not_found` branch and a live-DB gate on the dup-name / missing-parent
|
||
outcomes.
|
||
- **DB `list_users` / `search_users` lite SELECT** (drop `password_hash` +
|
||
`ui_preferences`, neither in `UserDto`) and **contacts `(address_book_id, full_name,
|
||
first_name, last_name)` composite index** for the paginated `ORDER BY`.
|
||
- **File-metadata short-TTL cache** so a range-seek storm stops re-`SELECT`ing the
|
||
whole file row after the first seek (ROUND7 removed the per-seek authz; the metadata
|
||
read remains). A genuinely new cache + write-invalidation wiring — its own validated
|
||
pass.
|
||
- **S3 read zero-copy forward** and the encrypted `PLAINTEXT_EMIT_SIZE` bump — the
|
||
ROUND25–28 carried-forward items needing MinIO / real-backend fixtures.
|
||
|
||
## Environment / methodology
|
||
|
||
- Counting global allocator (`examples/bench_round29_micro.rs`), no Postgres for the
|
||
gate. Each section is BEFORE (replica of the shipped-before shape) vs AFTER (replica
|
||
of the shipped-after shape, which the source now matches) with a value-equivalence
|
||
assertion and a `GATE FAIL … rollback` `exit(1)` if AFTER doesn't allocate fewer
|
||
than BEFORE. §F and §G additionally validated against a live PostgreSQL 16 with the
|
||
full migration set applied and a seeded fixture (query validity, result equivalence,
|
||
wire-byte delta).
|
||
- Built with `RUSTFLAGS="-C target-cpu=x86-64-v3"` (the checked-in
|
||
`.cargo/config.toml` pins `target-cpu=native`, which `SIGILL`s on this host).
|
||
- Verified beyond the bench: `cargo fmt --all --check` clean,
|
||
`cargo clippy --all-features --all-targets -- -D warnings` clean,
|
||
`cargo test --lib` green.
|