Skip to content

perf: speed up read_span with bulk memchr line scanning#211

Draft
connorshea wants to merge 1 commit into
mainfrom
perf/memchr-read-span
Draft

perf: speed up read_span with bulk memchr line scanning#211
connorshea wants to merge 1 commit into
mainfrom
perf/memchr-read-span

Conversation

@connorshea

Copy link
Copy Markdown
Member

Summary

context_info (the engine behind every built-in SourceCode::read_span impl) walked the source byte-at-a-time from offset 0 for every call. This PR scans the region before the span in bulk with memchr2 instead, deriving line count, context line starts, and column arithmetically from newline positions. The original byte-loop is kept verbatim for the span itself and the trailing context, so behavior is unchanged.

It also fixes a panic found while validating the change: a zero-length span starting exactly one byte past EOF ("a".read_span(&(2, 0).into(), 0, 0)) sliced out of bounds instead of returning Err(MietteError::OutOfBounds). This was reachable from GraphicalReportHandler, which calls read_span(label, 0, 0) on the primary label, so an off-by-one zero-length label span could panic report rendering. A regression test is included.

memchr moves from transitive to direct dependency.

Performance

Measured on a 3MB / 40k-line source, span near EOF, --release:

Metric before after speedup
read_span, LF endings 2.17 ms 260 µs 8.4x
read_span, CRLF endings 2.20 ms 453 µs 4.8x
full graphical render, 1-label diagnostic 3.43 ms 390 µs 8.8x

Render cost previously scaled linearly with file size (3 full-file scans per rendered diagnostic); those scans are now ~8x cheaper. Heap usage is unchanged: one 32-byte VecDeque allocation per call at default context settings, independent of file size (verified with a counting allocator).

Verification

  • Differential-fuzzed the new implementation against the old one: 309,698 cases (random content with LF/CRLF/lone-CR mixes, random spans including zero-length and out-of-bounds, context settings 0–3) with 0 mismatches; the 68 cases where the old code panics now return Err(OutOfBounds).
  • Full test suite passes including the 38 graphical golden-output tests (the color_format failures are pre-existing/environmental — they fail identically on main in a non-tty sandbox).

🤖 Generated with Claude Code

context_info now scans the region before the span in bulk with
memchr2 instead of a byte-at-a-time loop, deriving line count,
context line starts, and column from newline positions. The original
byte-loop still handles the span itself and the trailing context.

Measured on a 3MB source with the span near EOF: read_span goes from
2.17ms to 260us per call (8.4x, LF endings) / 453us (4.8x, CRLF);
end-to-end GraphicalReportHandler render of a single-label diagnostic
drops from 3.43ms to 390us (8.8x). Verified against the previous
implementation with 300k+ differential fuzz cases (0 mismatches).

Also fixes a panic found by that fuzzing: a zero-length span starting
exactly one byte past EOF sliced out of bounds instead of returning
MietteError::OutOfBounds. Adds a regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@connorshea

Copy link
Copy Markdown
Member Author

I need to review/do more testing before marking this ready

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