editor: skip code-block re-color rebuild when colors are unchanged (APP-5458) - #15212
Open
warp-agent-staging[bot] wants to merge 2 commits into
Open
editor: skip code-block re-color rebuild when colors are unchanged (APP-5458)#15212warp-agent-staging[bot] wants to merge 2 commits into
warp-agent-staging[bot] wants to merge 2 commits into
Conversation
Buffer::color_code_block_ranges_internal previously rebuilt a code block's entire content into a brand-new SumTree on every re-style pass, even when the requested colors exactly matched what was already applied. This is the common case for NotebookCommand::try_apply_cached_highlighting, which reapplies the same cached colors every time the debounced highlight pass fires while a block's text is unchanged. Each rebuild allocates Arc<Node> SumTree nodes proportional to the block's size, which shows up as a multi-GB memory spike for large pasted code or command output. - Add code_block_colors_match, a read-only check that compares the block's existing color markers against the requested colors and returns true only when they match exactly (including the synthetic marker that closes a color inherited from before the block, and the special case of colors=[] on non-block-aligned offsets). When it matches, color_code_block_ranges_internal returns immediately without touching self.content, matching the existing no-op-delta convention used elsewhere in this file. - When colors do differ and a rebuild is required, batch runs of unchanged text into a single append_str call (via a small String buffer) instead of one append_str(&c.to_string()) call per character, eliminating a per-char heap allocation and reducing the number of SumTree writes from O(chars) to O(color transitions). Both changes preserve the existing loop's marker semantics exactly (the started_colored/is_first_item handling, the style_end - 1 break condition, and the trailing unclosed-range guard are all unchanged). Extended the existing buffer coloring tests with cases for re-styling with identical colors (asserting the no-op path is used), multi-byte UTF-8 content, a code block adjacent to a block-item marker, and a block that inherits an open color from before its own start. Co-Authored-By: Warp <agent@warp.dev>
pending_text in color_code_block_ranges_internal was unbounded: on a genuine re-color with few or no color transitions, it accumulated a whole run of unchanged text into one contiguous String before flushing, staging an allocation proportional to the entire block -- reintroducing an O(block-size) allocation on exactly the large-input case this fix targets. Flush pending_text once it reaches TEXT_FRAGMENT_SIZE bytes (the same chunk size append_str itself splits text into), in addition to the existing flushes at color-marker and non-text-item boundaries. This keeps the batched SumTree writes without ever staging more than a small, fixed amount of text at once. Added test_color_code_block_large_run_without_color_transitions, which colors a ~500-character block with a single color range (no internal marker transitions), exercising several buffer flushes and confirming the resulting content is still correct, both for the initial color and for a no-op re-application. Co-Authored-By: Warp <agent@warp.dev>
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.
Description
Fixes the memory issue in APP-5458: notebook code-block syntax highlighting rebuilt the block's entire content into a brand-new
SumTreeon every re-style pass, even when the requested colors exactly matched what was already applied.NotebookCommand::try_apply_cached_highlightingreapplies the same cached colors every time the debounced highlight pass fires while a block's text is unchanged — this is a common, hot path. Each such call went throughBuffer::color_code_block_ranges_internal, which walked every character in the code block and rebuilt it viaappend_str/pushcalls into a newSumTree, allocatingArc<Node>tree nodes proportional to the block's size. For large pasted code or command output, this produced multi-GB memory spikes (9.75 GB in the reported Sentry event, 73% of it inSumTree::push).Scope of this fix: this addresses the identical-recolor no-op case, which is the dominant real-world trigger for repeated rebuilds (every debounced highlight tick once a block's highlighting has stabilized). A genuine re-color with new colors (e.g. the very first highlight pass on a large pasted block) still rebuilds the whole styled block — that path is optimized for allocation count, not eliminated, as described below. This is a mitigation of the profiled scenario, not a proven elimination of the exact 9.75 GB case; I did not have a way to reproduce or re-profile that specific event to confirm the spike is gone.
Before / after
color_code_block_ranges_internal— including ones where the requested colors were byte-for-byte identical to what was already applied — walked the whole block character by character, callingappend_str(&c.to_string())once per character (a heap allocation per char) andpush()once per color marker transition, producing a brand-newSumTreesubtree sized proportional to the block's character count.Buffer::code_block_colors_match, compares the block's existing color markers against the requestedcolorsbefore doing anything else. If they match exactly (including the synthetic marker that closes a color inherited from before the block), the function returns immediately without touchingself.content— no allocation at all. This is the fast pathtry_apply_cached_highlightinghits on every debounce tick once a block's highlighting has stabilized.Stringbuffer and flushed with oneappend_strcall, instead of oneappend_str(&c.to_string())call (and heap allocation) per character. That buffer is bounded: it's also flushed once it reachesTEXT_FRAGMENT_SIZE(the same chunk sizeappend_stritself splits text into internally), so a long run of unstyled text between transitions can't stage aStringproportional to the whole block before flushing it — otherwise a block with few or no color transitions would still allocate one large contiguous staging copy, reintroducing an O(block-size) allocation on exactly the large-input case this ticket is about. This reduces the number ofSumTreewrites from O(chars) to O(color transitions + block-size /TEXT_FRAGMENT_SIZE).Both changes preserve the original loop's exact marker semantics (the
started_colored/is_first_itemhandling for an inherited color, thestyle_end - 1break condition, and the trailing unclosed-range guard).Safety of the no-op path
The no-op path only returns a
Nonedelta — the same convention already used elsewhere in this file (e.g. theToggleTaskListAtOffsetfallback andapply_core_edit_actions's empty-action case) for edits that are true no-ops. I verifiedupdate_content_with_autoscroll's handling of aNonedelta: it skips the undo push, the content-version bump, and allBufferEventemissions — appropriate here since nothing in the buffer actually changes.NotebookCommand::on_buffer_content_updatedadditionally filtersContentChangedevents toorigin != EditOrigin::SystemEdit, andcolor_code_block_ranges_internalis always called withEditOrigin::SystemEdit, so that specific listener already ignores these events regardless of whether a delta is emitted.The comparison is conservative: any item other than
Text,Newline, orColormarkers within the range (e.g. a link or inline-style marker) makes it bail out and do a real rebuild, since a real rebuild would silently drop such items. It's likewise conservative for malformed/unusualcolorsinputs — zero-width ranges, overlapping ranges, out-of-order ranges, ranges outside the block, etc. all fail to match the expected marker sequence and fall through to the existing rebuild path rather than risking a false match.Linked Issue
Linear: APP-5458
color_code_block_ranges_internalroot cause (not the other facets of Sentry issue 7259255054 tracked separately in APP-5353/APP-5356/APP-4844).Testing
This is a headless, non-visual change to
crates/editor's buffer content model, verified with unit tests (no GUI verification possible or expected here).cargo nextest run -p warp_editor— all 489 tests pass, including 5 new tests added for this change:test_color_code_block_no_op_on_identical_colors— re-applies identical colors and asserts the returned delta isNone(i.e. the no-op fast path is actually taken), then verifies clearing colors is a real, once-applied-then-idempotent change.test_color_code_block_multibyte_utf8— colors a block containing a 2-byte UTF-8 character, verifying byte/char offset handling is unaffected by the batched-text-write change, and that re-applying is a no-op.test_color_code_block_adjacent_to_block_item— colors a code block immediately preceded by a<hr>block-item marker, verifying the no-op path doesn't disturb it.test_color_code_block_started_colored_no_op— a code block that inherits an open color from before its own start (hand-constructed content), verifying the no-op detection correctly reasons about the synthetic closing marker.test_color_code_block_large_run_without_color_transitions— colors a ~500-character block with a single color range spanning the whole block (no internal marker transitions), exercising severalTEXT_FRAGMENT_SIZE-bounded flushes of the staging buffer and confirming correctness for both the initial color and a no-op re-application.test_color_code_block,test_remove_coloring_in_middle_of_block(which exercises re-styling from a non-block-aligned offset withcolors=[]— the edge case that initially broke my first draft of the no-op check),test_edit_colored_code_block, andtest_unstyling_code_block_do_not_leak_syntax_color, all still pass unchanged.notebooks::editortest suite in theappcrate (cargo test -p warp --lib notebooks::editor::, 84 tests) to coverNotebookCommand's syntax-highlighting/undo/redo/cache paths that call into this code — all pass locally, includingtest_syntax_highlighting_in_commandandtest_delete_block_after_highlighted_block(which specifically exercises the cached-highlighting reapplication path). Note: this run has not been independently reproduced in every environment — theappcrate is large and a full-features compile can be killed by a sandbox's memory limit rather than failing on its own merits, so treat this as a strong signal from one full local run rather than a guaranteed-reproducible CI result../script/format --checkandcargo clippy -p warp_editor --all-targets --tests -- -D warningsboth pass clean.I did not measure an actual memory reduction, and I did not reproduce or re-profile the specific 9.75 GB Sentry event (no profiling tooling or the original repro available in this sandbox). The improvement is argued from complexity — no-op case: O(n) reads with zero allocation, replacing O(n) allocations; genuine-rebuild case: O(color transitions + block-size /
TEXT_FRAGMENT_SIZE) bounded writes instead of O(chars) unbounded ones — and confirmed correct via the tests above. This is a mitigation of the profiled repeated-rebuild scenario, not a proven elimination of the exact reported spike../script/runAgent Mode
CHANGELOG-BUG-FIX: Fixed a memory issue where re-highlighting a notebook code block could repeatedly rebuild its entire contents even when nothing changed, causing large memory spikes for big pasted code or command output.