992bdae898
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
522 lines
21 KiB
Rust
522 lines
21 KiB
Rust
//! 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 `"` → `"`, re-allocating an owned `Cow`,
|
||
//! so 2 allocs/row (buffer + escape). AFTER emits the quotes as borrowed
|
||
//! pre-escaped `"` 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 `"` → `"`,
|
||
/// 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("ed))).unwrap();
|
||
w.write_event(Event::End(BytesEnd::new("D:getetag")))
|
||
.unwrap();
|
||
}
|
||
|
||
/// AFTER: borrowed pre-escaped `"` 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(""")))
|
||
.unwrap();
|
||
w.write_event(Event::Text(BytesText::new(etag))).unwrap();
|
||
w.write_event(Event::Text(BytesText::from_escaped(""")))
|
||
.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(""")))
|
||
.unwrap();
|
||
w.write_event(Event::Text(BytesText::new(id))).unwrap();
|
||
w.write_event(Event::Text(BytesText::from_escaped(""")))
|
||
.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.");
|
||
}
|