Skip to content

fix: perf quadratic ^block-id transform on long lines - #455

Open
zeyutang wants to merge 2 commits into
shd101wyy:developfrom
zeyutang:perf/block-id-guard
Open

fix: perf quadratic ^block-id transform on long lines#455
zeyutang wants to merge 2 commits into
shd101wyy:developfrom
zeyutang:perf/block-id-guard

Conversation

@zeyutang

Copy link
Copy Markdown

Problem

transformMarkdown runs the ^block-id replace on every normal line, ungated:

line = line.replace(/(.*?)\s+\^([a-zA-Z0-9_-]+)$/, ...);

The lazy (.*?) head under the $ anchor backtracks quadratically on every non-matching line, which could mean every sentence of a real document in the worst case. Cost grows with the sum of squared line lengths, so large documents with long lines (e.g. one sentence per line, without manual line wrap) reach multi-second previews: a CPU profile of a 192 KB paper attributes ~93% of warm parseMD self-time to this one regex.

Fix

Gate the replace behind a linear-time suffix test, leaving the replace untouched:

if (/\s\^[a-zA-Z0-9_-]+$/.test(line)) { ... }

The guard matches iff the replace regex matches (both require the line to end with whitespace, then ^, then one or more id characters), and when both match the original replace still runs, so rendered HTML is byte-identical. A greedy rewrite (/^(.*)\s+\^([a-zA-Z0-9_-]+)$/) would also be fast but shifts part of a multi-space run into group 1, changing output whitespace.

Timing (same machine, Node v26.4.0) before after
warm parseMD, 192 KB one-sentence-per-line file ~2.1 s ~55 ms
smoke test, 2000 non-matching lines x ~2128 chars ~5.6 s ~10 ms

Tests

  • Fixture pair block-id.md / block-id.expect.md covering the syntax battery (transform positives, mid-line and no-space negatives, $$ math and code-fence protection, indented code), pinning byte-identical output.
  • Unit tests for cases whose bytes editors and git rewrite in fixture files: trailing space after the id, CRLF, multiple carets (a ^x ^y), whitespace-only prefix.
  • Long-line smoke test with no asserted time bound (e.g., ~10 ms guarded vs ~5.6 s unguarded).

zeyutang added 2 commits July 22, 2026 21:35
* add block-id.md/.expect.md fixture pair covering the syntax battery
* pin trailing-space, CRLF, multi-caret, empty-prefix cases in unit tests
* unit tests cover the bytes that editors and git rewrite in fixtures
* gate the quadratic lazy-head replace behind a linear suffix test
* guard matches iff the replace matches, so output bytes are unchanged
* warm parseMD on a 192 KB sentence-per-line paper: ~2.1 s to ~55 ms
* add a long-line smoke test whose runtime exposes quadratic regressions
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