Skip to content

Add batching to remote editor sync#10772

Open
kevinyang372 wants to merge 4 commits into
masterfrom
kevin/add-batching-to-remote-edit-sync
Open

Add batching to remote editor sync#10772
kevinyang372 wants to merge 4 commits into
masterfrom
kevin/add-batching-to-remote-edit-sync

Conversation

@kevinyang372
Copy link
Copy Markdown
Member

@kevinyang372 kevinyang372 commented May 12, 2026

Batch Client Buffer Edits with Debouncing

Problem

Every user keystroke in a remote file editor immediately sends a BufferEdit proto message to the remote server daemon. Rapid typing floods the SSH transport with many small messages, each with fixed overhead (proto encoding, length prefix, SSH framing).

Solution

Add a PendingEditBatch to BufferSource::Remote that accumulates edits during a 200ms debounce window, then sends them as a single BufferEdit message. Key design points:

  • sync_clock.client_version is bumped immediately on each keystroke (not deferred), so conflict detection works correctly even before the batch is flushed
  • Discard on conflict: if a server push arrives while we have unflushed edits, the batch is discarded (not flushed) — flushing would be pointless since the server's S has moved past our batch's S_expected
  • Flush before save: pending edits are flushed before SaveBuffer so the server has the latest content
  • Discard on OpenBuffer response: fresh content from the server makes any pending batch stale

Changes

  • PendingEditBatch struct with flush(self, client, path) and discard(self) methods
  • push_edit_to_pending_batch on GlobalBufferModel handles accumulation + CV bump + timer cancellation
  • Debounce timer uses Timer::after + abort_handle pattern (same as LanguageServerShutdownManager)
  • Batch discard wired into conflict detection (handle_buffer_conflict_detected, handle_buffer_updated_push conflict branch, apply_open_buffer_response)
  • Batch flush wired into save path (GlobalBufferModel::save for remote buffers)
  • 4 new tests in global_buffer_model_tests.rs covering discard-on-push, discard-on-conflict, clean-push, and immediate CV bump
  • All 20 existing + new tests pass

Version Vector Correctness

The batched BufferEdit carries a single S_expected (captured when the first edit in the batch arrives) and the final C_new. The server only cares about the latest accepted C, so skipping intermediate values is correct. Server-side apply_client_edit already accepts repeated TextEdit, so no server changes needed.


Oz conversation | Plan

@cla-bot cla-bot Bot added the cla-signed label May 12, 2026
Copy link
Copy Markdown
Member Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@kevinyang372 kevinyang372 marked this pull request as ready for review May 12, 2026 23:39
@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 12, 2026

@kevinyang372

I'm starting a first review of this pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

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

Overview

This PR batches remote editor buffer edits behind a debounce timer and flushes pending edits before remote saves.

Concerns

  • Pending edits are removed from the batch and sent with a best-effort notification before save; if the notification cannot be enqueued, the following SaveBuffer request can still persist stale server content.

Verdict

Found: 0 critical, 1 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

// Flush any pending edit batch so the server has the latest
// content before persisting to disk.
if let Some(batch) = pending_batch.take() {
batch.flush(&client, &path);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] send_buffer_edit is best-effort (try_send), so this can drop the only pending batch and still issue SaveBuffer, letting save succeed with stale server content; make the flush fallible/awaited before taking the batch or sending the save request.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i think this is reasonable

@kevinyang372 kevinyang372 requested a review from moirahuang May 12, 2026 23:55
if let Some(timer) = &self.debounce_timer {
timer.abort();
}
log::debug!(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit nit: should this log::debug be inside the if so it only logs if we actually aborted

// Flush any pending edit batch so the server has the latest
// content before persisting to disk.
if let Some(batch) = pending_batch.take() {
batch.flush(&client, &path);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i think this is reasonable

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants