use std::alloc::{GlobalAlloc, Layout, System};
use std::hint::black_box;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Instant;
use rust_xlsxwriter::Workbook;
struct TrackingAllocator;
static CURRENT: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);
unsafe impl GlobalAlloc for TrackingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { System.alloc(layout) };
if !ptr.is_null() {
let now = CURRENT.fetch_add(layout.size(), Ordering::SeqCst) + layout.size();
PEAK.fetch_max(now, Ordering::SeqCst);
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) };
CURRENT.fetch_sub(layout.size(), Ordering::SeqCst);
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let new_ptr = unsafe { System.realloc(ptr, layout, new_size) };
if !new_ptr.is_null() {
let old_size = layout.size();
if new_size >= old_size {
let delta = new_size - old_size;
let now = CURRENT.fetch_add(delta, Ordering::SeqCst) + delta;
PEAK.fetch_max(now, Ordering::SeqCst);
} else {
CURRENT.fetch_sub(old_size - new_size, Ordering::SeqCst);
}
}
new_ptr
}
}
#[global_allocator]
static ALLOCATOR: TrackingAllocator = TrackingAllocator;
fn reset_peak() {
PEAK.store(CURRENT.load(Ordering::SeqCst), Ordering::SeqCst);
}
fn current() -> usize {
CURRENT.load(Ordering::SeqCst)
}
fn peak() -> usize {
PEAK.load(Ordering::SeqCst)
}
fn measure_peak<F: FnOnce() -> R, R>(f: F) -> (R, usize) {
let baseline = current();
reset_peak();
let result = f();
let net = peak().saturating_sub(baseline);
(result, net)
}
fn make_dense(rows: u32, cols: u16, changed: bool) -> Vec<u8> {
let mut wb = Workbook::new();
let ws = wb.add_worksheet();
for r in 0..rows {
for c in 0..cols {
let val = if changed && r == 0 && c == 0 {
"changed".to_string()
} else {
format!("val_{r}_{c}")
};
ws.write_string(r, c, &val).unwrap();
}
}
wb.save_to_buffer().unwrap()
}
fn make_sparse(rows: u32, cols: u16, density_pct: u32, changed: bool) -> Vec<u8> {
let mut wb = Workbook::new();
let ws = wb.add_worksheet();
let mut n: u32 = 0;
for r in 0..rows {
for c in 0..cols {
n += 1;
if n.is_multiple_of(100 / density_pct) {
let val = if changed && r == 0 && c == 0 {
"changed"
} else {
"val"
};
ws.write_string(r, c, val).unwrap();
}
}
}
wb.save_to_buffer().unwrap()
}
fn run_agreement_check(temp_dir: &std::path::Path) {
println!("== Agreement check (required test 1) ==");
let old = make_dense(20, 5, false);
let new = make_dense(20, 5, true);
let old_path = temp_dir.join("agree-old.xlsx");
let new_path = temp_dir.join("agree-new.xlsx");
std::fs::write(&old_path, &old).unwrap();
std::fs::write(&new_path, &new).unwrap();
let v1_diff = sheets_diff_v1_2::core::diff::Diff::try_new(&old_path, &new_path)
.expect("v1.2 try_new should succeed on a well-formed fixture");
let v1_changed: usize = v1_diff.cell_diffs.iter().map(|sc| sc.cells.len()).sum();
let v2_diff =
sheets_diff::compare_paths(&old_path, &new_path).expect("v2 compare_paths should succeed");
let v2_changed = v2_diff.summary.cells_changed;
std::fs::remove_file(&old_path).ok();
std::fs::remove_file(&new_path).ok();
println!(" v1.2 cell_diffs (summed across sheets): {v1_changed}");
println!(" v2 summary.cells_changed: {v2_changed}");
assert_eq!(
v1_changed, v2_changed as usize,
"v1.2 and v2 disagree on changed-cell count for an unambiguous fixture -- \
a benchmark of two functions that disagree about the answer is not a comparison"
);
println!(" PASS -- both versions agree\n");
}
struct ScenarioResult {
name: &'static str,
v1_ms: Vec<f64>,
v2_ms: Vec<f64>,
v1_peak: usize,
v2_peak: usize,
}
fn run_scenario(
name: &'static str,
old: Vec<u8>,
new: Vec<u8>,
temp_dir: &std::path::Path,
repeats: u32,
) -> ScenarioResult {
let old_path = temp_dir.join(format!("{name}-old.xlsx"));
let new_path = temp_dir.join(format!("{name}-new.xlsx"));
std::fs::write(&old_path, &old).unwrap();
std::fs::write(&new_path, &new).unwrap();
let mut v1_ms = Vec::with_capacity(repeats as usize);
let mut v2_ms = Vec::with_capacity(repeats as usize);
for _ in 0..repeats {
let start = Instant::now();
let d =
sheets_diff_v1_2::core::diff::Diff::try_new(black_box(&old_path), black_box(&new_path))
.unwrap();
black_box(&d);
v1_ms.push(start.elapsed().as_secs_f64() * 1000.0);
let start = Instant::now();
let d = sheets_diff::compare_paths(black_box(&old_path), black_box(&new_path)).unwrap();
black_box(&d);
v2_ms.push(start.elapsed().as_secs_f64() * 1000.0);
}
let (_, v1_peak) = measure_peak(|| {
let d =
sheets_diff_v1_2::core::diff::Diff::try_new(black_box(&old_path), black_box(&new_path))
.unwrap();
black_box(&d);
d
});
let (_, v2_peak) = measure_peak(|| {
let d = sheets_diff::compare_paths(black_box(&old_path), black_box(&new_path)).unwrap();
black_box(&d);
d
});
std::fs::remove_file(&old_path).ok();
std::fs::remove_file(&new_path).ok();
ScenarioResult {
name,
v1_ms,
v2_ms,
v1_peak,
v2_peak,
}
}
fn avg(v: &[f64]) -> f64 {
v.iter().sum::<f64>() / v.len() as f64
}
fn variance_pct(v: &[f64]) -> f64 {
let (min, max) = v
.iter()
.fold((f64::MAX, f64::MIN), |(mn, mx), &x| (mn.min(x), mx.max(x)));
(max - min) / max.max(1e-9) * 100.0
}
fn print_scenario(r: &ScenarioResult) {
let v1_avg = avg(&r.v1_ms);
let v2_avg = avg(&r.v2_ms);
let time_delta_pct = (v2_avg - v1_avg) / v1_avg.max(1e-9) * 100.0;
let mem_delta_pct = (r.v2_peak as f64 - r.v1_peak as f64) / (r.v1_peak.max(1) as f64) * 100.0;
println!("-- {} --", r.name);
println!(
" time: v1.2 avg={:.3} ms {:?} (variance {:.2}%)",
v1_avg,
r.v1_ms
.iter()
.map(|v| format!("{v:.2}"))
.collect::<Vec<_>>(),
variance_pct(&r.v1_ms)
);
println!(
" v2 avg={:.3} ms {:?} (variance {:.2}%)",
v2_avg,
r.v2_ms
.iter()
.map(|v| format!("{v:.2}"))
.collect::<Vec<_>>(),
variance_pct(&r.v2_ms)
);
println!(" v2 vs v1.2: {time_delta_pct:+.1}%");
println!(
" peak memory: v1.2={} bytes v2={} bytes v2 vs v1.2: {mem_delta_pct:+.1}%",
r.v1_peak, r.v2_peak
);
println!();
}
fn main() {
let temp_dir =
std::env::temp_dir().join(format!("sheets-diff-v1-2-bench-{}", std::process::id()));
std::fs::create_dir_all(&temp_dir).unwrap();
println!("sheets-diff v1.2-vs-v2 comparison (M7 Handoff 02)");
println!("profile: release, opt-level=\"z\" (this crate's [profile.release])");
println!("v1.2 pin: sheets-diff-v1_2 = \"=1.2.0\" (Cargo.toml)");
println!(
"v2 version under test: {} -- see docs/src/maintainers/performance.md \
for the exact commit this report's numbers were generated against",
env!("CARGO_PKG_VERSION")
);
println!();
run_agreement_check(&temp_dir);
const REPEATS: u32 = 5;
let scenarios: Vec<ScenarioResult> = vec![
run_scenario(
"small_dense",
make_dense(50, 20, false),
make_dense(50, 20, true),
&temp_dir,
REPEATS,
),
run_scenario(
"tall",
make_dense(5_000, 20, false),
make_dense(5_000, 20, true),
&temp_dir,
REPEATS,
),
run_scenario(
"sparse",
make_sparse(1_000, 100, 5, false),
make_sparse(1_000, 100, 5, true),
&temp_dir,
REPEATS,
),
];
println!("== Per-scenario results ==");
println!(
"What v2 does that v1.2 does not, for every number below: typed \
CellValue normalisation (not string comparison), row alignment \
support, per-cell diagnostics, DiffMetrics tracking, and \
independent value/formula change detection. v2 being slower is \
not automatically a defect -- it may be the cost of capability \
v1.2 never had.\n"
);
for r in &scenarios {
print_scenario(r);
}
std::fs::remove_dir_all(&temp_dir).ok();
println!("Done.");
}