Skip to content

perf: cut DOCX, spreadsheet and HTML memory; refresh bench results - #49

Merged
chaliy merged 23 commits into
mainfrom
claude/project-thread-n3yks7
Oct 27, 2026
Merged

chaliy merged 23 commits into
mainfrom
claude/project-thread-n3yks7

Conversation

@chaliy

@chaliy chaliy commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Михайло · project thread

What changed

Fixes the remaining markkit-bench findings (the nesting one shipped in #43). Output is unchanged; memory and allocations drop.

  • DOCX: the body of document.xml is converted block by block while it is parsed, and each block's XML subtree is freed right after (xml::parse_split). The XML tree is compact: element names and attribute values are interned, attribute and child lists are exact-size boxed slices, and run text stays borrowed until it becomes IR (shared ooxml inline code, so PPTX benefits too). ZIP entries are read into a buffer pre-sized from the declared size (clamped to max_archive_entry_size and 16 MiB; limits still enforced on real bytes).
  • Spreadsheets: XLSX cells are formatted straight from calamine's DataRef (no clone into Data and second copy), and cell text is trimmed in place.
  • HTML: the scraper node arena is pre-sized from a clamped byte scan instead of growing by doubling; inline node lists are sized from the child count; text is collapsed without a copy; whitespace-only text allocates nothing.
  • Rendering (all formats): the Markdown escaper copies runs with nothing to escape directly (tested equal to the slow path), output normalization no longer allocates per line, and nested renderers stop reserving 4 KiB each.
  • New benchmark run committed; README and bench README tables refreshed with a 0.1.1 comparison column.

Why

Bench findings from #22: DOCX peaked at 18.5 MiB for a 9 KiB file, XLSX made ~5 allocations per cell, HTML peaked at ~25x input.

Before / After

just bench, allocations and peak heap per extraction. Before is main at 16b3014 + #43, after is this branch:

Case Allocs before → after Peak before → after
DOCX 400-section report 304,191 → 136,866 18.5 → 3.9 MiB
XLSX 150k cells 1,157,620 → 512,786 24.9 → 21.5 MiB
ODS 2000×8 125,054 → 72,498 3.0 → 3.0 MiB
HTML long docs page 94,262 → 49,342 7.5 → 4.7 MiB
HTML 50 tables 86,900 → 33,635 8.6 → 5.1 MiB
HTML blog post 4,765 → 2,505 304 → 241 KiB

DOCX report latency 52.6 → ~20 ms. Other timings are within noise. Re-checked after syncing with #44/#45/#47/#48: same numbers.

Risk

  • Low-medium. DOCX parsing now streams the body: structure errors (no w:document, no w:body) still surface (docx_without_document_or_body_is_error), and node and depth limits still apply while streaming (docx_streamed_body_keeps_node_and_depth_limits).
  • Capacity hints come from untrusted input but are clamped (ZIP: ≤16 MiB; DOM: ≤ len/4 nodes and ≤2^18 nodes). Unit tests cover the clamps.

Checklist

  • Tests added or updated (xml split/interning, escaper fast path = slow path, fmt_ref = fmt_data, trim_owned, node estimate clamps, DOCX structure/limit tests)
  • Backward compatibility considered (no API or output change)

🤖 Generated with Claude Code

chaliy added 22 commits October 26, 2026 23:35
Measures per-case latency, ops/s, throughput and heap allocations
(count, bytes, peak) plus process peak RSS over a generated
HTML/DOCX/XLSX/ODS corpus. just bench saves JSON + Markdown reports to
crates/markkit-bench/results/, mirroring bashkit-bench. Criterion moves
to just bench-criterion.
…duplication

html5ever's tree builder is quadratic in nesting depth: 687 KiB of nested
divs took 12.6 s and 292 KiB of unclosed nested tables ran past the 60 s
timeout. A pre-scan (html::nesting::cap_nesting) now drops tags nested past
max_nesting_depth + 8 before html5ever runs, keeping text.

Tables took rows via descendant queries, copying nested-table rows into
every enclosing table (4x output per level). Rows now come from direct
structure only.

Text flattened at max_nesting_depth leaked script/style/template bodies;
flat_text skips non-content elements. New invariant S17.
# Conflicts:
#	knowledge/security/threat-model.md
docx_report: peak heap 18.5 MiB -> 3.9 MiB, allocations 265k -> 137k.
Body blocks are converted while document.xml is parsed and freed after;
XML names and attribute values are interned; attribute and child lists
are exact-size; run text stays borrowed until it becomes IR. Markdown
escaping and output normalization skip per-run/per-line allocations,
which helps every format.
xlsx_large (150k cells): allocations 768k -> 513k, ods_medium 86k -> 72k.
XLSX cells are formatted straight from calamine's DataRef instead of
cloning into Data first, cell text is trimmed in place, and table
layout sizes each grid row once.
html_docs_page peak 7.5 -> 4.7 MiB, html_tables 8.6 -> 5.1 MiB;
allocations roughly halved. The scraper node arena is sized from a
clamped byte scan instead of growing by doubling, collect_inline sizes
its node list from the child count, text is collapsed without a copy,
and nested renderers no longer reserve 4 KiB each.
@chaliy chaliy self-assigned this Oct 27, 2026
@chaliy
chaliy merged commit 2cdd8c3 into main Oct 27, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant