From 4425fed53f1e54dde045365878cb79e57f856b92 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:12:20 +0100 Subject: [PATCH] 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 --- crates/xlsx/examples/write_perf.rs | 89 ++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 crates/xlsx/examples/write_perf.rs diff --git a/crates/xlsx/examples/write_perf.rs b/crates/xlsx/examples/write_perf.rs new file mode 100644 index 000000000..37f80cf6c --- /dev/null +++ b/crates/xlsx/examples/write_perf.rs @@ -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 ", 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"); + } +}