Files
Oxicloud/examples/bench_round22_micro.rs
T
Claude 992bdae898 perf: round 22 — hot-GET HeaderMap borrow, native-WebDAV/CalDAV etag borrowed quotes, FileDto content_hash move, CalendarEvent stamp, ShareItemType case-fold
Benchmark-gated, same rule as ROUND2-21: every change ships with a
BEFORE/AFTER counting-allocator benchmark and a byte/-value equivalence
gate; an AFTER that fails to reduce allocations exits non-zero (rollback).
See benches/ROUND22.md and examples/bench_round22_micro.rs. All arms
no-Postgres.

- H1: the hot GET handlers (get_thumbnail, download_file, list_files_query,
  list_photos, NextCloud preview, public-share download/access) take
  `req: Request` last and read `req.headers()` by borrow instead of axum's
  HeaderMap extractor, whose FromRequestParts impl clones the whole request
  header table just to read 1-3 headers (the ROUND14 §A4 middleware pattern,
  finally propagated to the handlers). 2 -> 0 allocs/req · 9.95x wall.
- W1: native WebDAV write_etag_quoted — the etag emitter for every /webdav/
  PROPFIND row (per file AND per folder, up to 500/page) — emits the quotes
  as borrowed pre-escaped " text events instead of escaping a "{etag}"
  String (the ROUND20 §C1 / ROUND21 §R4 pattern). 3 -> 0 allocs/row.
- C1: CalDAV getetag routed through a shared write_quoted_etag helper across
  all 5 sites (3 per-event + 2 per-calendar); the now-dead etag: &mut String
  buffer threaded through write_event_response/standard/requested props + the
  two per-page buffers removed. 2 -> 0 allocs/row.
- D1: FileDto::from reuses the moved parts.blob_hash instead of cloning it
  via the content_hash() getter (the ROUND19/20 move-not-clone sweep missed
  it — hash/etag are read before into_parts()). Per file row of every
  listing. 1 -> 0 allocs/row.
- E1: CalendarEvent::update_time_range/update_all_day stamp timed
  DTSTART/DTEND via fmt::compact_ical_utc stack render (chrono fallback out
  of range) instead of the %Y%m%dT%H%M%SZ strftime interpreter. 4 -> 0.
- S1: ShareItemType::try_from uses eq_ignore_ascii_case instead of a
  throwaway to_lowercase() String. 1 -> 0 allocs/parse.

