Skip to content

Optimize BufferLine.copyFrom sparse map copying#6005

Open
Tyriar wants to merge 6 commits into
masterfrom
cursor/optimize-bufferline-copyfrom-71e1
Open

Optimize BufferLine.copyFrom sparse map copying#6005
Tyriar wants to merge 6 commits into
masterfrom
cursor/optimize-bufferline-copyfrom-71e1

Conversation

@Tyriar

@Tyriar Tyriar commented Jun 6, 2026

Copy link
Copy Markdown
Member

Summary

Optimizes BufferLine.copyFrom for the production scroll-recycle path (BufferService.scrollrecycle().copyFrom(cachedBlankLine)) and for lines with combining characters / extended attributes.

Changes

  • _copySparseMapsFrom
    • Empty-map fast path: Shared EMPTY_SPARSE_MAP sentinel (Object.create(null)) with copy-on-write for _combined. Empty _extendedAttrs uses undefined (addon-image writes this map directly without BufferLine APIs).
    • Populated-map path: Inlined _data flag scan instead of per-column _copyCellMapsFrom calls (fewer function call overheads than master).
  • addons/addon-image: Fork _extendedAttrs with ??= {} before writes; optional chaining on reads (fixes shared-map regression).
  • test/benchmark/BufferLine.benchmark.ts: Micro-benchmarks for scroll recycle and sparse-source copyFrom.

copyFrom is only called from BufferService.scroll when the circular buffer is full.

Micro-benchmark results

500,000 copyFrom operations per case, 5 runs, average runtime (npm run benchmark -- -s "out-test/benchmark/BufferLine.benchmark.js" out-test/benchmark/BufferLine.benchmark.js). Benchmark file from this PR; only BufferLine.ts swapped per revision. Measured at dec5452fc.

Three-way comparison (ddf2b7d vs master vs this PR)

ddf2b7d is the last merge on master before #5972 (fix(buffer): clear stale attrs and combined data in BufferLine), which replaced copyFrom's key-only map copy with an O(cols) _copySparseMapsFrom scan.

Case ddf2b7d (pre-#5972) master This PR PR vs master PR vs ddf2b7d
Scroll recycle, cols=80 15.99 ms 62.43 ms 6.79 ms −89.1% (9.2×) −57.5% (2.4×)
Scroll recycle, cols=279 22.34 ms 203.22 ms 19.20 ms −90.6% (10.6×) −14.1% (1.2×)
Sparse source, cols=80 611.51 ms 337.19 ms 317.44 ms −5.9% (1.1×) −48.1% (1.9×)
Sparse source, cols=279 1539.39 ms 942.76 ms 863.35 ms −8.4% (1.1×) −43.9% (1.8×)

What each revision does in copyFrom

Revision Empty-map path (yes / scroll recycle) Sparse-map path
ddf2b7d for…in over empty _combined / _extendedAttrs + 2× {} alloc for…in over map keys only
master (#5972) O(cols) _copyCellMapsFrom loop every scroll Same O(cols) loop
This PR _combined sentinel identity check (O(1)); _extendedAttrs = undefined Inlined O(cols) flag scan (no per-cell function calls)

Takeaways

CDP results (timeout 1s yes on demo terminal)

Chromium CDP CPU profile, cols=140, rows=38, scrollback=1000, ~2.76s sample window. Attributed time for copy-path symbols (note: V8 often does not nest copyFrom with _copyCellMapsFrom on the same stack frame, so compare the combined copy-path total).

Symbol master total ms master % PR total ms PR %
copyFrom 46.70 1.69% 62.57 2.26%
_copySparseMapsFrom 68.36 2.47% 44.33 1.60%
_copyCellMapsFrom 310.02 11.21% 0 0%
Copy-path combined 425.08 15.38% 106.90 3.87%

Copy-path combined time: −75% vs master. _copyCellMapsFrom samples eliminated on the blank-line scroll path.

Design notes (alternatives explored)

  • undefined for _combined: Regressed sparse copyFrom in benchmarks; not used for _combined.
  • undefined for _extendedAttrs: Adopted (addon-image writes maps directly; shared sentinel caused integration test failures).
  • Object.freeze on sentinels: Safe with copy-on-write but no measurable perf gain; not adopted.

Testing

  • npm run test-unit -- **/out-esbuild/**/BufferLine.test.js — 51 passing
  • npm run test-integration -- --suite=addon-image — 209 passing
  • npm run lint-changes
Open in Web Open in Cursor 

cursoragent and others added 3 commits June 4, 2026 21:05
Skip O(cols) _copyCellMapsFrom loop when the source line has no sparse
combined/extended map entries (scroll recycle from cached blank line).

Copy sparse entries by map keys only when needed. Add CDP profiling
script and BufferLine.copyFrom micro-benchmark.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
Use shared empty-map sentinels with copy-on-write for scroll recycling,
and an inlined _data flag scan for populated sparse maps (faster than
per-cell function calls on master). Avoid slow Object.assign map copies.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
@Tyriar

Tyriar commented Jun 6, 2026

Copy link
Copy Markdown
Member Author

Testing running yes for a couple of seconds, see the total time % for copyFrom.

Before:

Screenshot 2026-06-06 at 9 32 19 AM

After:

Screenshot 2026-06-06 at 9 33 40 AM

@Tyriar

Tyriar commented Jun 6, 2026

Copy link
Copy Markdown
Member Author

Before on ddf2b7d (before the correctness changes in b3de9fa:

Screenshot 2026-06-06 at 9 37 49 AM

@Tyriar Tyriar marked this pull request as ready for review June 6, 2026 16:58
@Tyriar Tyriar added this to the 7.0.0 milestone Jun 6, 2026
@Tyriar Tyriar enabled auto-merge (squash) June 6, 2026 16:58
@Tyriar Tyriar disabled auto-merge June 6, 2026 21:47
EMPTY_SPARSE_EXTENDED was shared across lines via copyFrom; addon-image
writes _extendedAttrs directly without copy-on-write, which caused tile
eviction failures and broke instanceof Object checks.

Use undefined for empty extended maps (keep EMPTY_SPARSE_MAP for _combined).
Ensure addon-image forks the map before writes and update the accessor test.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
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.

2 participants