Optimize forEachDecorationAtCell by maintaining cache against buffer line index#5902
Merged
Conversation
Refs xtermjs#5176. forEachDecorationAtCell and getDecorationsAtCell previously scanned every registered decoration on each call. With thousands of search highlights this becomes O(total decorations) per cell and blocks rendering during viewport refreshes. Maintain a Map of buffer line -> decorations that cover that line. Multi-line decorations are indexed on every line in their span, so mixed single-line and multi-line workloads stay fast without falling back to a full scan. Keep the index aligned with buffer mutations: - Register and unregister decorations in the line buckets on add/dispose. - On trim, shift line keys synchronously (before marker handlers run). - On insert/delete, re-index affected decorations in a microtask after marker line updates are applied. Inject IBufferService so line listeners follow the active buffer. Add unit tests for trim, dispose-on-trim, insert, and dense single-line plus multi-line lookup. Add DecorationService.benchmark.ts to measure sparse hits and viewport-sized grid scans with 20k decorations. Co-authored-by: Cursor <cursoragent@cursor.com>
Extract the per-line lookup index from DecorationService into an exported DecorationLineCache class in the same module, with DecorationService listed first for readability. Optimize buffer insert/delete maintenance to shift map keys in O(unique indexed lines) instead of full re-indexing every affected decoration. Only multi-line decorations that span the mutation point get a full re-index. Add a direct DecorationLineCache unit test in DecorationService.test.ts. Co-authored-by: Cursor <cursoragent@cursor.com>
Introduce MicrotaskTimer as a TimeoutTimer-style helper that schedules a single runner via queueMicrotask, with set and cancel but no cancelAndSet. Use it in DecorationLineCache to batch insert/delete line-index updates after marker line adjustments, replacing ad-hoc queueMicrotask scheduling. Co-authored-by: Cursor <cursoragent@cursor.com>
This was referenced May 26, 2026
Closed
Member
Author
|
@anthonykim1 big improvement to a long standing issue btw |
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.
Summary
Speeds up
DecorationService.forEachDecorationAtCellandgetDecorationsAtCellby indexing decorations per logical buffer line (via marker coordinates), so render-time lookup scales with decorations on the queried line instead of the total decoration count. Addresses renderer blocking when search (and similar features) register very large numbers of highlights.Resolves #5176
Refs #3809, #5443, microsoft/vscode#230365
Problem
forEachDecorationAtCellpreviously iterated every registered decoration on each call. The DOM renderer invokes it per cell during row refresh, so cost is roughly:O(viewport cells × total decorations)With thousands of search highlights (and higher highlight limits in clients like VS Code), a single viewport refresh can scan the full decoration set tens of thousands of times and noticeably block the main thread. This is the bottleneck called out in #5176.
Prior approaches (e.g. #5886) used a fast path only when no multi-line decorations exist; a single multi-line decoration forced a full scan again.
Related issues this does not fully resolve:
SortedList/ GC)Solution
Per-line index (
DecorationLineCache)DecorationLineCacheinsrc/common/services/DecorationService.tsmaintainsMap<logicalLine, decorations[]>.marker.line(stable logical buffer coordinates), notCircularListring slots.height > 1) are stored in every line bucket they span, so mixed single-line + multi-line workloads stay on the fast path.DecorationServicedelegates add/remove/lookup to the cache; the existingSortedListremains for sorted iteration elsewhere.Buffer line mutations
The cache listens to active buffer
trim/insert/deleteevents (viaIBufferService) and keeps indexes aligned with marker movement:_indexedStartLineInsert/delete maintenance is O(unique indexed lines) for key shifts plus a light pass over decorations, not a full remove/re-add per decoration.
MicrotaskTimerMicrotaskTimerinsrc/common/Async.tsmirrorsTimeoutTimerergonomics (set,cancel,dispose) but schedules viaqueueMicrotask.DecorationLineCacheuses it to batch multiple insert/delete sync callbacks into one microtask after marker line updates.Internal API
IInternalDecoration._indexedStartLinesupports bucket removal whenmarker.lineis cleared on dispose.Benchmarks
New
test/benchmark/DecorationService.benchmark.ts:Run benchmarks:
Results:
Manual perf profile
Test case:
ls -lR .4 times to fill scrollbackBefore:
After:
Tests
Extended
src/common/services/DecorationService.test.ts:splice)DecorationLineCachesmoke testCommits on this branch (vs
master)IBufferServicewiring, tests, benchmarkTest plan
npm run build && npm run esbuildnpm run test-unit -- out-esbuild/common/services/DecorationService.test.jsnpm run benchmark -- -s "out-test/benchmark/DecorationService.benchmark.js|DecorationService.forEachDecorationAtCell" out-test/benchmark/DecorationService.benchmark.jsnpm run lint-changesheight > 1still highlights all spanned rowsNotes
forEachDecorationAtCellAPI used by DOM/WebGL render paths today.