Skip to content

agent: Batch streaming edit operations#58037

Merged
bennetbo merged 2 commits into
mainfrom
batch-streaming-edits
May 29, 2026
Merged

agent: Batch streaming edit operations#58037
bennetbo merged 2 commits into
mainfrom
batch-streaming-edits

Conversation

@Anthony-Eid

@Anthony-Eid Anthony-Eid commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

While profiling agent sessions that make a lot of edit_file operations, I noticed the LSP textDocument/didChange handler firing excessively. Looking into this, I found out that the streaming edit pipeline was applying each CharOperation from StreamingDiff as its own buffer.edit transaction, and every transaction emits a BufferEvent::Edited event. Each event can trigger several other expensive events depending on whether the buffer is being rendered in an editor or is registered with a language.

For example, there are didChange LSP events, the editor's on edit work (matching brackets, bracket colorization, code actions, outline), and more. A single edit_file could trigger hundreds of these at the higher end in a single synchronous app update, which would block the foreground thread for a bit and cause Zed to drop frames.

I fixed this by collecting all of a chunk's CharOperations and applying them in one buffer.edit call, so only a single BufferEvent::Edited event gets emitted. This is safe because operations are non overlapping by design of streaming diff (the edit cursor only advances).

Why this wasn't caught earlier

The cost only fully appears when a buffer is both registered with a language server and rendered in an editor. Without that, most of the per transaction observers never run, so the existing edit_file_tool benchmark (which ran the tool against a bare buffer) didn't surface it. I reworked the benchmark to open the edited buffer in an editor view, register a fake language server with per edit diagnostics, and lay out a frame, so it exercises the same cascade as the real editor. I also added a larger fixture.

Results

Measured with the release-fast profile on the reworked benchmark:

Fixture Initial file Before After Improvement
tiny_function_rewrite 1.4 KB 31.1 ms 12.1 ms −61%
small_function_rewrite 3.0 KB 42.4 ms 19.3 ms −55%
medium_many_small_changes 4.6 KB 309.2 ms 151.5 ms −51%
medium_insertions 4.6 KB 171.8 ms 126.1 ms −27%
large_multi_edit 44 KB 9,549 ms 919 ms −90%

Self-Review Checklist:

  • I've reviewed my own diff for quality, security, and reliability
  • Unsafe blocks (if any) have justifying comments
  • The content is consistent with the UI/UX checklist
  • Tests cover the new/changed behavior
  • Performance impact has been considered and is acceptable

Release Notes:

  • Improved agent's edit file tool performance

@cla-bot cla-bot Bot added the cla-signed The user has signed the Contributor License Agreement label May 29, 2026
@zed-community-bot zed-community-bot Bot added the staff Pull requests authored by a current member of Zed staff label May 29, 2026
@bennetbo bennetbo added this pull request to the merge queue May 29, 2026
Merged via the queue into main with commit 6bca213 May 29, 2026
35 checks passed
@bennetbo bennetbo deleted the batch-streaming-edits branch May 29, 2026 09:53
kylekz pushed a commit to kylekz/zed that referenced this pull request May 29, 2026
Sync fork up to the latest zed-industries/zed upstream (6bca213, zed-industries#58037),
on top of the existing partial sync already on main (which reached zed-industries#57456).

Resolved conflicts:
- .gitignore: kept both the fork (/claude-code-ext/) and upstream
  (.nixos-test-history) entries.
- crates/proto/proto/zed.proto: integrated upstream Envelope fields 453-456
  (document links) alongside the fork's reserved claude-code-ide range
  (10000-10004).

Cargo.lock reconciled with cargo so it matches upstream's pins plus the
fork's claude_code_ide / claude_code_ide_server dependencies; verified with
`cargo metadata --locked`.
TomPlanche pushed a commit to TomPlanche/zed that referenced this pull request Jun 2, 2026
## Summary

While profiling agent sessions that make a lot of `edit_file`
operations, I noticed the LSP `textDocument/didChange` handler firing
excessively. Looking into this, I found out that the streaming edit
pipeline was applying each `CharOperation` from `StreamingDiff` as its
own `buffer.edit` transaction, and every transaction emits a
`BufferEvent::Edited` event. Each event can trigger several other
expensive events depending on whether the buffer is being rendered in an
editor or is registered with a language.

For example, there are `didChange` LSP events, the editor's on edit work
(matching brackets, bracket colorization, code actions, outline), and
more. A single `edit_file` could trigger hundreds of these at the higher
end in a single synchronous app update, which would block the foreground
thread for a bit and cause Zed to drop frames.

I fixed this by collecting all of a chunk's `CharOperation`s and
applying them in one `buffer.edit` call, so only a single
`BufferEvent::Edited` event gets emitted. This is safe because
operations are non overlapping by design of streaming diff (the edit
cursor only advances).

## Why this wasn't caught earlier

The cost only fully appears when a buffer is both registered with a
language server and rendered in an editor. Without that, most of the per
transaction observers never run, so the existing `edit_file_tool`
benchmark (which ran the tool against a bare buffer) didn't surface it.
I reworked the benchmark to open the edited buffer in an editor view,
register a fake language server with per edit diagnostics, and lay out a
frame, so it exercises the same cascade as the real editor. I also added
a larger fixture.

## Results

Measured with the `release-fast` profile on the reworked benchmark:

| Fixture | Initial file | Before | After | Improvement |
| --- | --- | --- | --- | --- |
| `tiny_function_rewrite` | 1.4 KB | 31.1 ms | 12.1 ms | −61% |
| `small_function_rewrite` | 3.0 KB | 42.4 ms | 19.3 ms | −55% |
| `medium_many_small_changes` | 4.6 KB | 309.2 ms | 151.5 ms | −51% |
| `medium_insertions` | 4.6 KB | 171.8 ms | 126.1 ms | −27% |
| `large_multi_edit` | 44 KB | 9,549 ms | 919 ms | −90% |

Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the UI/UX checklist
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:
- Improved agent's edit file tool performance
kylekz pushed a commit to kylekz/zed that referenced this pull request Jun 5, 2026
Sync fork up to the latest zed-industries/zed upstream (8ce658f, zed-industries#58579),
on top of the previous sync that reached 6bca213 (zed-industries#58037).

Resolved conflicts:
- crates/zed/src/main.rs: dropped the standalone `git_graph::init(cx)` call
  because upstream folded the `git_graph` crate into `git_ui` (the
  `git_ui::init` on the preceding line now invokes `git_graph::init`
  internally). Kept the fork-only `claude_code_ide::init(cx)` call.

Other fork-specific touch points (workspace `Cargo.toml`, `.gitignore`
`/claude-code-ext/` entry, `crates/proto/proto/zed.proto` reserved range
10000-10004, `crates/proto/src/proto.rs`, `crates/project/src/project.rs`,
`crates/project/src/terminals.rs`, `crates/remote_server/src/headless_project.rs`,
`crates/zed/Cargo.toml`, etc.) auto-merged cleanly against upstream's
changes; verified the fork additions (claude_code_ide crates,
claude_code_ide_dispatcher modules, script/build-fork.ps1, README header,
gpui_tokio shim) are intact.

Cargo.lock auto-merged; verified with `cargo metadata --locked`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement staff Pull requests authored by a current member of Zed staff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants