332 lines
12 KiB
Rust
332 lines
12 KiB
Rust
|
|
//! Round-18 calendar-event edit CPU/alloc micro-pack (no Postgres).
|
|||
|
|
//!
|
|||
|
|
//! Same rule as ROUND2–17: each section is BEFORE (verbatim replica of the
|
|||
|
|
//! shipped-before shape) vs AFTER (verbatim replica of the shipped-after
|
|||
|
|
//! shape), with a byte-for-byte equivalence gate and a `GATE FAIL … rollback`
|
|||
|
|
//! check that exits non-zero if the AFTER arm fails to reduce allocations — the
|
|||
|
|
//! round's roll-back rule encoded into the benchmark.
|
|||
|
|
//!
|
|||
|
|
//! [C1] `CalendarEvent::update_ical_property` / `remove_ical_property`
|
|||
|
|
//! rewrote the ENTIRE `ical_data` body with `format!("{}{}{}")` on every
|
|||
|
|
//! call, and allocated TWO search needles per call (`\nNAME:` and the
|
|||
|
|
//! redundant `\r\nNAME:` — the CRLF form can never match where the LF
|
|||
|
|
//! form doesn't, since `\nNAME:` is its suffix). Because
|
|||
|
|
//! `calendar_storage_adapter::update_event` applies each changed field
|
|||
|
|
//! independently, a multi-field REST edit paid one full-body (up to
|
|||
|
|
//! ~11 KB) String allocation PER changed property, plus two needles.
|
|||
|
|
//! The shipped-after form mutates the body in place (`replace_range` for
|
|||
|
|
//! an existing property, four `insert`/`insert_str` for a new one) and
|
|||
|
|
//! builds the single `\nNAME:` needle on the stack — zero heap needle,
|
|||
|
|
//! no fresh-body allocation. The edited spans are byte-for-byte the same
|
|||
|
|
//! the `format!` reconstruction produced (`replace_range(a..b, v)` ≡
|
|||
|
|
//! `before + v + after`), so the emitted body is identical — including
|
|||
|
|
//! the pre-existing quirk that editing a CRLF line drops its `\r`.
|
|||
|
|
//!
|
|||
|
|
//! Run:
|
|||
|
|
//! cargo run --release --features bench --example bench_round18_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;
|
|||
|
|
|
|||
|
|
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 {
|
|||
|
|
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!(
|
|||
|
|
"| {:<44} | {:>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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|||
|
|
// [C1] calendar-event edit — per-property full-body format! vs in-place edit
|
|||
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
/// An edit step, mirroring the `event.update_*(…)` calls that
|
|||
|
|
/// `calendar_storage_adapter::update_event` fans out into `update_ical_property`
|
|||
|
|
/// (present → replace) and `remove_ical_property` (a cleared `Option`).
|
|||
|
|
enum Op<'a> {
|
|||
|
|
Update(&'a str, &'a str),
|
|||
|
|
Remove(&'a str),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── BEFORE: verbatim replica of the shipped-before methods ──────────────────
|
|||
|
|
|
|||
|
|
fn before_update(ical: &mut String, property_name: &str, value: &str) {
|
|||
|
|
let search_str = format!("\n{}:", property_name);
|
|||
|
|
let search_str_alt = format!("\r\n{}:", property_name);
|
|||
|
|
|
|||
|
|
let pos = ical
|
|||
|
|
.find(&search_str)
|
|||
|
|
.or_else(|| ical.find(&search_str_alt));
|
|||
|
|
|
|||
|
|
if let Some(pos) = pos {
|
|||
|
|
let value_start = pos + search_str.len();
|
|||
|
|
let value_end = ical[value_start..]
|
|||
|
|
.find('\n')
|
|||
|
|
.map(|p| value_start + p)
|
|||
|
|
.unwrap_or_else(|| ical.len());
|
|||
|
|
let before = &ical[..value_start];
|
|||
|
|
let after = &ical[value_end..];
|
|||
|
|
*ical = format!("{}{}{}", before, value, after);
|
|||
|
|
} else {
|
|||
|
|
let end_pos = ical.find("END:VEVENT").unwrap_or(ical.len());
|
|||
|
|
let before = &ical[..end_pos];
|
|||
|
|
let after = &ical[end_pos..];
|
|||
|
|
*ical = format!("{}{}:{}\n{}", before, property_name, value, after);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn before_remove(ical: &mut String, property_name: &str) {
|
|||
|
|
let search_str = format!("\n{}:", property_name);
|
|||
|
|
let search_str_alt = format!("\r\n{}:", property_name);
|
|||
|
|
|
|||
|
|
let pos = ical
|
|||
|
|
.find(&search_str)
|
|||
|
|
.or_else(|| ical.find(&search_str_alt));
|
|||
|
|
|
|||
|
|
if let Some(pos) = pos {
|
|||
|
|
let value_end = ical[pos + 1..]
|
|||
|
|
.find('\n')
|
|||
|
|
.map(|p| pos + 1 + p)
|
|||
|
|
.unwrap_or_else(|| ical.len());
|
|||
|
|
let before = &ical[..pos];
|
|||
|
|
let after = &ical[value_end..];
|
|||
|
|
*ical = format!("{}{}", before, after);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── AFTER: verbatim replica of the shipped-after methods ────────────────────
|
|||
|
|
|
|||
|
|
fn line_needle<'a>(buf: &'a mut [u8; 64], name: &str) -> Option<&'a str> {
|
|||
|
|
let n = name.len();
|
|||
|
|
if n + 2 > buf.len() {
|
|||
|
|
return None;
|
|||
|
|
}
|
|||
|
|
buf[0] = b'\n';
|
|||
|
|
buf[1..1 + n].copy_from_slice(name.as_bytes());
|
|||
|
|
buf[1 + n] = b':';
|
|||
|
|
std::str::from_utf8(&buf[..n + 2]).ok()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn after_update(ical: &mut String, property_name: &str, value: &str) {
|
|||
|
|
let mut buf = [0u8; 64];
|
|||
|
|
let needle_owned;
|
|||
|
|
let needle: &str = match line_needle(&mut buf, property_name) {
|
|||
|
|
Some(n) => n,
|
|||
|
|
None => {
|
|||
|
|
needle_owned = format!("\n{property_name}:");
|
|||
|
|
&needle_owned
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if let Some(pos) = ical.find(needle) {
|
|||
|
|
let value_start = pos + needle.len();
|
|||
|
|
let value_end = ical[value_start..]
|
|||
|
|
.find('\n')
|
|||
|
|
.map_or(ical.len(), |p| value_start + p);
|
|||
|
|
ical.replace_range(value_start..value_end, value);
|
|||
|
|
} else {
|
|||
|
|
let end_pos = ical.find("END:VEVENT").unwrap_or(ical.len());
|
|||
|
|
ical.reserve(property_name.len() + value.len() + 2);
|
|||
|
|
ical.insert(end_pos, '\n');
|
|||
|
|
ical.insert_str(end_pos, value);
|
|||
|
|
ical.insert(end_pos, ':');
|
|||
|
|
ical.insert_str(end_pos, property_name);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn after_remove(ical: &mut String, property_name: &str) {
|
|||
|
|
let mut buf = [0u8; 64];
|
|||
|
|
let needle_owned;
|
|||
|
|
let needle: &str = match line_needle(&mut buf, property_name) {
|
|||
|
|
Some(n) => n,
|
|||
|
|
None => {
|
|||
|
|
needle_owned = format!("\n{property_name}:");
|
|||
|
|
&needle_owned
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if let Some(pos) = ical.find(needle) {
|
|||
|
|
let value_end = ical[pos + 1..]
|
|||
|
|
.find('\n')
|
|||
|
|
.map_or(ical.len(), |p| pos + 1 + p);
|
|||
|
|
ical.replace_range(pos..value_end, "");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn apply_before(base: &str, ops: &[Op]) -> String {
|
|||
|
|
let mut ical = base.to_string();
|
|||
|
|
for op in ops {
|
|||
|
|
match op {
|
|||
|
|
Op::Update(n, v) => before_update(&mut ical, n, v),
|
|||
|
|
Op::Remove(n) => before_remove(&mut ical, n),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
ical
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn apply_after(base: &str, ops: &[Op]) -> String {
|
|||
|
|
let mut ical = base.to_string();
|
|||
|
|
for op in ops {
|
|||
|
|
match op {
|
|||
|
|
Op::Update(n, v) => after_update(&mut ical, n, v),
|
|||
|
|
Op::Remove(n) => after_remove(&mut ical, n),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
ical
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// A realistic stored VEVENT body (CRLF-terminated, ~1.5 KB with attendees and
|
|||
|
|
/// a VALARM) — the shape `calendar_storage_adapter` hydrates before applying an
|
|||
|
|
/// `UpdateEventDto`.
|
|||
|
|
fn base_body() -> String {
|
|||
|
|
let mut b = String::from(
|
|||
|
|
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//OxiCloud//NONSGML Calendar//EN\r\n\
|
|||
|
|
BEGIN:VEVENT\r\nUID:evt-round18@oxicloud.test\r\nDTSTAMP:20260101T100000Z\r\n\
|
|||
|
|
DTSTART:20260101T120000Z\r\nDTEND:20260101T130000Z\r\n\
|
|||
|
|
SUMMARY:Original quarterly planning sync\r\n\
|
|||
|
|
DESCRIPTION:The original description body for the event, moderately long.\r\n\
|
|||
|
|
LOCATION:Room A, Ground Floor\r\nCATEGORIES:work,planning,quarterly\r\n\
|
|||
|
|
ORGANIZER;CN=Alice Example:mailto:alice@oxicloud.test\r\n",
|
|||
|
|
);
|
|||
|
|
// A handful of attendees + a VALARM to bring the body to a realistic size,
|
|||
|
|
// so BEFORE's per-property full-body `format!` copies real bytes.
|
|||
|
|
for i in 0..8 {
|
|||
|
|
b.push_str(&format!(
|
|||
|
|
"ATTENDEE;CN=Guest {i};PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:guest{i}@oxicloud.test\r\n"
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
b.push_str(
|
|||
|
|
"BEGIN:VALARM\r\nACTION:DISPLAY\r\nDESCRIPTION:Reminder\r\nTRIGGER:-PT15M\r\nEND:VALARM\r\n\
|
|||
|
|
END:VEVENT\r\nEND:VCALENDAR\r\n",
|
|||
|
|
);
|
|||
|
|
b
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn section_calendar_edit() {
|
|||
|
|
let iters: usize = env_or("BENCH_ITERS", 200_000);
|
|||
|
|
let base = base_body();
|
|||
|
|
|
|||
|
|
// A full multi-field REST edit: five existing properties replaced (SUMMARY,
|
|||
|
|
// DESCRIPTION, LOCATION, DTSTART, DTEND — the last two rewritten twice, as
|
|||
|
|
// the time-range + all-day updates both do), one new property inserted
|
|||
|
|
// (RRULE, absent from the body), one cleared (CATEGORIES). Exactly the
|
|||
|
|
// `update_ical_property` / `remove_ical_property` fan-out of `update_event`.
|
|||
|
|
let ops = [
|
|||
|
|
Op::Update("SUMMARY", "Updated quarterly planning sync"),
|
|||
|
|
Op::Update(
|
|||
|
|
"DESCRIPTION",
|
|||
|
|
"A revised, noticeably longer description so the replacement value differs in length from the original and exercises the grow path.",
|
|||
|
|
),
|
|||
|
|
Op::Update("LOCATION", "Conference Room 42, Building B"),
|
|||
|
|
Op::Update("DTSTART", "20260202T090000Z"),
|
|||
|
|
Op::Update("DTEND", "20260202T100000Z"),
|
|||
|
|
Op::Update("DTSTART", "20260202T000000Z"),
|
|||
|
|
Op::Update("DTEND", "20260202T010000Z"),
|
|||
|
|
Op::Update("RRULE", "FREQ=WEEKLY;COUNT=10"),
|
|||
|
|
Op::Remove("CATEGORIES"),
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// Equivalence gate: the emitted body is byte-for-byte identical.
|
|||
|
|
let b = apply_before(&base, &ops);
|
|||
|
|
let a = apply_after(&base, &ops);
|
|||
|
|
assert_eq!(b, a, "C1 emitted body differs between BEFORE and AFTER");
|
|||
|
|
|
|||
|
|
let before = measure(iters, || {
|
|||
|
|
black_box(apply_before(black_box(&base), black_box(&ops)));
|
|||
|
|
});
|
|||
|
|
let after = measure(iters, || {
|
|||
|
|
black_box(apply_after(black_box(&base), black_box(&ops)));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
println!(
|
|||
|
|
"\n## [C1] calendar-event multi-field edit ({} ops, {}-byte body)",
|
|||
|
|
ops.len(),
|
|||
|
|
base.len()
|
|||
|
|
);
|
|||
|
|
println!("# both arms pay one identical `base.to_string()` reset per op (constant, shared)");
|
|||
|
|
header_footer("update_event in-place property rewrite", &before, &after);
|
|||
|
|
gate_allocs("C1", &before, &after);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn main() {
|
|||
|
|
println!("#################################################################");
|
|||
|
|
println!("# Round-18 calendar-event edit CPU/alloc micro-pack");
|
|||
|
|
println!("#################################################################");
|
|||
|
|
|
|||
|
|
section_calendar_edit();
|
|||
|
|
|
|||
|
|
println!("\nGATE PASS (all sections)");
|
|||
|
|
}
|