From cc68d71ac203963c53360096f35e0948ad13dd22 Mon Sep 17 00:00:00 2001 From: prxnvt <53838599+prxnvt@users.noreply.github.com> Date: Sat, 31 Oct 2026 13:04:05 -0400 Subject: [PATCH] XLSX: form controls find their VML shapes through an index Each control listed in looked for its VML shape by scanning every shape, so a sheet of 10,000 controls took over 8 s to open. The shapes are now indexed by number once, and the same sheet opens in about 0.4 s. --- crates/xlsx/src/controls.rs | 11 +++++++++-- crates/xlsx/src/tests/controls_tests.rs | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/crates/xlsx/src/controls.rs b/crates/xlsx/src/controls.rs index 58b240745..37765dd19 100644 --- a/crates/xlsx/src/controls.rs +++ b/crates/xlsx/src/controls.rs @@ -12,7 +12,7 @@ //! list boxes, drop-downs, ActiveX controls) are left to pass-through (`passthrough.rs`), which //! keeps them unchanged; a sheet can hold both, in one `` element and one VML drawing. -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::fmt::Write as _; use gridcraft_core::CellRef; @@ -404,7 +404,14 @@ pub fn read(cx: &mut Ctx<'_>, controls: Option<&El>, rels: &[Rel], sheet: &mut S None => None, }; let shapes: Vec<&El> = vml.as_ref().map(|v| v.kids("shape").collect()).unwrap_or_default(); - let shape_of = |n: u64| shapes.iter().copied().find(|s| number_of(s) == Some(n)); + // Looked up once per listed control: an index keeps a sheet of many controls linear. + let mut by_number: HashMap = HashMap::with_capacity(shapes.len()); + for s in &shapes { + if let Some(n) = number_of(s) { + by_number.entry(n).or_insert(s); + } + } + let shape_of = |n: u64| by_number.get(&n).copied(); // Every shape the sheet lists, read here or not. let mut listed: HashSet = HashSet::new(); for ctl in controls.map(controls_in).unwrap_or_default() { diff --git a/crates/xlsx/src/tests/controls_tests.rs b/crates/xlsx/src/tests/controls_tests.rs index f8aa60d08..8825e040a 100644 --- a/crates/xlsx/src/tests/controls_tests.rs +++ b/crates/xlsx/src/tests/controls_tests.rs @@ -282,3 +282,24 @@ fn hostile_form_controls_are_capped_and_repaired() { let (back, _) = read_xlsx(&write_xlsx(&wb).unwrap()).unwrap(); assert_eq!(back.sheet(0).unwrap().controls.len(), c.len()); } + +/// Each listed control finds its VML shape through an index, so a sheet of many controls reads in +/// linear time (it used to scan every shape for every control). +#[test] +#[allow(clippy::disallowed_methods)] // a native-only timing test: the clock is read off wasm +fn many_listed_controls_read_in_linear_time() { + let mut wb = Workbook::default(); + if let Some(sh) = wb.sheet_mut(0) { + for i in 0..10_000u32 { + let a = anchor(&format!("B{}", i + 1), 60.0, 18.0); + sh.controls.push(FormControl { id: i + 1, kind: ControlKind::Spin, anchor: a, ..Default::default() }); + } + } + let bytes = write_xlsx(&wb).unwrap(); + let t0 = std::time::Instant::now(); + let (back, _) = read_xlsx(&bytes).unwrap(); + let took = t0.elapsed(); + assert_eq!(back.sheet(0).unwrap().controls.len(), 10_000); + // About 0.4 s here; scanning every shape for every control took over 8 s. + assert!(took < std::time::Duration::from_secs(3), "{took:?}"); +}