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
This commit is contained in:
Claude
2026-07-16 16:50:07 +00:00
parent aba89c4f5d
commit 82ee7da0d2
18 changed files with 1245 additions and 195 deletions
@@ -607,6 +607,12 @@ impl DeltaUploadService {
Ok(DeltaDownloadOutcome::Ready(ordered))
}
/// Backend-recommended read-ahead depth for multi-chunk drains
/// (see `DedupService::read_prefetch`).
pub fn read_prefetch(&self) -> usize {
self.dedup.read_prefetch()
}
/// Stream one authorized chunk's bytes (entitlement was established by
/// [`authorize_chunk_download_with_perms`]).
pub async fn chunk_stream(
@@ -5,7 +5,9 @@ use std::sync::Arc;
use crate::application::dtos::file_dto::FileDto;
use crate::application::ports::authorization_ports::AuthorizationEngine;
use crate::application::ports::file_ports::{FileRetrievalUseCase, OptimizedFileContent};
use crate::application::ports::file_ports::{
FileRetrievalUseCase, OptimizedFileContent, RangeContent,
};
use crate::application::ports::resource_access_hook::ResourceAccessHook;
use crate::application::ports::storage_ports::FileReadPort;
use crate::common::errors::DomainError;
@@ -284,6 +286,69 @@ impl FileRetrievalService {
let files = self.file_read.get_files_by_ids(ids).await?;
Ok(files.into_iter().map(FileDto::from).collect())
}
/// Range read that first consults the RAM content cache (see
/// [`Self::get_file_range_preloaded`]).
pub async fn get_file_range_preloaded_with_perms(
&self,
dto: &FileDto,
caller_id: Uuid,
start: u64,
end: Option<u64>,
) -> Result<RangeContent, DomainError> {
self.require_file(&dto.id, Permission::Read, caller_id)
.await?;
// Same throttled Recent recording as the streaming variant.
self.notify_file_accessed(caller_id, &dto.id);
self.get_file_range_preloaded(dto, start, end).await
}
/// Range read for HTTP Range Requests, cache-aware.
///
/// Media players and PDF viewers fetch these files *exclusively* through
/// Range requests (a `bytes=0-` probe, then seeks) — the plain streaming
/// path paid 1 PG round-trip (blob-hash resolve) + a chunk open/seek for
/// EVERY seek, even when the whole blob was already sitting in the moka
/// content cache as one contiguous `Bytes`. For sub-`CACHE_THRESHOLD`
/// files this now answers from the cache: `Bytes::slice` is a refcount
/// bump — zero copy, zero I/O, zero PG (benches/RANGE-CACHE.md). A miss
/// populates the cache via the same single-flight `get_or_load` Tier 1
/// uses, so one probe warms every subsequent seek. `end` is exclusive
/// (callers pass `Some(last_byte + 1)`), matching the streaming variant.
pub async fn get_file_range_preloaded(
&self,
dto: &FileDto,
start: u64,
end: Option<u64>,
) -> Result<RangeContent, DomainError> {
let cacheable = dto.size < CACHE_THRESHOLD && !dto.content_hash.is_empty();
if cacheable && let Some(cache) = &self.content_cache {
let etag: Arc<str> = format!("\"{}\"", dto.content_hash).into();
let ct: Arc<str> = dto.mime_type.clone();
let file_read = Arc::clone(&self.file_read);
let id_owned = dto.id.clone();
let cap = dto.size as usize;
let (bytes, _etag, _ct) = cache
.get_or_load(dto.content_hash.to_string(), etag, ct, async move {
debug!("💾 Range cache MISS: {} – loading from disk", id_owned);
Self::read_full(&file_read, &id_owned, cap).await
})
.await?;
let len = bytes.len() as u64;
let s = start.min(len) as usize;
let e = end.unwrap_or(len).min(len) as usize;
if s <= e {
return Ok(RangeContent::Bytes(bytes.slice(s..e)));
}
// Degenerate range the validator should have rejected — fall
// through to the streaming path rather than panic on slice.
}
let stream = self
.file_read
.get_file_range_stream(&dto.id, start, end)
.await?;
Ok(RangeContent::Stream(stream))
}
}
impl FileRetrievalUseCase for FileRetrievalService {