Files
Oxicloud/benches/ROUND29.md
T
Claude 8e55caa8a9 perf(round29): cache-serve borrow-probe, NC REPORT href buffer, auth per-req allocs, DB over-fetch
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
2026-07-21 10:25:36 +00:00

245 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.