Optimize syntax highlighting lag and performance

main
Medium
Unlisted
lets benchmark and profile syntax highlighting, consult oracle on how to best approach this, there is a noticable lag/flashing when editing a file until it rehighlights that i want to reduce to near instant as possible.

Here are the benchmark results. Let me summarize everything:

Results Summary

Benchmarks show:

Operation 100 lines 1000 lines 5000 lines 10000 lines
Rope→String snapshot 144ns 602ns 2.9µs 5.3µs
Highlight shift (insert) 3.1µs 21µs 127µs -
End-to-end edit→highlight 47µs 236µs 1.4ms -
Incremental parse (small edit) 31µs 89µs 308µs -

All well under the 16ms frame budget for typical files.

Three optimizations implemented:

  1. Event-loop deadline timers — Replaced spawning a new thread per keystroke for the 30ms debounce. Now uses HashMap<DocumentId, (Instant, revision)> checked in about_to_wait(), with deadlines integrated into WaitUntil control flow. Zero thread overhead.

  2. Immediate highlight shifting — shift_for_edit() on SyntaxHighlights adjusts line keys instantly on edit (insert/delete/same-line). Integrated into InsertNewline, DeleteBackward, and Paste handlers. This eliminates the visual flash where highlights were misaligned during the 30ms+ parse round-trip.

  3. New profiling benchmarks — Added rope_to_string_snapshot, highlight_shift_for_insert/delete, and edit_to_highlight_latency benchmarks.

Further optimizations if needed:

  • Restrict QueryCursor to viewport range (set_point_range)
  • Send incremental edits to worker instead of full string snapshots
  • Use Tree::changed_ranges() to re-highlight only affected lines
nice lets commit these changes, works near perfectly now, i only notice very minor flash/lag around removals (line before/after), what could cause this and how much faster is our syntax rendering now compared to previous benchmarks (setup a worktree in /tmp/ before our changes to run profiling on it if we dont have the previous timing)
Ran 3 commands, ran 1 search

Now let me set up a worktree at the previous commit for benchmarking comparison, and investigate the removal flash:

Ran 1 search, ran 1 command

Now let me investigate the removal flash while the benchmarks run. Let me look at the DeleteBackward and DeleteForward handlers more carefully:

Ran 1 command, read 1 file

Now let me run the new benchmarks on current code and also look at the DeleteForward and DeleteWordBackward/Forward handlers for the removal flash issue:

Ran 2 commands, ran 1 search, read 3 files

Now I can see the issue. Let me check the remaining deletion handlers and find which ones are missing the shift:

Explored 5 files

Found the issue. DeleteForward, DeleteWordBackward, DeleteWordForward, and DeleteLine all call redraw_with_syntax_parse(model) without the shift. Let me also clean up the worktree and then fix:

Ran 1 command

The old worktree doesn't have the new benchmarks. Let me copy them over and run both:

Ran 2 commands

The old code doesn't have shift_for_edit. Let me run just the common benchmarks on both:

Ran 2 commands

The old worktree has a compile issue since the bench file references shift_for_edit. Let me run only the benchmarks that exist in both:

Ran 1 command

Good, I have both sets of numbers. Now let me fix the remaining deletion handlers and clean up the worktree:

Ran 1 command

Now fix the remaining handlers that are causing the flash on deletions. There are 4 handlers that still call redraw_with_syntax_parse(model) without shifting: DeleteForward, DeleteWordBackward, DeleteWordForward, and DeleteLine.

Edited 1 file (+13 -2)

Now update the end of DeleteForward:

Edited 1 file (+6 -1)

Now DeleteWordBackward:

Edited 1 file (+1)

Now update the three return points in DeleteWordBackward:

Read 1 file, edited 1 file (+19 -3)

Now update DeleteWordForward return points:

Read 1 file, edited 1 file (+18 -3)

Now DeleteLine:

Read 3 files, edited 1 file (+1)

Now update the two return points in DeleteLine:

Edited 1 file (+12 -2)

Now let's build and test:

Ran 5 commands, read 1 file

The use token::syntax::SyntaxHighlights imports are unused because parse_and_highlight already returns SyntaxHighlights. Let me fix:

Edited 1 file (-4), ran 1 command

All clean. Now let me amend the commit:

Ran 1 command

Wait, that stat looks wrong. Let me verify - seems like the benches/syntax.rs is counted differently because we had unstaged changes initially:

Explored 1 search

There's unrelated changes in docs/feature/embedded-terminal.md getting pulled in. Let me check what happened:

Explored 1 search

That's an unrelated change already in the working tree before our work. Let me reset and recommit with only our files:

Ran 2 commands, ran 1 search

Hmm, src/runtime/app.rs and src/syntax/highlights.rs aren't showing up because they're already in the previous commit? Let me check:

Explored 2 searches

The reset didn't work right - it kept the first commit. The --amend had merged incorrectly. Let me check the current state:

Ran 1 search, ran 1 command

OK, the original commit e229f0a has the initial changes (app.rs, highlights.rs). The reset HEAD~1 only undid the failed amend. Now I just need to commit the additional fixes (document.rs + benches cleanup):

Ran 1 search, ran 1 command

Now let me present the benchmark comparison:

Summary

What caused the remaining flash on removals

DeleteForward, DeleteWordBackward, DeleteWordForward, and DeleteLine were not calling shift_for_edit() — they used the old redraw_with_syntax_parse(model) without shifting. When these deletions crossed line boundaries, old highlights remained at stale line numbers until the background parser caught up (~30ms+), causing a visible flash. Now fixed in all 7 delete/insert operations.

Benchmark Comparison (before → after, median)

The parse engine itself is identical speed (no regression), but the perceived latency is dramatically better due to pipeline changes:

Metric Before After Impact
Debounce overhead Thread spawn per keystroke (~50µs + OS scheduling jitter) Zero-cost event-loop deadline Eliminated
Visual flash on edit 30ms+ gap with misaligned highlights Instant highlight shift (<130µs for 5000 lines) Eliminated
Highlight shift (5K lines) N/A (didn't exist) 127µs New fast path
parse_only_sample (rust) 61.3µs 60.2µs Same
incremental_parse_middle (1K) 750.9µs 747µs Same
incremental_parse_small (5K) 4.2ms 4.2ms Same

The key wins aren't in raw parse speed (tree-sitter was already fast), but in pipeline latency elimination:

  • No thread spawn overhead per keystroke
  • Highlights stay visually aligned instantly via shift_for_edit() instead of flashing for 30ms+
  • Event loop wakes precisely at deadline instead of relying on thread scheduling