Skip to content

perf(streaming): speed up iterRows on wide sheets of styled empty cells (#302) - #303

Merged
baseballyama merged 2 commits into
mainfrom
fix/issue-302-iter-rows-throughput
Oct 11, 2026
Merged

baseballyama merged 2 commits into
mainfrom
fix/issue-302-iter-rows-throughput

Conversation

@baseballyama

Copy link
Copy Markdown
Member

Summary

loadWorkbookStream's iterRows was several times slower than it needed to be on wide sheets where almost every cell is empty but styled (<c r="X1" s="n"/>). The row walk now drives saxes directly instead of consuming a per-event async iterator, so it runs faster than loadWorkbook on the same bytes instead of 1.5–3x slower.

Motivation

#302 reports ~46 µs per cell on a 30,000 × 227 sheet with 0.4% of cells holding a value. A profile showed saxes itself was a small part of the time. Most of it went to the iterParse layer: one event object per SAX event, Clark-notation names and attribute maps, and an async generator step for each event.

Closes #302

Changes

  • iterSheetRows (behind ReadOnlyWorksheet.iterRows / iterValues) registers its handlers on a SaxesParser and collects finished rows synchronously. It crosses an async boundary once per 64K feed instead of once per event. Rows completed before a content-limit error or a corrupt cell are still yielded before the error.
  • The feed / decode / DTD-prescan driver moved from src/xml/iterparse.ts to the internal src/xml/sax-feed.ts, and iterParse is now built on it. It lives in its own module so no public .d.ts imports saxes types. saxes' own declarations fail under skipLibCheck: false, and check:consumer caught this.
  • coordinateToTuple parses the plain "AB12" form from char codes before falling back to the regex. Both readers call it once per cell.

Testing

  • Existing streaming / xml / worksheet suites pass unchanged (pnpm test: 3761 tests).
  • New perf gate tests/perf/iter-rows-throughput.test.ts: 5,000 × 227 styled cells, comparing iterRows time against loadWorkbook on the same bytes. On main it fails (ratio 1.12). On this branch it passes (0.81–0.84).
  • Manual bench, 10,000 × 227 sheet built with the reproduction from the issue, alternating runs on the same machine: main 4.2–19.9 s, this branch 0.9–3.2 s.
PERF_GATE=1 pnpm test:perf tests/perf/iter-rows-throughput.test.ts

Breaking changes

None

Checklist

  • I have read CLAUDE.md and followed the project's conventions.
  • I have added or updated tests for the change.
  • I have added or updated documentation where user-visible behavior changed.
  • If this is a breaking change, I have added a changeset / CHANGELOG entry and
    flagged it above.
  • I have re-read my own diff and removed dead code, debug prints, and stale comments.
  • If I used an LLM to draft this PR, I have verified each change myself, the PR
    represents real work that warrants a maintainer's review, and I am willing to
    defend each line in review.

🤖 Generated with Claude Code

https://claude.ai/code/session_0142ArZjzXh8UP4dVS623qG3

…ls (#302)

A sheet whose rows carry hundreds of `<c r="X1" s="n"/>` cells is nearly
all SAX events and almost no values, so the per-event cost of the row
walk dominated: each event was allocated as an object with Clark-notation
names and attributes and handed through an async generator step. The
walk now registers its handlers on saxes directly and only crosses an
async boundary once per 64K feed, which brings it under loadWorkbook's
time for the same bytes instead of 1.5–3x over it.

The saxes driver moves to an internal module so no public declaration
imports saxes' types, which do not type-check under skipLibCheck: false.

coordinateToTuple also reads the plain "AB12" shape off char codes ahead
of the regex, which both readers call once per cell.

Closes #302

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142ArZjzXh8UP4dVS623qG3
Copilot AI balanced review requested due to automatic review settings October 11, 2026 06:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The corpus case that guards CDATA reads through loadWorkbookStream, which
no longer goes through iterParse, so the mutant has to sit on the handler
the row walk registers itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142ArZjzXh8UP4dVS623qG3
@baseballyama
baseballyama merged commit 77a8f6e into main Oct 11, 2026
16 checks passed
@baseballyama
baseballyama deleted the fix/issue-302-iter-rows-throughput branch October 11, 2026 07:01
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.

feat: Reading wide sheets with mostly-empty styled cells is ~17x slower per cell than dense sheets

2 participants