Files
Oxicloud/benches/ROUND2.md
T
Claude 82ee7da0d2 perf: serve ranges from RAM cache, stream ZIPs, overlap ingest settle, O(1) chunk gate
Round 2 of benchmark-gated optimizations (benches/ROUND2.md; every change
gated by a before/after in examples/bench_round2.rs — an AFTER that did
not beat its BEFORE was to be rolled back; none needed it):

- Range requests (REST/DAV/shares) answered from the moka content cache
  for sub-10MB files: PG resolve + open/seek/read -> Bytes::slice.
  256KiB seeks: 1,730/s -> 3.7M/s (p50 552us -> 0.15us).
- Streaming folder/share ZIPs via tokio duplex: TTFB no longer scales
  with archive size (326ms -> 0.4ms on 192MiB corpus; total also faster).
  Content-Length dropped (size unknown up front).
- NC chunked-upload per-PUT gate: O(k) directory scan+stat -> in-RAM
  per-session counter (lazy rebuild on cold start). 1,000-chunk upload
  gate cost: 33.1s -> 0.09s cumulative.
- Delta download + commit-verify now use the CDC path's
  buffered(read_prefetch) read-ahead: 64-chunk drain at 5ms open
  latency 440ms -> 51ms; order preserved.
- CDC ingest settles batches on a spawned task (depth-1 pipeline) so
  the source stream keeps flowing during PG pin + backend writes;
  rollback ledger shared + lock-serialized so compensation stays exact
  on cancellation. 512MiB paced ingest: 60-69 -> 74-75 MB/s.
  OXICLOUD_INGEST_OVERLAP=0 restores inline settling (ops/bench hatch).
- Frontend: instant-upload BLAKE3 hashing moved off the main thread to
  a bounded Web Worker pool (File handles by reference); vitest gate
  asserts the pool beats sequential (first gate draft posting buffers
  was 2.6x slower and was rewritten — copies dominated).

Validation: cargo fmt + clippy -D warnings clean; 514 unit + 544
integration tests green; 270 frontend tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBK1RdtzyP6759Muqe1K1w
2026-07-16 16:50:07 +00:00

