diff --git a/scripts/benchmark-compose-actions.rs b/scripts/benchmark-compose-actions.rs new file mode 100644 index 00000000..99f34b17 --- /dev/null +++ b/scripts/benchmark-compose-actions.rs @@ -0,0 +1,146 @@ +//! Measure production document export, block formatting and list continuation. +//! rustc --edition=2024 -O scripts/benchmark-compose-actions.rs -o /tmp/compose-actions +//! /tmp/compose-actions export uniform 1048576 10 +//! For a matched source baseline, set COMPOSE_DOCUMENT_SOURCE to its absolute +//! path and build with --cfg benchmark_baseline. One condition per process. +//! Requested-live heap bytes exclude allocator overhead, layout, painting and +//! app RSS/PSS. Fixture construction and entered text are outside the action. + +use std::{ + alloc::{GlobalAlloc, Layout, System}, + sync::atomic::{AtomicUsize, Ordering}, + time::Instant, +}; +#[cfg(benchmark_baseline)] +#[allow(dead_code)] +mod rich_compose { + include!(env!("COMPOSE_DOCUMENT_SOURCE")); +} +#[cfg(not(benchmark_baseline))] +#[allow(dead_code)] +#[path = "../src/rich_compose.rs"] +mod rich_compose; +use rich_compose::RichComposeDocument; +struct Counter; +static LIVE: AtomicUsize = AtomicUsize::new(0); +static PEAK: AtomicUsize = AtomicUsize::new(0); +static CALLS: AtomicUsize = AtomicUsize::new(0); +#[global_allocator] +static ALLOCATOR: Counter = Counter; +fn grow(bytes: usize) { + let live = LIVE.fetch_add(bytes, Ordering::Relaxed) + bytes; + PEAK.fetch_max(live, Ordering::Relaxed); +} +unsafe impl GlobalAlloc for Counter { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let pointer = unsafe { System.alloc(layout) }; + if !pointer.is_null() { + grow(layout.size()); + CALLS.fetch_add(1, Ordering::Relaxed); + } + pointer + } + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + let pointer = unsafe { System.alloc_zeroed(layout) }; + if !pointer.is_null() { + grow(layout.size()); + CALLS.fetch_add(1, Ordering::Relaxed); + } + pointer + } + unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, size: usize) -> *mut u8 { + let next = unsafe { System.realloc(pointer, layout, size) }; + if !next.is_null() { + if size >= layout.size() { + grow(size - layout.size()); + } else { + LIVE.fetch_sub(layout.size() - size, Ordering::Relaxed); + } + CALLS.fetch_add(1, Ordering::Relaxed); + } + next + } + unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) { + unsafe { System.dealloc(pointer, layout) }; + LIVE.fetch_sub(layout.size(), Ordering::Relaxed); + } +} +fn fingerprint(value: &str) -> u64 { + value.bytes().fold(0xcbf29ce484222325, |hash, byte| { + (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3) + }) +} +fn main() { + let args = std::env::args().collect::>(); + assert_eq!(args.len(), 5, "expected action scenario bytes samples"); + let action = args[1].as_str(); + let scenario = args[2].as_str(); + assert!(matches!(action, "export" | "block" | "list")); + assert!(matches!(scenario, "uniform" | "fragmented" | "linked")); + let bytes: usize = args[3].parse().expect("draft bytes"); + let samples: usize = args[4].parse().expect("samples"); + assert!((1024..=4 * 1024 * 1024).contains(&bytes)); + assert!((1..=100).contains(&samples)); + let base = if action == "list" { + format!("• {}", "x".repeat(bytes - 4)) + } else { + "x".repeat(bytes) + }; + let entered = format!("{base}\n"); + println!( + "{{\"schema_version\":1,\"scope\":\"document_requested_heap\",\"action\":\"{action}\",\"scenario\":\"{scenario}\",\"draft_bytes\":{bytes},\"warmup_actions\":1,\"samples\":[" + ); + for index in 0..=samples { + let mut document = RichComposeDocument::default(); + let end = base.len() as i32; + document.synchronize(&base, end, end); + match scenario { + "fragmented" => { + for mark in 0..32 { + let start = (mark * base.len() / 64) as i32; + document.format("bold", &base, start, start + 1); + } + } + "linked" => { + document.set_link("https://example.org/?a=1&b=2", &base, 0, end); + } + _ => {} + } + let before = LIVE.load(Ordering::Relaxed); + PEAK.store(before, Ordering::Relaxed); + CALLS.store(0, Ordering::Relaxed); + let started = Instant::now(); + let output = match action { + "export" => document.body_html(), + "block" => { + document.format("bullet", &base, 0, end); + None + } + "list" => { + let end = entered.len() as i32; + document.synchronize(&entered, end, end); + None + } + _ => unreachable!(), + }; + let elapsed_us = started.elapsed().as_micros(); + let peak_extra = PEAK.load(Ordering::Relaxed).saturating_sub(before); + let settled_extra = LIVE.load(Ordering::Relaxed) as i128 - before as i128; + let allocation_calls = CALLS.load(Ordering::Relaxed); + let rendered = output.unwrap_or_else(|| document.body_html().unwrap()); + if index > 0 { + if index > 1 { + println!(","); + } + print!( + "{{\"elapsed_us\":{elapsed_us},\"peak_extra_bytes\":{peak_extra},\"settled_extra_bytes\":{settled_extra},\"allocation_calls\":{allocation_calls},\"html_bytes\":{},\"html_fingerprint\":{},\"selection\":[{},{}]}}", + rendered.len(), + fingerprint(&rendered), + document.selection().start, + document.selection().end + ); + } + std::hint::black_box((&document, &rendered)); + } + println!("\n]}}"); +}