Files
Oxicloud/benches/ROUND27.md
T
Claude 8c936de50d perf(round27): NextCloud PROPFIND oc:id per-row buffer, contact JSONB write direct-serialize
Two behaviour-preserving allocation cuts (benches/ROUND27.md), each with a
counting-allocator BEFORE/AFTER gate that rolls back if AFTER does not allocate
fewer than BEFORE:

- H1 NextCloud PROPFIND: the streaming page loops built oc:id as a fresh String
  per child (format_oc_id -> format!("{:08}{}", id, instance)). Add
  format_oc_id_into(&mut buf, id, svc) and compute into one oc_buf reused across
  the page (next to the existing href buffer) — 1 String/row -> 0. 998->0
  per-row allocs on a 500-row page, 2.16x wall. The write fns still take
  Option<&str>, so no signature change; oc:id bytes identical. Scoped to the two
  PROPFIND page loops (the hot directory-listing path); REPORT/trashbin deferred.
- P2 contact create/update: bind sqlx::types::Json(&dtos) (Encode runs to_writer
  straight into the JSONB buffer) instead of serde_json::to_value(&dtos) + bind,
  which built a throwaway Value DOM per JSONB column. Write-side twin of ROUND23
  J1. 21->2 allocs, 4.68x wall for a 3-entry column. Behaviour-preserving:
  to_value sorts keys and direct serialize keeps struct order, but Postgres
  normalizes JSONB key order so the stored value is identical (verified via psql:
  '{...alpha...}'::jsonb = '{...struct...}'::jsonb -> t), and reads decode by
  field name; the etag comes from the domain entity, not the stored JSONB.

Adds bench_round27_micro. Verified: cargo fmt clean, cargo clippy --features
bench -D warnings clean (real exit), cargo test --lib --features bench = 529
passed / 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8gs91AhmazoxMsDcNk3KT
2026-07-21 01:30:53 +00:00

5.4 KiB
Raw Blame History

Round 27 — NextCloud PROPFIND oc:id per-row buffer (alloc), contact JSONB write direct-serialize (alloc)

Two behaviour-preserving allocation cuts from the ROUND25/26 backlog, each behind a counting-allocator BEFORE/AFTER gate that exit(1)s ("GATE FAIL … rollback") unless AFTER allocates strictly fewer than BEFORE.

Reproduce:

RUSTFLAGS="-C target-cpu=x86-64-v3" \
  cargo run --release --features bench --example bench_round27_micro

[H1] NextCloud PROPFIND: per-row oc:id String → one reused buffer per page

The streaming PROPFIND page loops built oc:id as a fresh String per child — format_oc_id(id, svc) = format!("{:08}{}", id, instance_id) — then passed oc_id.as_deref() into write_{file,folder}_response. The sibling per-row costs (href, etag, dates) were already reduced to a reused buffer / borrowed events (ROUND19 §M6, ROUND20 §C1); oc:id was explicitly left as the last per-row String (ROUND20 deferred). AFTER adds format_oc_id_into(&mut out, id, svc) (the 0-alloc form) and computes into one oc_buf reused across the page, alongside the existing href buffer — 1 String/row → 0 (amortized to one buffer per page). The write functions still take Option<&str>, so their signatures don't change; the emitted oc:id bytes are identical.

Scoped to the two PROPFIND page loops (the hot directory-listing path — the most common NextCloud operation). The lower-traffic REPORT/trashbin sites and the single-emit self-response sites are left as format_oc_id (see Not shipped).

arm ns/op allocs/op
BEFORE 34 185.3 1 000.00
AFTER 14 484.9 2.00

998 → 0 per-row allocs (2 amortized buffers for the whole page), 2.36× wall over a 500-row page. Gate: AFTER allocs/op strictly lower. Equivalence: the oc:id bytes from the reused buffer match format_oc_id for every id.

[P2] Contact create/update: throwaway serde_json::Value DOM → Json(&dtos) direct serialize

contact_pg_repository::{create,update}_contact built a throwaway serde_json::Value per JSONB column (serde_json::to_value(&email_dtos) etc.) and bound that — sqlx re-serializes the Value to JSONB bytes at encode time, so the flow was DTOs → Value DOM (alloc) → bytes, the tree discarded. AFTER binds sqlx::types::Json(&dtos), whose Encode runs serde_json::to_writer on the borrowed value straight into the JSONB buffer — no intermediate DOM. This is the write-side twin of the read-side ROUND23 §J1 fix. The old .unwrap_or(JsonValue::Null) fallback was effectively dead (serializing a Vec<plain-struct> can't fail).

arm ns/op allocs/op
BEFORE 781.8 21.00
AFTER 167.0 2.00

21 → 2 allocs (the whole Value DOM removed), 4.68× wall for a 3-entry column. Gate: AFTER allocs/op strictly lower.

Key-order note (behaviour-preserving, verified). serde_json::to_value backs the object with a sorted Map, so the BEFORE path emitted keys alphabetically (email,is_primary,type) while direct serialize keeps struct order (email,type,is_primary). This is not an observable change: Postgres normalizes JSONB key order on store, so both inputs land as the identical stored value — confirmed via psql ('{…alpha…}'::jsonb = '{…struct…}'::jsonb → t, both normalizing to {"type":…,"email":…,"is_primary":…}) — and the read path decodes by field name (ROUND23 §J1's Json<Vec<Dto>>), so the round-tripped Contact is identical. The contact etag is computed from the domain entity before the write, not from the stored JSONB, so it is unaffected. The benchmark's equivalence gate asserts the two serializations decode back to the same DTOs.


Not shipped — carried forward

  • format_oc_id_into for the REPORT + trashbin loops. The four REPORT emit loops (report_handler) share the identical per-row-String shape and would take the same buffer treatment; the trashbin per-item writer (write_trash_item_response) would need the buffer threaded through its signature. Lower traffic than PROPFIND; deferred to keep this round's diff PROPFIND-local.
  • S3 read zero-copy forward — needs a MinIO/stub ByteStream fixture.
  • Frontend folder-listing cache — the dead getCachedFolder/cacheFolder SWR cache. A pure-frontend revival only saves latency (instant paint on revisit) because the /api/folders/{id}/resources feed carries no ETag, so the background revalidate still refetches the full body; the bandwidth win needs a backend /resources ETag + conditional 304, plus SWR wiring that respects the route's cursor pagination. A dedicated backend+frontend pass.

Environment / methodology

  • Counting global allocator (examples/bench_round27_micro.rs), no Postgres. 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 (H1: identical oc:id bytes; P2: identical serialized JSONB) and a GATE FAIL … rollback exit(1) if AFTER doesn't allocate fewer than BEFORE.
  • Built with RUSTFLAGS="-C target-cpu=x86-64-v3" (the checked-in .cargo/config.toml pins target-cpu=native, which SIGILLs on this host).
  • Verified beyond the bench: cargo fmt --all --check clean, cargo clippy --features bench -- -D warnings clean, cargo test --lib --features bench green.