108 lines
5.5 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 2 — read path, upload path, archives (before/after gates)
Five backend changes + one frontend change, each gated by a before/after
benchmark (`examples/bench_round2.rs`; frontend gate in
`frontend/src/lib/api/endpoints/deltaUpload.hash.test.ts`). Rule of the
round: an AFTER that doesn't beat its BEFORE gets rolled back — none did.
Reproduce:
```bash
BENCH_SECTIONS=1,2,3,5 cargo run --release --features bench --example bench_round2
OXICLOUD_INGEST_OVERLAP=0 BENCH_SECTIONS=4 cargo run --release --features bench --example bench_round2
OXICLOUD_INGEST_OVERLAP=1 BENCH_SECTIONS=4 cargo run --release --features bench --example bench_round2
cd frontend && npx vitest run src/lib/api/endpoints/deltaUpload.hash.test.ts
```
## [1] Range requests served from the content cache — 2,156×
Media players and PDF viewers fetch files *exclusively* via Range requests
(a `bytes=0-` probe, then seeks). All three range paths (REST, DAV helper,
public shares) went straight to `get_file_range_stream`: a PG blob-hash
resolve + chunk open/seek/read per seek — even when the whole sub-10 MB blob
sat in the moka content cache as contiguous `Bytes`.
`FileRetrievalService::get_file_range_preloaded` now answers from the cache
(`Bytes::slice` = refcount bump; a miss populates it via the same
single-flight loader Tier 1 uses, so one probe warms every later seek).
| per 256 KiB seek (6 MiB file) | seeks/s | p50 µs | p99 µs |
|-------------------------------|--------:|-------:|-------:|
| BEFORE — PG + open/seek/read | 1,730 | 552.5 | 818.8 |
| AFTER — cache hit + slice | 3,730,560 | 0.15 | 2.85 |
## [2] NC chunked-upload gate: O(N²) directory scan → O(1) counter — 357×
`handle_put_chunk` recomputed "session bytes so far" on EVERY chunk PUT by
listing the session directory and stat-ing every existing chunk — chunk k
scans k files; a 1,000-chunk (10 GB) upload does ~500k stats.
`NextcloudChunkedUploadService` now keeps an in-RAM per-session counter
(seeded on MKCOL, bumped per accepted chunk, dropped on cleanup/overwrite,
lazily rebuilt from the listing on cold start — crash semantics unchanged).
Cumulative gate cost across a 1,000-chunk upload: **33,063 ms → 93 ms**.
## [3] Delta download / commit-verify read-ahead — 8.7× (latency-bound)
`delta_download_chunks` and `hash_chunk_sequence` drained chunks strictly
sequentially — every chunk-open's round-trip paid serially — while the main
CDC download path already overlaps opens with `buffered(read_prefetch)`.
Both now use the same combinator (order preserved — `buffered` yields in
input order).
64-chunk drain with 5 ms per-open latency (object-store model):
**440 ms → 51 ms**. On local disk the same combinator measured +7–12 %
(benches/BLOB-PREFETCH.md).
## [4] CDC ingest: settle overlapped with reading — +7–25 %
`ingest_chunks_from_stream` awaited each batch settle (PG pin round-trip +
up to 8 MiB of backend writes) INLINE — the HTTP source was not polled at
all during the settle, so read and settle phases alternated instead of
overlapping. The settle now runs on a spawned task (depth-1 pipeline) that
records into the guard's shared, lock-serialized state — rollback stays
exact even if the request future is dropped mid-settle.
`OXICLOUD_INGEST_OVERLAP=0` restores the inline behaviour (the bench's
BEFORE side, and an ops escape hatch).
512 MiB unique-content ingest, source paced at 300 MB/s, two reps:
**60 / 69 MB/s (inline) → 75 / 74 MB/s (overlapped)**.
## [5] Streaming ZIP: constant time-to-first-byte — 779× on this corpus
`create_folder_zip` built the ENTIRE archive into a temp file before the
handler sent byte one — TTFB grew with folder size (a multi-GB folder =
minutes of "waiting for server"). `create_folder_zip_stream` plans inline
(planning errors still surface as proper HTTP errors), then writes the
archive on a spawned task through `tokio::io::duplex`, streaming bytes as
they are produced. Folder downloads and public-share ZIPs both use it; a
mid-archive blob error truncates the stream (no central directory → clients
detect corruption) — the standard streamed-ZIP tradeoff. Content-Length is
no longer sent (size unknown up front).
48 × 4 MiB media corpus: TTFB **326.1 ms → 0.4 ms**; total wall also
improved (484 ms → 55 ms — no disk round-trip through the temp file).
TTFB in BEFORE scales linearly with archive size; AFTER is constant.
## [6] Frontend: instant-upload hashing on a worker pool
`resolveOwnedHashes` hashed every small file of a drop sequentially on the
MAIN THREAD (synchronous WASM BLAKE3 per file) before any upload lane
started — seconds of UI jank on large drops. Hashing now fans out over a
bounded pool of dedicated Web Workers (`static/workers/hashWorker.js`,
`File` handles passed by reference, reads happen inside the worker), with
the old inline loop kept as fallback where `Worker` is unavailable.
Architecture gate (node worker_threads, read+hash 24 × 4 MiB, file
references — faithful to the browser shape): 3-lane pool beats the
sequential loop; asserted by `deltaUpload.hash.test.ts` so a regression
fails CI. First model of this gate (posting BUFFERS instead of file
references) was 2.6× SLOWER — structured-clone copies dominated — and was
rewritten; kept here as a reminder that the gate must model the real
data-flow.
## Skipped this round
- **Swimlane (group-by) view virtualization** — needs interactive browser
measurement (frame times while scrolling) that this environment can't
produce; deferred rather than shipped unverified.