perf(input): cache per-line tree-sitter matches across frames#2462
perf(input): cache per-line tree-sitter matches across frames#2462dux-web wants to merge 1 commit into
Conversation
…ames `SyntaxHighlighter::styles()` re-ran a tree-sitter query for the visible range on every frame, so scrolling a code editor re-highlighted unchanged lines continuously. Cache `match_styles` results per line, keyed by a new `generation` counter that is bumped whenever the syntax tree changes (edits and background-parse completion). While `generation` is stable (e.g. during scrolling) cached line matches are reused; an edit invalidates them. Profiling a code-editor scroll: Window::draw dropped ~40% (8709 -> 5220 samples) with highlighting unchanged, including correct first-screen highlighting after a background parse completes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
On the highlight cache: I don't think it's worth it. I benchmarked
Two things stand out:
So we'd be trading real added complexity (a generation counter, per-frame cache, invalidation) for a speedup on a path that already costs <1% of a frame. I don't think that trade is worth it — I'd rather drop the cache and keep |
Problem
When scrolling a
code_editor,SyntaxHighlighter::styles()re-runs the tree-sittermatch query (
match_styles) over the entire visible byte range on every frame, evenwhen nothing changed. For large/complex files this dominates the editor's paint cost.
Change
Add a per-line match cache to
SyntaxHighlighter:generationcounter is bumped whenever the document textchanges (
update()) and whenever a background parse swaps in a newer tree(
apply_background_tree()).match_styles_cached()splits the requested range into buffer lines (via the sameropeyLineType::LFAPI already used here), and caches each line'smatch_stylesresult keyed by line-start byte. The whole cache is invalidated in O(1) when
generationchanges, so an edit or a completed background parse transparentlyrefreshes it.
MATCH_CACHE_MAX_LINEScap bounds memory on very large files.styles()now callsmatch_styles_cached()instead ofmatch_styles(). Output isidentical; only repeated work across frames is removed.
Bumping
generationinsideapply_background_tree()is important: the first paint mayuse a partial tree (2ms sync-parse timeout); when the full background tree lands, the
generation bump invalidates the stale partial-tree entries so highlighting is correct.
Result
Profiling a code-editor scroll (macOS
sample): the editor'sWindow::drawcostdropped ~40% (8709 → 5220 samples) with highlighting visually unchanged, including
correct first-screen highlighting after the background parse completes.
Notes