perf: round 6 backend — CardDAV cursor streaming, borrowed NC id chain, binary UUID decode, one-alloc hex
Benchmark-gated (equivalence + BEFORE/AFTER in examples/bench_*, results and reproduce commands in benches/ROUND6.md): - CardDAV whole-book REPORT + depth-1 PROPFIND stream through a PG cursor (stream_contacts_by_book, 500-contact pages) instead of materialising every vCard twice: 8 000 contacts TTFB 37.4 → 7.6 ms (4.9x), peak heap 19.0 → 7.0 MiB (2.7x), wall -23%; REPORT and PROPFIND byte-identical to the buffered writers. - NC numeric-id chain fully borrowed: get_or_create_file_ids/folder_ids take &[&str] and return HashMap<Uuid, i64>; batch_resolve_ids callers (PROPFIND pages, REPORT, trashbin, OCS search) pass id slices and look up via nc_id_of. 2.006 → 0.006 allocs/child (334x), 1.53x wall per 500-child page. batch_check_favorites binds &[&str] as text[]. - file_blob_read_repository listing SELECTs drop id::text/folder_id::text server casts: rows decode binary Uuid (16 vs 36 bytes on the wire) and render once in row_to_file. A/B on 500-row pages: 1.225 → 1.044 ms mean (1.17x), p95 1.686 → 1.345 (bench_uuid_text_cast; single-row, param and min() sites left as-is deliberately). - IncrementalHasher::finalize_hex renders through common::fmt::hex_lower instead of one format! per digest byte: 18 → 1 (md5) / 35 → 1 (sha256) allocs per chunk finalize, 14-15x wall. - Share landing overlaps the access-count UPDATE with the unlock fetch via tokio::join! (one round-trip off every public link hit). - REJECTED by benchmark and reverted: try_join_all fan-out of the batch-favorites authz pre-check — 42.6 → 56.4 ms cold, 0.15 → 0.23 ms warm against local-socket PG (bench_favorites_authz kept as evidence). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017aJu9ghvuT8WqC31ZEGTBA
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
//! Micro-alloc benchmark: digest-hex rendering and NC id-batch marshalling.
|
||||
//!
|
||||
//! Two round-6 changes, both equivalence-gated against their verbatim
|
||||
//! BEFORE shapes and measured with a counting allocator:
|
||||
//!
|
||||
//! 1. `IncrementalHasher::finalize_hex` (upload_ingest.rs) rendered MD5 /
|
||||
//! SHA-256 digests with `.map(|b| format!("{b:02x}")).collect()` — one
|
||||
//! heap `String` per digest byte (16 / 32 allocs) per chunk finalize.
|
||||
//! AFTER: `common::fmt::hex_lower` writes into one preallocated String.
|
||||
//!
|
||||
//! 2. `batch_resolve_ids` (NC webdav_handler) cloned every child id into a
|
||||
//! `Vec<String>` and the id service keyed its result map by `String` —
|
||||
//! ~3 heap allocs per child per page. AFTER the whole chain is borrowed:
|
||||
//! `Vec<&str>` in, `HashMap<Uuid, i64>` out, `Uuid::parse_str` lookups.
|
||||
//!
|
||||
//! Run:
|
||||
//! cargo run --release --features bench --example bench_hex_ids
|
||||
//! Tunables (env): BENCH_ITERS (10000), BENCH_CHILDREN (500).
|
||||
|
||||
use std::alloc::{GlobalAlloc, Layout, System};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Instant;
|
||||
|
||||
use md5::Digest;
|
||||
use oxicloud::common::fmt::hex_lower;
|
||||
use uuid::Uuid;
|
||||
|
||||
static ALLOC_CALLS: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
struct CountingAlloc;
|
||||
|
||||
unsafe impl GlobalAlloc for CountingAlloc {
|
||||
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||
ALLOC_CALLS.fetch_add(1, Ordering::Relaxed);
|
||||
unsafe { System.alloc(layout) }
|
||||
}
|
||||
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
|
||||
unsafe { System.dealloc(ptr, layout) }
|
||||
}
|
||||
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
|
||||
ALLOC_CALLS.fetch_add(1, Ordering::Relaxed);
|
||||
unsafe { System.realloc(ptr, layout, new_size) }
|
||||
}
|
||||
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
|
||||
ALLOC_CALLS.fetch_add(1, Ordering::Relaxed);
|
||||
unsafe { System.alloc_zeroed(layout) }
|
||||
}
|
||||
}
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: CountingAlloc = CountingAlloc;
|
||||
|
||||
fn env_or<T: std::str::FromStr>(key: &str, default: T) -> T {
|
||||
env::var(key)
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn measure<R>(f: impl FnOnce() -> R) -> (R, u64, f64) {
|
||||
let a0 = ALLOC_CALLS.load(Ordering::Relaxed);
|
||||
let t = Instant::now();
|
||||
let r = f();
|
||||
let el = t.elapsed().as_secs_f64();
|
||||
let allocs = ALLOC_CALLS.load(Ordering::Relaxed) - a0;
|
||||
(r, allocs, el)
|
||||
}
|
||||
|
||||
// ── 1. digest hex ───────────────────────────────────────────────────────────
|
||||
|
||||
/// BEFORE, verbatim: one `format!` per digest byte.
|
||||
fn hex_before(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
fn bench_hex(iters: usize) {
|
||||
// Deterministic digests of both production sizes (MD5=16, SHA-256=32).
|
||||
let md5s: Vec<[u8; 16]> = (0..64u64)
|
||||
.map(|i| md5::Md5::digest(i.to_le_bytes()).into())
|
||||
.collect();
|
||||
let sha256s: Vec<[u8; 32]> = (0..64u64)
|
||||
.map(|i| sha2::Sha256::digest(i.to_le_bytes()).into())
|
||||
.collect();
|
||||
|
||||
// Equivalence gate: byte-identical output on every digest.
|
||||
for d in &md5s {
|
||||
assert_eq!(hex_lower(d), hex_before(d), "md5 hex mismatch");
|
||||
}
|
||||
for d in &sha256s {
|
||||
assert_eq!(hex_lower(d), hex_before(d), "sha256 hex mismatch");
|
||||
}
|
||||
|
||||
println!("── finalize_hex: per-byte format! vs hex_lower ({iters} finalizes/arm) ──\n");
|
||||
println!(
|
||||
"| {:<8} | {:<8} | {:>12} | {:>10} | {:>12} |",
|
||||
"digest", "arm", "allocs", "wall ms", "allocs/call"
|
||||
);
|
||||
for (label, digests) in [("md5", md5s.len()), ("sha256", sha256s.len())] {
|
||||
for arm in ["BEFORE", "AFTER"] {
|
||||
let (sink, allocs, secs) = measure(|| {
|
||||
let mut sink = 0usize;
|
||||
for i in 0..iters {
|
||||
let s = match (label, arm) {
|
||||
("md5", "BEFORE") => hex_before(&md5s[i % digests]),
|
||||
("md5", "AFTER") => hex_lower(&md5s[i % digests]),
|
||||
("sha256", "BEFORE") => hex_before(&sha256s[i % digests]),
|
||||
_ => hex_lower(&sha256s[i % digests]),
|
||||
};
|
||||
sink += s.len();
|
||||
}
|
||||
sink
|
||||
});
|
||||
std::hint::black_box(sink);
|
||||
println!(
|
||||
"| {:<8} | {:<8} | {:>12} | {:>10.2} | {:>12.2} |",
|
||||
label,
|
||||
arm,
|
||||
allocs,
|
||||
secs * 1e3,
|
||||
allocs as f64 / iters as f64
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2. NC id-batch marshalling ──────────────────────────────────────────────
|
||||
|
||||
/// BEFORE, verbatim caller+service marshalling: clone ids into `Vec<String>`,
|
||||
/// key the result map by cloned `String`, look children up by `&String`.
|
||||
fn ids_before(child_ids: &[String], nc: &HashMap<Uuid, i64>) -> Vec<Option<i64>> {
|
||||
let file_uuids: Vec<String> = child_ids.to_vec();
|
||||
let mut map: HashMap<String, i64> = HashMap::with_capacity(file_uuids.len());
|
||||
for raw in &file_uuids {
|
||||
let Ok(uuid) = Uuid::parse_str(raw) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(id) = nc.get(&uuid) {
|
||||
map.insert(raw.clone(), *id);
|
||||
}
|
||||
}
|
||||
child_ids.iter().map(|id| map.get(id).copied()).collect()
|
||||
}
|
||||
|
||||
/// AFTER: borrowed slice in, `Uuid`-keyed map out, parse-and-get lookups —
|
||||
/// the exact shapes now in `batch_resolve_ids` + `nc_id_of`.
|
||||
fn ids_after(child_ids: &[String], nc: &HashMap<Uuid, i64>) -> Vec<Option<i64>> {
|
||||
let file_uuids: Vec<&str> = child_ids.iter().map(String::as_str).collect();
|
||||
let mut map: HashMap<Uuid, i64> = HashMap::with_capacity(file_uuids.len());
|
||||
for raw in &file_uuids {
|
||||
let Ok(uuid) = Uuid::parse_str(raw) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(id) = nc.get(&uuid) {
|
||||
map.insert(uuid, *id);
|
||||
}
|
||||
}
|
||||
child_ids
|
||||
.iter()
|
||||
.map(|id| Uuid::parse_str(id).ok().and_then(|u| map.get(&u).copied()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn bench_ids(pages: usize, children: usize) {
|
||||
// A PROPFIND page of `children` DTO ids (36-byte uuid strings) resolved
|
||||
// against the id service's numeric mapping.
|
||||
let uuids: Vec<Uuid> = (0..children).map(|_| Uuid::new_v4()).collect();
|
||||
let child_ids: Vec<String> = uuids.iter().map(|u| u.to_string()).collect();
|
||||
let nc: HashMap<Uuid, i64> = uuids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, u)| (*u, i as i64 + 1000))
|
||||
.collect();
|
||||
|
||||
// Equivalence gate: identical per-child resolution, including an
|
||||
// unparseable id and an unmapped-but-valid id.
|
||||
let mut gate_ids = child_ids.clone();
|
||||
gate_ids.push("not-a-uuid".to_string());
|
||||
gate_ids.push(Uuid::new_v4().to_string());
|
||||
assert_eq!(
|
||||
ids_before(&gate_ids, &nc),
|
||||
ids_after(&gate_ids, &nc),
|
||||
"id resolution mismatch"
|
||||
);
|
||||
|
||||
println!("\n── batch_resolve_ids marshalling: String-keyed vs borrowed+Uuid ──");
|
||||
println!(" ({pages} pages × {children} children/arm)\n");
|
||||
println!(
|
||||
"| {:<8} | {:>12} | {:>10} | {:>14} |",
|
||||
"arm", "allocs", "wall ms", "allocs/child"
|
||||
);
|
||||
for arm in ["BEFORE", "AFTER"] {
|
||||
let (sink, allocs, secs) = measure(|| {
|
||||
let mut sink = 0usize;
|
||||
for _ in 0..pages {
|
||||
let resolved = if arm == "BEFORE" {
|
||||
ids_before(&child_ids, &nc)
|
||||
} else {
|
||||
ids_after(&child_ids, &nc)
|
||||
};
|
||||
sink += resolved.iter().flatten().count();
|
||||
}
|
||||
sink
|
||||
});
|
||||
assert_eq!(sink, pages * children, "all children must resolve");
|
||||
println!(
|
||||
"| {:<8} | {:>12} | {:>10.2} | {:>14.3} |",
|
||||
arm,
|
||||
allocs,
|
||||
secs * 1e3,
|
||||
allocs as f64 / (pages * children) as f64
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let iters: usize = env_or("BENCH_ITERS", 10_000);
|
||||
let children: usize = env_or("BENCH_CHILDREN", 500);
|
||||
|
||||
bench_hex(iters);
|
||||
bench_ids(iters / 10, children);
|
||||
|
||||
println!("\n(BEFORE arms are verbatim replicas of the replaced shapes; equivalence");
|
||||
println!(" asserted before timing. Allocs counted via a wrapping GlobalAlloc.)");
|
||||
}
|
||||
Reference in New Issue
Block a user