Skip to content

Zero-copy sync for single partial line blocks#586

Open
chall37 wants to merge 2 commits into
gnachman:masterfrom
chall37:worktree-zero-copy-pr
Open

Zero-copy sync for single partial line blocks#586
chall37 wants to merge 2 commits into
gnachman:masterfrom
chall37:worktree-zero-copy-pr

Conversation

@chall37
Copy link
Copy Markdown
Contributor

@chall37 chall37 commented Feb 20, 2026

Evolves the incremental merge from 0ee322c into zero-copy sync for the eligible single-partial-line case. Instead of memcpy'ing deltas on each sync, progenitor and copy share the character buffer across sync cycles; sync updates only CLL and metadata.

The buffer is append-only: progenitor writes past the copy's CLL boundary, and the copy reads only within its current boundary. Merge advances the copy's CLL to include newly appended data, making sync O(1) instead of O(delta).

Non-append mutations (line completion, removal, drop, setPartial) break sharing via existing mutation guards and fall back to COW behavior. Buffer relocation on resize also disables zero-copy and uses the existing fallback path.

Replace incremental merge (O(delta) memcpy per sync) with zero-copy
merge (O(1) metadata-only update). When catting a large file with no
newlines, the progenitor and copy now share the same
iTermCharacterBuffer permanently. On each sync, only the CLL and
metadata are updated — no character data is ever copied.

Key changes:
- Add _zeroCopyShared flag to LineBlock; cowCopy enables it for
  single-partial-line blocks
- validMutationCertificate skips buffer clone when _zeroCopyShared
- Six mutation guards clone the buffer and clear the flag on
  non-append mutations
- canZeroCopyMergeFromProgenitor checks buffer identity, relocation,
  append-only status, and COW ownership state
- zeroCopyMergeFromProgenitor updates CLL + metadata in O(1)
- Resize-in-place fast path moved from LineBlock to LineBuffer to
  preserve append-only eligibility
- iTermCharacterBuffer tracks realloc relocation via wasRelocated
@chall37
Copy link
Copy Markdown
Contributor Author

chall37 commented Feb 20, 2026

Actually, I think we can go even further. The only mutations that actually need buffer isolation are the ones that change the logical view destructively (dropLines, removeLastRawLine, popLastLine), and even those don't modify buffer bytes, just CLL/metadata.

So the split could be:

  • Appends (partial extend, line completion, new line): buffer stays shared, merge copies the CLL delta
  • Destructive (drop, remove, pop): break sharing

That would keep zero-copy alive through line breaks, multi-line output, anything that's fundamentally appending. Which is the overwhelmingly common case during normal terminal output, not just the pathological 50MB-no-newline scenario.

But I think that scope should be its own PR (assuming this one is compelling).

Edit: On second thought, I think actually modifying the existing COW to be append-aware is simpler and avoids all of the complexity of bypassing legacy cow to basically do parallel cow under a different name.

@gnachman
Copy link
Copy Markdown
Owner

If you really want to take it to the limits, you don't need to copy the character buffer until characters would be overwritten. That means dropping from the start of a block is always safe (it just updates offsets, doesn't touch actual characters). Popping from the end is safe until a subsequent append.

But, yes please in another PR :)

Comment thread sources/LineBlock.mm
- (void)changeBufferSize:(int)capacity cert:(id<iTermLineBlockMutationCertificate>)cert {
ITAssertWithMessage(capacity >= [self rawSpaceUsed], @"Truncating used space");
capacity = MAX(1, capacity);
[cert setRawBufferCapacity:capacity];
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to my comment about wasRelocated, this is only safe if we've already cloned the character buffer.

Copy link
Copy Markdown
Contributor Author

@chall37 chall37 Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed as a consequence of removing wasRelocated.

Comment thread sources/LineBlock.mm Outdated
if (partial == is_partial) {
return;
}
if (_zeroCopyShared) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Factor out this if statement, which is repeated in many places.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

factored into -cloneCharacterBufferIfZeroCopyShared

Comment thread sources/LineBlock.mm Outdated
// take gLineBlockMutex. The mutex protects the COW ownership graph
// (cowCopy/validMutationCertificate on the mutation thread); the merge
// path is serialized with mutations at a higher level (screen lock).
ITAssertWithMessage(NSThread.isMainThread, @"Zero-copy merge must run on the main thread");
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should take the lock. I don't believe there is much lock contention (since lock-holding mutations would happen concurrentlyvery rarely while in this code path). Not assuming this runs on the main thread frees us up to use LineBlock across threads in other contexts without limitation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — canZeroCopyMergeFromProgenitor acquires gLineBlockMutex at entry

Comment thread sources/iTermCharacterBuffer.m Outdated
screen_char_t *oldBuffer = _buffer;
_buffer = iTermRealloc(_buffer, newSize, sizeof(screen_char_t));
if (_buffer != oldBuffer) {
_wasRelocated = YES;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think wasRelocated can be done correctly. It's never safe to resize a shared character buffer. You can't guarantee that the other reference isn't in use (for example, search runs concurrenctly in its own thread). The only way to safely realloc is to first make a copy of the character buffer so you know you're only copying your own instance and no other LineBlock could be affected by it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed wasRelocated. The approach is now clone-before-resize in changeBufferSize:cert:

Comment thread sources/LineBlock.mm Outdated
if (!partial) {
if (_zeroCopyShared) {
_characterBuffer = [_characterBuffer clone];
_zeroCopyShared = NO;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's odd that zeroCopyShared remains true in one line block after a copy clones the character buffer and sets _zeroCopyShared=NO. That leads to unnecessary copies:

a = LineBlock()
a.append("partial line")
b = a.cowCopy()
// a and b both have _zeroCopyShared=YES
b.pop() // b copies characterBuffer, resets _zeroCopyShared to NO
a.pop() // a copies characterBuffer, resets _zeroCopyShared to NO

Instead of holding the zeroCopyShared flag, it might make more sense for _characterBuffer to carry a reference count. Prior to making a destructive change, if the refcount > 1, clone it (and all of that should happen atomically).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_zeroCopyShared boolean is completely gone, iTermCharacterBuffer now carries _shareCount and isShared checks _shareCount > 1.

Replace the _zeroCopyShared boolean flag with a reference count in
iTermCharacterBuffer. With a boolean, both sides of a shared pair would
clone the buffer even when only one needed to; with a ref count, once one
side clones and decrements to 1, the other side sees exclusive ownership
and skips the clone.

cloneCharacterBufferIfZeroCopyShared gates on _shareCount > 1. All
mutation sites that must not write to a shared buffer call this before
writing. Resize goes through the same guard (clone-before-realloc) to
prevent use-after-free for concurrent readers holding raw pointers.

Add LineBlockZeroCopyMergeTests (56 tests) covering: eligibility
conditions for zero-copy merge, merge correctness (CLL, metadata,
content, EOL, cache invalidation), shareCount invariants across all
mutation paths, multi-cycle and stress tests, and fallback to standard
merge on non-append mutations.
@chall37 chall37 requested a review from gnachman February 26, 2026 20:47
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