Verified: cargo clippy --features bench --all-targets -D warnings clean,
cargo fmt --all --check clean, cargo test --lib --features bench = 529
passed / 0 failed (incl. the OpenAPI-spec-validity test guarding the H1
utoipa-handler signature change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKyQ4AnYtgp1JtjzweyMeo
2026-07-20 13:48:47 +00:00

522 lines
21 KiB
Rust
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-22 CPU/alloc micro-pack (no Postgres).
//!
//! Same rule as ROUND2–21: each section is BEFORE (verbatim replica of the
//! shipped-before shape) vs AFTER (verbatim replica of the shipped-after shape,
//! which the source is then made to match), with a byte/-value equivalence gate
//! and a `GATE FAIL … rollback` check that `std::process::exit(1)`s if the AFTER
//! arm fails to beat its BEFORE — the round's roll-back rule encoded into the
//! benchmark. An AFTER that doesn't win is never applied to the source.
//!
//! [H1] Hot GET handlers (`get_thumbnail`, `download_file`, `list_files`,
//! `list_photos`, NC `preview`, public-share download/access) took the
//! axum `HeaderMap` extractor, which does `parts.headers.clone()` — an
//! owned clone of the whole request header table (~2 allocs) — just to
//! read 1–3 headers. AFTER takes `req: Request` and reads `req.headers()`
//! by borrow (the ROUND14 §A4 middleware pattern applied to the handlers).
//!
//! [W1] The native WebDAV `write_etag_quoted` (per file AND per folder of
//! every `/webdav/` PROPFIND row, up to 500/page — the most-travelled
//! DAV path) built a `"{etag}"` String then wrote it auto-escaped;
//! `quick_xml` escapes the `"` → `&quot;`, re-allocating an owned `Cow`,
//! so 2 allocs/row (buffer + escape). AFTER emits the quotes as borrowed
//! pre-escaped `&quot;` events (the ROUND20 §C1 / ROUND21 §R4 pattern).
//!
//! [C1] The CalDAV `getetag` emit (per event of every calendar REPORT/multiget,
//! per calendar of the home-set PROPFIND) still escaped a `"…"` value
//! (reused buffer → 1 alloc/event escape; `format!` calendar sites → 2).
//! AFTER routes all five sites through a `write_quoted_etag` helper
//! (borrowed pre-escaped quotes) — 0 allocs.
//!
//! [D1] `FileDto::from` cloned `content_hash` via `file.content_hash()
//! .to_string()` and then `into_parts()` MOVED the same `blob_hash` into
//! `parts.blob_hash`, which was dropped unused — 1 wasted alloc on every
//! listing row. AFTER reuses `parts.blob_hash` (0).
//!
//! [E1] `CalendarEvent::update_time_range` / `update_all_day` stamped timed
//! DTSTART/DTEND via `format!("{}", t.format("%Y%m%dT%H%M%SZ"))` — chrono's
//! strftime interpreter (~3 allocs). AFTER stack-renders via the shipped
//! `fmt::compact_ical_utc` and passes the `&str` straight to
//! `update_ical_property` (0 allocs), chrono fallback out of range.
//!
//! [S1] `ShareItemType::try_from` matched `s.to_lowercase().as_str()` — a
//! throwaway Unicode-lowercased String — against two ASCII literals.
//! AFTER uses `eq_ignore_ascii_case` (byte-identical acceptance, 0 allocs).
//!
//! Run:
//! cargo run --release --features bench --example bench_round22_micro
//! Tunables (env): BENCH_ITERS (200000)
use std::alloc::{GlobalAlloc, Layout, System};
use std::env;
use std::hint::black_box;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use axum::http::{HeaderMap, HeaderName, HeaderValue, header};
use chrono::{TimeZone, Utc};
use quick_xml::Writer;
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
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)
}
struct Measured {
wall_ns_per_op: f64,
allocs_per_op: f64,
}
fn measure<F: FnMut()>(iters: usize, mut f: F) -> Measured {
// Warm up (grow any reused buffers, prime caches) so the measured window
// reflects steady state, not first-touch growth.
for _ in 0..(iters / 20).max(1) {
f();
}
let a0 = ALLOC_CALLS.load(Ordering::Relaxed);
let t = Instant::now();
for _ in 0..iters {
f();
}
let wall = t.elapsed().as_nanos() as f64 / iters as f64;
let allocs = (ALLOC_CALLS.load(Ordering::Relaxed) - a0) as f64 / iters as f64;
Measured {
wall_ns_per_op: wall,
allocs_per_op: allocs,
}
}
fn print_row(label: &str, m: &Measured) {
println!(
"| {:<52} | {:>12.1} | {:>10.2} |",
label, m.wall_ns_per_op, m.allocs_per_op
);
}
fn header_footer(name: &str, before: &Measured, after: &Measured) {
println!("| arm | ns/op | allocs/op |");
print_row(&format!("BEFORE {name}"), before);
print_row(&format!("AFTER {name}"), after);
println!(
"# {:.2}x wall, {:.2} fewer allocs/op",
before.wall_ns_per_op / after.wall_ns_per_op,
before.allocs_per_op - after.allocs_per_op
);
}
fn gate_allocs(tag: &str, before: &Measured, after: &Measured) {
if after.allocs_per_op >= before.allocs_per_op {
eprintln!("GATE FAIL [{tag}]: AFTER did not reduce allocations — rollback");
std::process::exit(1);
}
}
// ────────────────────────────────────────────────────────────────────────────
// [H1] Hot GET handler HeaderMap extractor — axum `HeaderMap` (clones the whole
// request header table via `parts.headers.clone()`) vs `Request` + borrow.
// ────────────────────────────────────────────────────────────────────────────
/// A realistic browser GET request header set (what a thumbnail / photo-list /
/// download request actually carries). The `HeaderMap` extractor clones ALL of
/// it just so the handler can read 1–3 headers (IF_NONE_MATCH / ACCEPT / RANGE).
fn realistic_request_headers() -> HeaderMap {
let mut h = HeaderMap::new();
h.insert(header::HOST, HeaderValue::from_static("cloud.example.com"));
h.insert(
header::USER_AGENT,
HeaderValue::from_static(
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/126.0 Safari/537.36",
),
);
h.insert(
header::ACCEPT,
HeaderValue::from_static("image/avif,image/webp,image/apng,image/*,*/*;q=0.8"),
);
h.insert(
header::ACCEPT_ENCODING,
HeaderValue::from_static("gzip, deflate, br, zstd"),
);
h.insert(
header::ACCEPT_LANGUAGE,
HeaderValue::from_static("en-US,en;q=0.9,es;q=0.8"),
);
h.insert(
header::REFERER,
HeaderValue::from_static("https://cloud.example.com/photos"),
);
h.insert(header::CONNECTION, HeaderValue::from_static("keep-alive"));
h.insert(
HeaderName::from_static("sec-fetch-dest"),
HeaderValue::from_static("image"),
);
h.insert(
HeaderName::from_static("sec-fetch-mode"),
HeaderValue::from_static("no-cors"),
);
h.insert(
HeaderName::from_static("sec-fetch-site"),
HeaderValue::from_static("same-origin"),
);
h.insert(
header::COOKIE,
HeaderValue::from_static(
"oxicloud_session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.payload.signature; csrf=abc123",
),
);
h.insert(
header::IF_NONE_MATCH,
HeaderValue::from_static("\"thumb-6b1e9f00-preview-webp\""),
);
h
}
fn section_h1() {
let iters: usize = env_or("BENCH_ITERS", 200_000);
let headers = realistic_request_headers();
// Equivalence: both arms read the identical IF_NONE_MATCH value.
let cloned = headers.clone();
let borrowed = headers
.get(header::IF_NONE_MATCH)
.and_then(|v| v.to_str().ok());
assert_eq!(
cloned
.get(header::IF_NONE_MATCH)
.and_then(|v| v.to_str().ok()),
borrowed,
"H1 read value differs"
);
// BEFORE: axum's `HeaderMap` extractor materializes an owned clone of the
// whole request header table (`parts.headers.clone()`), then the handler
// reads one header out of it.
let before = measure(iters, || {
let owned = black_box(&headers).clone();
black_box(
owned
.get(header::IF_NONE_MATCH)
.and_then(|v| v.to_str().ok()),
);
});
// AFTER: take `req: Request` and read `req.headers()` by borrow — the header
// table is never cloned; the same value is read straight from the borrow.
let after = measure(iters, || {
let borrow: &HeaderMap = black_box(&headers);
black_box(
borrow
.get(header::IF_NONE_MATCH)
.and_then(|v| v.to_str().ok()),
);
});
println!(
"\n## [H1] Hot GET handler HeaderMap clone (per thumbnail/photo/download/preview/share req)"
);
header_footer("HeaderMap::clone() vs &HeaderMap borrow", &before, &after);
gate_allocs("H1", &before, &after);
}
// ────────────────────────────────────────────────────────────────────────────
// [W1] Native WebDAV getetag — sized String + escape vs borrowed pre-escaped.
// ────────────────────────────────────────────────────────────────────────────
/// BEFORE (verbatim `webdav_adapter::write_etag_quoted`): sized `"{etag}"`
/// String then auto-escaped write — `quick_xml` escapes the `"` → `&quot;`,
/// re-allocating an owned `Cow`. 2 allocs/row (buffer + escape).
fn w1_before(buf: &mut Vec<u8>, etag: &str) {
let mut w = Writer::new(&mut *buf);
w.write_event(Event::Start(BytesStart::new("D:getetag")))
.unwrap();
let mut quoted = String::with_capacity(etag.len() + 2);
quoted.push('"');
quoted.push_str(etag);
quoted.push('"');
w.write_event(Event::Text(BytesText::new(&quoted))).unwrap();
w.write_event(Event::End(BytesEnd::new("D:getetag")))
.unwrap();
}
/// AFTER: borrowed pre-escaped `&quot;` quotes around the escaped body.
fn w1_after(buf: &mut Vec<u8>, etag: &str) {
let mut w = Writer::new(&mut *buf);
w.write_event(Event::Start(BytesStart::new("D:getetag")))
.unwrap();
w.write_event(Event::Text(BytesText::from_escaped("&quot;")))
.unwrap();
w.write_event(Event::Text(BytesText::new(etag))).unwrap();
w.write_event(Event::Text(BytesText::from_escaped("&quot;")))
.unwrap();
w.write_event(Event::End(BytesEnd::new("D:getetag")))
.unwrap();
}
fn section_w1() {
let iters: usize = env_or("BENCH_ITERS", 200_000);
let etag = "d41d8cd98f00b204e9800998ecf8427e-1719792000"; // realistic file etag
// Equivalence: byte-identical output, incl. an etag with XML-special chars.
let (mut b1, mut b2) = (Vec::new(), Vec::new());
w1_before(&mut b1, etag);
w1_after(&mut b2, etag);
assert_eq!(b1, b2, "W1 emitted bytes differ (hex etag)");
let (mut s1, mut s2) = (Vec::new(), Vec::new());
w1_before(&mut s1, "a&b<c\"d-42");
w1_after(&mut s2, "a&b<c\"d-42");
assert_eq!(s1, s2, "W1 emitted bytes differ (special chars)");
let mut buf = Vec::with_capacity(96);
let before = measure(iters, || {
buf.clear();
w1_before(black_box(&mut buf), black_box(etag));
});
let after = measure(iters, || {
buf.clear();
w1_after(black_box(&mut buf), black_box(etag));
});
println!("\n## [W1] Native WebDAV getetag (per file+folder PROPFIND row, up to 500/page)");
header_footer("sized String + escape vs borrowed events", &before, &after);
gate_allocs("W1", &before, &after);
}
// ────────────────────────────────────────────────────────────────────────────
// [C1] CalDAV getetag — reused-buffer escape vs borrowed pre-escaped.
// ────────────────────────────────────────────────────────────────────────────
/// BEFORE (verbatim `caldav_adapter` event sites): reused buffer holds `"{id}"`,
/// written auto-escaped — the buffer is amortized, so the only per-event alloc
/// is the escape of the two `"` → owned `Cow`. 1 alloc/event.
fn c1_before(buf: &mut Vec<u8>, etag_buf: &mut String, id: &str) {
let mut w = Writer::new(&mut *buf);
w.write_event(Event::Start(BytesStart::new("D:getetag")))
.unwrap();
etag_buf.clear();
etag_buf.push('"');
etag_buf.push_str(id);
etag_buf.push('"');
w.write_event(Event::Text(BytesText::new(etag_buf.as_str())))
.unwrap();
w.write_event(Event::End(BytesEnd::new("D:getetag")))
.unwrap();
}
/// AFTER (`write_quoted_etag`): borrowed pre-escaped quotes; the UUID body is a
/// borrow (no XML-special chars). 0 allocs/event.
fn c1_after(buf: &mut Vec<u8>, id: &str) {
let mut w = Writer::new(&mut *buf);
w.write_event(Event::Start(BytesStart::new("D:getetag")))
.unwrap();
w.write_event(Event::Text(BytesText::from_escaped("&quot;")))
.unwrap();
w.write_event(Event::Text(BytesText::new(id))).unwrap();
w.write_event(Event::Text(BytesText::from_escaped("&quot;")))
.unwrap();
w.write_event(Event::End(BytesEnd::new("D:getetag")))
.unwrap();
}
fn section_c1() {
let iters: usize = env_or("BENCH_ITERS", 200_000);
let id = "6b1e9f00-4c2a-4f1e-9b7a-2d5e8c1f0a3b"; // calendar / event UUID
// Equivalence: byte-identical output for the UUID body.
let (mut b1, mut eb, mut b2) = (Vec::new(), String::new(), Vec::new());
c1_before(&mut b1, &mut eb, id);
c1_after(&mut b2, id);
assert_eq!(b1, b2, "C1 emitted bytes differ");
let mut buf = Vec::with_capacity(96);
let mut etag_buf = String::with_capacity(40);
let before = measure(iters, || {
buf.clear();
c1_before(black_box(&mut buf), black_box(&mut etag_buf), black_box(id));
});
let after = measure(iters, || {
buf.clear();
c1_after(black_box(&mut buf), black_box(id));
});
println!("\n## [C1] CalDAV getetag (per event of every REPORT/multiget, per calendar)");
header_footer("reused-buffer escape vs borrowed events", &before, &after);
gate_allocs("C1", &before, &after);
}
// ────────────────────────────────────────────────────────────────────────────
// [D1] FileDto::from content_hash — getter clone (moved parts.blob_hash dropped)
// vs reuse the moved String.
// ────────────────────────────────────────────────────────────────────────────
/// The 64-char BLAKE3-hex String a `File` owns in `blob_hash` (allocated in both
/// arms — the baseline; the delta is exactly the `content_hash` clone).
fn make_hash() -> String {
"d41d8cd98f00b204e9800998ecf8427ed41d8cd98f00b204e9800998ecf8427e".to_string()
}
/// BEFORE: `content_hash = file.content_hash().to_string()` clones the hash,
/// then `into_parts()` moves the *same* `blob_hash` into `parts.blob_hash`,
/// which is dropped unused in the `Self { … }` ctor.
fn d1_before(owned_hash: String) -> String {
let content_hash = owned_hash.clone(); // File::content_hash().to_string()
let parts_blob_hash = owned_hash; // into_parts() moves blob_hash
let _ = parts_blob_hash; // dropped unused in the Self{} ctor
content_hash
}
/// AFTER: `content_hash: parts.blob_hash` — reuse the moved String, no clone.
fn d1_after(owned_hash: String) -> String {
owned_hash
}
fn section_d1() {
let iters: usize = env_or("BENCH_ITERS", 200_000);
// Equivalence: identical content_hash string.
assert_eq!(
d1_before(make_hash()),
d1_after(make_hash()),
"D1 content_hash differs"
);
let before = measure(iters, || {
black_box(d1_before(black_box(make_hash())));
});
let after = measure(iters, || {
black_box(d1_after(black_box(make_hash())));
});
println!("\n## [D1] FileDto::from content_hash (per file row of every listing)");
header_footer("getter clone + drop moved vs reuse moved", &before, &after);
gate_allocs("D1", &before, &after);
}
// ────────────────────────────────────────────────────────────────────────────
// [E1] CalendarEvent timed DTSTART/DTEND — chrono strftime vs compact_ical_utc.
// ────────────────────────────────────────────────────────────────────────────
/// BEFORE: `format!("{}", t.format("%Y%m%dT%H%M%SZ"))` — chrono's strftime
/// interpreter builds a `DelayedFormat` and formats six fields through
/// `core::fmt`, heap-allocating.
fn e1_before(dt: chrono::DateTime<Utc>) -> String {
format!("{}", dt.format("%Y%m%dT%H%M%SZ"))
}
fn section_e1() {
let iters: usize = env_or("BENCH_ITERS", 200_000);
let secs: i64 = 1_752_753_434; // 2025-07-17T11:57:14Z
let dt = Utc.timestamp_opt(secs, 0).unwrap();
// Equivalence: the stack render equals the chrono strftime output.
let mut ebuf = [0u8; 16];
let after_str = oxicloud::common::fmt::compact_ical_utc(&mut ebuf, secs).expect("in range");
assert_eq!(e1_before(dt), after_str, "E1 stamp differs");
let before = measure(iters, || {
black_box(e1_before(black_box(dt)));
});
// AFTER: stack render via the shipped helper; the `&str` is passed straight
// to `update_ical_property` in the source — 0 allocs.
let after = measure(iters, || {
let mut buf = [0u8; 16];
black_box(oxicloud::common::fmt::compact_ical_utc(
&mut buf,
black_box(secs),
));
});
println!("\n## [E1] CalendarEvent timed DTSTART/DTEND (per event-edit PUT)");
header_footer("chrono %Y%m%dT%H%M%SZ vs compact_ical_utc", &before, &after);
gate_allocs("E1", &before, &after);
}
// ────────────────────────────────────────────────────────────────────────────
// [S1] ShareItemType::try_from — to_lowercase() String vs eq_ignore_ascii_case.
// ────────────────────────────────────────────────────────────────────────────
/// BEFORE: `s.to_lowercase().as_str()` — a throwaway Unicode-lowercased String
/// (always allocates) — matched against two ASCII literals.
fn s1_before(s: &str) -> u8 {
match s.to_lowercase().as_str() {
"file" => 0,
"folder" => 1,
_ => 2,
}
}
/// AFTER: `eq_ignore_ascii_case` — allocation-free, byte-identical acceptance
/// for the ASCII targets.
fn s1_after(s: &str) -> u8 {
if s.eq_ignore_ascii_case("file") {
0
} else if s.eq_ignore_ascii_case("folder") {
1
} else {
2
}
}
fn section_s1() {
let iters: usize = env_or("BENCH_ITERS", 200_000);
// Equivalence across mixed case + invalid input.
for s in ["file", "File", "FOLDER", "folder", "Folder", "bogus", ""] {
assert_eq!(s1_before(s), s1_after(s), "S1 verdict differs for {s:?}");
}
let sample = "Folder"; // mixed-case → to_lowercase allocates
let before = measure(iters, || {
black_box(s1_before(black_box(sample)));
});
let after = measure(iters, || {
black_box(s1_after(black_box(sample)));
});
println!("\n## [S1] ShareItemType::try_from (per share item-type parse)");
header_footer(
"to_lowercase() String vs eq_ignore_ascii_case",
&before,
&after,
);
gate_allocs("S1", &before, &after);
}
fn main() {
println!("# Round-22 micro-pack — BEFORE/AFTER (counting allocator, release)");
println!("# allocs/op is the deterministic gate; a non-winning AFTER exits 1 (rollback).");
section_h1();
section_w1();
section_c1();
section_d1();
section_e1();
section_s1();
println!("\nAll Round-22 sections passed their allocation gate.");
}