perf(highlight): stop re-parsing huge lines on every frame (fixes #2838) - #2841
Merged
Conversation
Viewing a 400+ KB single-line JSON pinned a CPU core (fresh#2838). Any input event (a keystroke, or just mouse motion over the window) triggers a re-render, and each render re-parsed the entire line. Root cause: the highlight cache only advanced its commit point on newline-terminated lines. That guards streaming buffers, whose trailing partial line may still grow — resuming from a state captured mid-line loses diff bg (the dark-bar artefact in + lines). A file that is one huge line with no trailing newline never committed anything, so every frame re-parsed from scratch; checkpoints are newline-aligned too, so there were none inside the line to resume from either. Two changes: - A parse that reaches the buffer end commits its trailing partial line. The streaming hazard cannot fire through it: both cache-consuming paths in highlight_viewport already require last_buffer_len == buffer.len() and no pending edit, so a grown buffer never resumes from that state. Checkpoints stay newline-aligned — find_parse_resume_point is NOT length-gated, so a mid-line checkpoint could still be chosen after growth. - Lines longer than MAX_PARSE_BYTES have only the viewport-overlapping window parsed, from a clone of the parse state. Past that size the parse range is already a viewport window, so the EOF commit cannot apply and bounded-but-approximate beats unbounded. Below it the EOF commit parses such a line once with exact colours. test_streaming_partial_diff_line_keeps_bg_when_rest_arrives pins the rendered outcome (every byte of a completed + line keeps its diff bg when the rest streams in) rather than the commit boundary, so it stays valid across this change; it passed before and after. Measured on the issue's file at 80x24, per input event at 2 Hz (so the editor is not saturated): mouse/scroll ~480 ms -> ~150 ms, typing ~468 ms -> ~152 ms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J3uiv3CFZEAFARmTkqxpaQ
sinelaw
force-pushed
the
claude/highlight-long-line-perf-wqwlh7
branch
from
July 28, 2026 18:48
43394fc to
12bbc1f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2838. Independent of the wrapped-line navigation PRs (#2784, #2813) — this touches only
highlight_engine.rsand branches straight off master.Problem
Viewing a 400+ KB single-line JSON pins a CPU core. Any input event — a keystroke, or just mouse motion over the window — triggers a re-render, and every render re-parsed the entire line.
Root cause
The highlight cache only advanced its commit point on newline-terminated lines. That rule guards streaming buffers, whose trailing partial line may still grow: resuming from a state captured mid-line loses the diff background (the dark-bar artefact inside
+lines). A file that is one huge line with no trailing newline — a minified JSON one-liner — therefore never committed anything, so every frame re-parsed from scratch. Checkpoints are newline-aligned too, so there were none inside the line to resume from either.Changes
highlight_viewportalready requirelast_buffer_len == buffer.len()and no pending edit, so a grown buffer never resumes from that state. Checkpoints stay newline-aligned —find_parse_resume_pointis not length-gated, so a mid-line checkpoint could still be chosen after growth.MAX_PARSE_BYTEShave only the viewport-overlapping window parsed, from a clone of the parse state. Past that size the parse range is already a viewport window, so the EOF commit cannot apply and bounded-but-approximate beats unbounded. Below it the EOF commit parses such a line once with exact colours.Measurements
On the issue's attached file at 80x24, per input event at 2 Hz so the editor is not saturated (at higher rates CPU saturates near 100% regardless and hides the difference):
Remaining cost is the wrap pipeline, not highlighting.
Testing
The guard being relaxed had no behavioural test, only one asserting the commit boundary. This PR adds
test_streaming_partial_diff_line_keeps_bg_when_rest_arrives, which pins the rendered outcome: when the rest of a partially-streamed+line arrives, every byte of the completed line still carries the diff background. It passes both before and after the change, so it validates the guard's purpose rather than its mechanism. The old boundary assertion is replaced by its EOF-commit counterpart.Full lib suite (3076), plus the syntax (145), highlight (191), and diff (114) e2e suites pass.
🤖 Generated with Claude Code
https://claude.ai/code/session_01J3uiv3CFZEAFARmTkqxpaQ
Generated by Claude Code