Add an XLSX write benchmark example

`cargo run --release -p gridcraft-xlsx --example write_perf -- [rows] [runs] [out.xlsx]`
builds a sales-style workbook (order ids, dates, categories, numbers, two
formula columns with cached values, booleans, sparse notes, a summary sheet)
and times write_xlsx, printing each run and the median / minimum. An optional
output path keeps the last package for byte comparisons between builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-10-27 15:12:20 +01:00
co-authored by Claude Opus 5.5
parent 45f5ab18e4
commit 4425fed53f
+89
View File
@@ -0,0 +1,89 @@
//! XLSX save timing on a generated workbook of realistic shape (release build):
//! `cargo run --release -p gridcraft-xlsx --example write_perf -- [rows] [runs] [out.xlsx]`
//!
//! The workbook mixes what a large business sheet holds: repeated and unique text, integers,
//! decimals, dates, booleans, formulas with cached values, a few styles, a header row and a
//! second summary sheet. Prints each run and the median / minimum of `write_xlsx`.
#![allow(clippy::disallowed_methods)] // a native-only benchmark: wasm never runs it
use std::time::Instant;
use gridcraft_core::{CellRef, Value};
use gridcraft_model::{Cell, Formula, NumFmt, Sheet, StyleId, Workbook};
fn build(rows: u32) -> Workbook {
let mut wb = Workbook::new();
let bold = wb.styles.derive(StyleId::DEFAULT, |s| s.font.bold = true);
let money = wb.styles.derive(StyleId::DEFAULT, |s| s.num_fmt = NumFmt::new("#,##0.00"));
let date = wb.styles.derive(StyleId::DEFAULT, |s| s.num_fmt = NumFmt::new("yyyy-mm-dd"));
let pct = wb.styles.derive(StyleId::DEFAULT, |s| s.num_fmt = NumFmt::new("0.0%"));
let regions = ["North", "South", "East", "West", "Central"];
let products = ["Widget", "Gadget", "Gizmo", "Doohickey", "Sprocket", "Thingamajig", "Whatsit"];
let mut s = Sheet::new("Sales");
let set = |s: &mut Sheet, row: u32, col: u32, value: Value, style: StyleId| {
s.cells.set(CellRef::new(row, col), Cell { value, formula: None, style });
};
for (col, title) in ["Order", "Date", "Region", "Product", "Units", "Price", "Revenue", "Share", "Paid", "Note"].iter().enumerate() {
set(&mut s, 0, col as u32, Value::text(*title), bold);
}
for r in 1..=rows {
let i = r as usize;
set(&mut s, r, 0, Value::text(format!("ORD-{:07}", r)), StyleId::DEFAULT);
set(&mut s, r, 1, Value::Number(45000.0 + (i % 730) as f64), date);
set(&mut s, r, 2, Value::text(regions[i % regions.len()]), StyleId::DEFAULT);
set(&mut s, r, 3, Value::text(products[i % products.len()]), StyleId::DEFAULT);
let units = (i * 7 % 250 + 1) as f64;
let price = ((i * 37 % 10_000) as f64) / 100.0 + 0.99;
set(&mut s, r, 4, Value::Number(units), StyleId::DEFAULT);
set(&mut s, r, 5, Value::Number(price), money);
let mut rev = Cell::formula(Formula::new(&format!("=E{0}*F{0}", r + 1)));
rev.value = Value::Number(units * price);
rev.style = money;
s.cells.set(CellRef::new(r, 6), rev);
let mut share = Cell::formula(Formula::new(&format!("=G{}/Summary!$B$1", r + 1)));
share.value = Value::Number(units * price / 1.0e7);
share.style = pct;
s.cells.set(CellRef::new(r, 7), share);
set(&mut s, r, 8, Value::Bool(!i.is_multiple_of(3)), StyleId::DEFAULT);
if i.is_multiple_of(10) {
set(&mut s, r, 9, Value::text(format!("Follow up with customer #{} & check <terms>", i % 977)), StyleId::DEFAULT);
}
}
let mut summary = Sheet::new("Summary");
let mut total = Cell::formula(Formula::new(&format!("=SUM(Sales!G2:G{})", rows + 1)));
total.value = Value::Number(1.0e7);
summary.cells.set(CellRef::new(0, 1), total);
summary.cells.set(CellRef::new(0, 0), Cell { value: Value::text("Total"), formula: None, style: bold });
for (k, region) in regions.iter().enumerate() {
let row = k as u32 + 2;
summary.cells.set(CellRef::new(row, 0), Cell { value: Value::text(*region), formula: None, style: StyleId::DEFAULT });
let mut f = Cell::formula(Formula::new(&format!("=SUMIFS(Sales!G:G,Sales!C:C,A{})", row + 1)));
f.value = Value::Number(2.0e6);
f.style = money;
summary.cells.set(CellRef::new(row, 1), f);
}
wb.sheets = vec![std::sync::Arc::new(s), std::sync::Arc::new(summary)];
wb
}
fn main() {
let mut args = std::env::args().skip(1);
let rows: u32 = args.next().and_then(|s| s.parse().ok()).unwrap_or(200_000);
let runs: usize = args.next().and_then(|s| s.parse().ok()).unwrap_or(7).max(1);
let out = args.next();
let wb = build(rows);
let mut times = vec![];
let mut bytes = vec![];
for i in 0..runs {
let t = Instant::now();
bytes = gridcraft_xlsx::write_xlsx(&wb).expect("write");
let dt = t.elapsed().as_secs_f64() * 1000.0;
println!("run {i}: {dt:.1} ms ({} bytes)", bytes.len());
times.push(dt);
}
times.sort_by(f64::total_cmp);
println!("write_xlsx {rows} rows: median {:.1} ms, min {:.1} ms", times[times.len() / 2], times[0]);
if let Some(path) = out {
std::fs::write(&path, &bytes).expect("write output");
}
}