mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-28 04:36:51 -06:00
Saving a large workbook spent about half its time building worksheet XML, mostly in small per-cell allocations. The writer now appends to the part directly: - Cell references, row numbers, style ids, shared string indexes and numbers are appended without temporary strings (push_a1, push_u64, push_num), and text is escaped in place (push_esc, with a fast path for plain ASCII). - sheetData walks the cell store in its own row-major order instead of copying every address into a BTreeSet and a per-row BTreeMap and looking each cell up again. Cells outside the store (pictures, spill values, table headers) still go through the sorted set. - Formulas with nothing the file spells differently (no LET/LAMBDA, no _xlfn function, no spill, @, trim operator or structured reference) are printed from the shared expression instead of being cloned and rebuilt twice by the file rewrite. - The A1 printer in gridcraft-formula appends references to the output instead of formatting each anchor into its own string. The output is byte-for-byte unchanged. write_perf, 200,000 rows (2.0M cells, 14.2 MB package), release, Ryzen 16 threads / Linux, 36 interleaved runs each: before: median 2083.9 ms, min 1615.7 ms after: median 1501.3 ms, min 1192.3 ms (-28.0 % median, -26.2 % min) What remains is mostly deflate (zlib-rs, level 6), about 70 % of the save. Tests pin the exact sheetData of a workbook covering row settings between cell rows, spill values, every value kind, number spellings, escaping and formulas with and without a file spelling (generated with the previous writer), and check each appending helper and the formula fast path against the code they replace. The formula fast path is guarded by a test that runs both paths over every function in the gridcraft-functions registry (a new dev-dependency, allowed by the layer rules), in 15 surrounding shapes covering every operator and rewrite, so a function or rewrite added later without updating the trigger list fails it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
73 lines
3.1 KiB
Rust
73 lines
3.1 KiB
Rust
//! Integration-style tests: round trips, hand-written fixtures, hostile input and CSV.
|
|
|
|
mod array_export;
|
|
mod cellmetadata;
|
|
mod chart_book_names;
|
|
mod chart_bool_default;
|
|
mod chart_combo_kinds;
|
|
mod chart_ref_offsets;
|
|
mod csv_tests;
|
|
mod encrypted_tests;
|
|
mod fixtures;
|
|
mod ink_roundtrip;
|
|
mod literal_category_roundtrip;
|
|
mod locale_numfmt;
|
|
mod malformed;
|
|
mod multilevel_category_roundtrip;
|
|
mod note_box;
|
|
mod ods_tests;
|
|
mod passthrough_tests;
|
|
mod phonetic_tests;
|
|
mod pivot_tests;
|
|
mod richdata_tests;
|
|
mod roundtrip;
|
|
mod shape_text_rtl;
|
|
mod shared_media;
|
|
mod sheet_data_pin;
|
|
mod sheet_names;
|
|
mod smooth_roundtrip;
|
|
mod trendline_roundtrip;
|
|
mod xls_tests;
|
|
mod xlsb_tests;
|
|
mod zip_order;
|
|
|
|
use std::io::Write;
|
|
|
|
/// Builds a zip package from (name, content) pairs.
|
|
pub fn make_zip(parts: &[(&str, &[u8])]) -> Vec<u8> {
|
|
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
|
for (name, data) in parts {
|
|
z.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
|
z.write_all(data).unwrap();
|
|
}
|
|
z.finish().unwrap().into_inner()
|
|
}
|
|
|
|
pub const CT: &str = r#"<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml"/><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/></Types>"#;
|
|
pub const ROOT_RELS: &str = r#"<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>"#;
|
|
|
|
/// A minimal one-sheet package with the given sheet XML body (inside `<worksheet>`), plus
|
|
/// optional extra parts and workbook relationships.
|
|
pub fn minimal(sheet_body: &str, extra_parts: &[(&str, &str)], wb_extra_rels: &str, wb_extra: &str) -> Vec<u8> {
|
|
let wb = format!(
|
|
r#"<?xml version="1.0"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">{wb_extra}<sheets><sheet name="Data" sheetId="1" r:id="rId1"/></sheets></workbook>"#
|
|
);
|
|
let rels = format!(
|
|
r#"<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>{wb_extra_rels}</Relationships>"#
|
|
);
|
|
let sheet = format!(
|
|
r#"<?xml version="1.0"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">{sheet_body}</worksheet>"#
|
|
);
|
|
let mut parts: Vec<(&str, &[u8])> = vec![
|
|
("[Content_Types].xml", CT.as_bytes()),
|
|
("_rels/.rels", ROOT_RELS.as_bytes()),
|
|
("xl/workbook.xml", wb.as_bytes()),
|
|
("xl/_rels/workbook.xml.rels", rels.as_bytes()),
|
|
("xl/worksheets/sheet1.xml", sheet.as_bytes()),
|
|
];
|
|
for (n, c) in extra_parts {
|
|
parts.push((n, c.as_bytes()));
|
|
}
|
|
make_zip(&parts)
|
|
}
|