Skip to content

text: Preserve structural sharing when rebuilding the fragment tree - #58681

Merged
osiewicz merged 3 commits into
mainfrom
fragment-builder-structural-sharing
Jun 5, 2026
Merged

text: Preserve structural sharing when rebuilding the fragment tree#58681
osiewicz merged 3 commits into
mainfrom
fragment-builder-structural-sharing

Conversation

@osiewicz

@osiewicz osiewicz commented Jun 5, 2026

Copy link
Copy Markdown
Member

Context

apply_remote_edit and apply_local_edit rebuild the fragment SumTree through FragmentBuilder. Since #51941, the builder collapsed the entire tree into a flat Vec<Fragment> (cloning every fragment) and rebuilt a fresh tree from scratch on every call. The new tree shared no nodes with the previous one, so each edit cost O(N) in the total number of fragments: clone all fragments, allocate an entirely new tree, and then free the whole previous tree on assignment — the last part showing up as a large amount of time spent dropping SumTrees.

That batching was a deliberate win for the bulk replace_all path (#51941, one edit() carrying millions of ranges), but it penalizes every other caller — most notably apply_remote_edit, which runs once per op in a loop in apply_ops and so rebuilt the whole tree per remote operation.

This change

Make FragmentBuilder chunk-based. Appended slices are kept as intact SumTree subtrees, so they keep sharing nodes with the previous tree; only individually pushed fragments are batched into Vecs. to_sum_tree appends the shared subtrees (touching just the right spine) and builds the loose runs in one pass, parallelizing the large ones.

Net effect:

  • Small edits on large/heavily-fragmented buffers (the apply_remote_edit collaborative path) go back to O(edited + log N) with a cheap drop, instead of O(total fragments).
  • The bulk replace_all path keeps its batched build and is not regressed.

Benchmarks

Using the file from #38927 (619 MB CSV, 10,325,246 matches of """), release-fast:

Case flatten (#51941) this PR
replace_all (all matches) 50.70 s 45.16 s
replace 1 match (--single) 16.0 ms 16.0 ms

replace_all is ~11% faster and not regressed. The single-match-on-a-freshly-loaded-file case is unchanged, because a fresh Buffer::local is barely fragmented, so even the from-scratch rebuild is cheap there; the structural-sharing win is on heavily-fragmented buffers receiving small edits.

The --single flag added to editor_benchmarks makes the latter case measurable.

Release Notes:

  • Improved the performance of applying edits to large buffers

osiewicz and others added 2 commits June 5, 2026 18:44
Lets the find/replace benchmark exercise the pessimistic case for a
from-scratch rebuild: a single tiny edit applied to a huge buffer, where
the cost is dominated by how the fragment tree is rebuilt rather than by
the edit itself.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
apply_remote_edit and apply_local_edit rebuild the fragment SumTree
through FragmentBuilder. Since #51941 the builder collapsed the entire
tree into a flat Vec<Fragment> (cloning every fragment) and rebuilt a
fresh tree from scratch on every call. The new tree shared no nodes with
the old one, so each edit cost O(N) in the total number of fragments:
clone all fragments, allocate an entirely new tree, and then free the
whole previous tree on assignment (the latter showing up as a large
amount of time spent dropping SumTrees).

Make FragmentBuilder chunk-based instead: appended slices are kept as
intact SumTree subtrees so they keep sharing nodes with the previous
tree, and only individually pushed fragments are batched into Vecs.
to_sum_tree appends the shared subtrees (touching just the right spine)
and builds the loose runs in one pass, parallelizing large ones. Small
edits on large buffers are back to O(edited + log N) with a cheap drop,
and the bulk replace_all path is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cla-bot cla-bot Bot added the cla-signed The user has signed the Contributor License Agreement label Jun 5, 2026
@zed-community-bot zed-community-bot Bot added the staff Pull requests authored by a current member of Zed staff label Jun 5, 2026
@osiewicz
osiewicz requested a review from Veykril June 5, 2026 17:13
@osiewicz
osiewicz added this pull request to the merge queue Jun 5, 2026
Merged via the queue into main with commit ab2683b Jun 5, 2026
32 checks passed
@osiewicz
osiewicz deleted the fragment-builder-structural-sharing branch June 5, 2026 19:27
@Veykril

Veykril commented Jun 6, 2026

Copy link
Copy Markdown
Member

@zed-zippy approve

TomPlanche pushed a commit to TomPlanche/zed that referenced this pull request Jun 8, 2026
…ed-industries#58681)

## Context

`apply_remote_edit` and `apply_local_edit` rebuild the fragment
`SumTree` through `FragmentBuilder`. Since zed-industries#51941, the builder collapsed
the entire tree into a flat `Vec<Fragment>` (cloning every fragment) and
rebuilt a fresh tree from scratch on every call. The new tree shared no
nodes with the previous one, so each edit cost O(N) in the *total*
number of fragments: clone all fragments, allocate an entirely new tree,
and then free the whole previous tree on assignment — the last part
showing up as a large amount of time spent dropping `SumTree`s.

That batching was a deliberate win for the bulk `replace_all` path
(zed-industries#51941, one `edit()` carrying millions of ranges), but it penalizes
every other caller — most notably `apply_remote_edit`, which runs once
per op in a loop in `apply_ops` and so rebuilt the whole tree per remote
operation.

## This change

Make `FragmentBuilder` chunk-based. Appended slices are kept as intact
`SumTree` subtrees, so they keep sharing nodes with the previous tree;
only individually pushed fragments are batched into `Vec`s.
`to_sum_tree` appends the shared subtrees (touching just the right
spine) and builds the loose runs in one pass, parallelizing the large
ones.

Net effect:
- Small edits on large/heavily-fragmented buffers (the
`apply_remote_edit` collaborative path) go back to O(edited + log N)
with a cheap drop, instead of O(total fragments).
- The bulk `replace_all` path keeps its batched build and is not
regressed.

## Benchmarks

Using the file from zed-industries#38927 (619 MB CSV, 10,325,246 matches of `"` →
`""`), `release-fast`:

| Case | flatten (zed-industries#51941) | this PR |
|---|---|---|
| `replace_all` (all matches) | 50.70 s | 45.16 s |
| replace 1 match (`--single`) | 16.0 ms | 16.0 ms |

`replace_all` is ~11% faster and not regressed. The
single-match-on-a-freshly-loaded-file case is unchanged, because a fresh
`Buffer::local` is barely fragmented, so even the from-scratch rebuild
is cheap there; the structural-sharing win is on heavily-fragmented
buffers receiving small edits.

The `--single` flag added to `editor_benchmarks` makes the latter case
measurable.

Release Notes:

- Improved the performance of applying edits to large buffers

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 18, 2026
jonx pushed a commit to jonx/zed-aros that referenced this pull request Jul 17, 2026
…ed-industries#58681)

## Context

`apply_remote_edit` and `apply_local_edit` rebuild the fragment
`SumTree` through `FragmentBuilder`. Since zed-industries#51941, the builder collapsed
the entire tree into a flat `Vec<Fragment>` (cloning every fragment) and
rebuilt a fresh tree from scratch on every call. The new tree shared no
nodes with the previous one, so each edit cost O(N) in the *total*
number of fragments: clone all fragments, allocate an entirely new tree,
and then free the whole previous tree on assignment — the last part
showing up as a large amount of time spent dropping `SumTree`s.

That batching was a deliberate win for the bulk `replace_all` path
(zed-industries#51941, one `edit()` carrying millions of ranges), but it penalizes
every other caller — most notably `apply_remote_edit`, which runs once
per op in a loop in `apply_ops` and so rebuilt the whole tree per remote
operation.

## This change

Make `FragmentBuilder` chunk-based. Appended slices are kept as intact
`SumTree` subtrees, so they keep sharing nodes with the previous tree;
only individually pushed fragments are batched into `Vec`s.
`to_sum_tree` appends the shared subtrees (touching just the right
spine) and builds the loose runs in one pass, parallelizing the large
ones.

Net effect:
- Small edits on large/heavily-fragmented buffers (the
`apply_remote_edit` collaborative path) go back to O(edited + log N)
with a cheap drop, instead of O(total fragments).
- The bulk `replace_all` path keeps its batched build and is not
regressed.

## Benchmarks

Using the file from zed-industries#38927 (619 MB CSV, 10,325,246 matches of `"` →
`""`), `release-fast`:

| Case | flatten (zed-industries#51941) | this PR |
|---|---|---|
| `replace_all` (all matches) | 50.70 s | 45.16 s |
| replace 1 match (`--single`) | 16.0 ms | 16.0 ms |

`replace_all` is ~11% faster and not regressed. The
single-match-on-a-freshly-loaded-file case is unchanged, because a fresh
`Buffer::local` is barely fragmented, so even the from-scratch rebuild
is cheap there; the structural-sharing win is on heavily-fragmented
buffers receiving small edits.

The `--single` flag added to `editor_benchmarks` makes the latter case
measurable.

Release Notes:

- Improved the performance of applying edits to large buffers

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jolutz pushed a commit to jolutz/zed that referenced this pull request Aug 8, 2026
…ed-industries#58681)

## Context

`apply_remote_edit` and `apply_local_edit` rebuild the fragment
`SumTree` through `FragmentBuilder`. Since zed-industries#51941, the builder collapsed
the entire tree into a flat `Vec<Fragment>` (cloning every fragment) and
rebuilt a fresh tree from scratch on every call. The new tree shared no
nodes with the previous one, so each edit cost O(N) in the *total*
number of fragments: clone all fragments, allocate an entirely new tree,
and then free the whole previous tree on assignment — the last part
showing up as a large amount of time spent dropping `SumTree`s.

That batching was a deliberate win for the bulk `replace_all` path
(zed-industries#51941, one `edit()` carrying millions of ranges), but it penalizes
every other caller — most notably `apply_remote_edit`, which runs once
per op in a loop in `apply_ops` and so rebuilt the whole tree per remote
operation.

## This change

Make `FragmentBuilder` chunk-based. Appended slices are kept as intact
`SumTree` subtrees, so they keep sharing nodes with the previous tree;
only individually pushed fragments are batched into `Vec`s.
`to_sum_tree` appends the shared subtrees (touching just the right
spine) and builds the loose runs in one pass, parallelizing the large
ones.

Net effect:
- Small edits on large/heavily-fragmented buffers (the
`apply_remote_edit` collaborative path) go back to O(edited + log N)
with a cheap drop, instead of O(total fragments).
- The bulk `replace_all` path keeps its batched build and is not
regressed.

## Benchmarks

Using the file from zed-industries#38927 (619 MB CSV, 10,325,246 matches of `"` →
`""`), `release-fast`:

| Case | flatten (zed-industries#51941) | this PR |
|---|---|---|
| `replace_all` (all matches) | 50.70 s | 45.16 s |
| replace 1 match (`--single`) | 16.0 ms | 16.0 ms |

`replace_all` is ~11% faster and not regressed. The
single-match-on-a-freshly-loaded-file case is unchanged, because a fresh
`Buffer::local` is barely fragmented, so even the from-scratch rebuild
is cheap there; the structural-sharing win is on heavily-fragmented
buffers receiving small edits.

The `--single` flag added to `editor_benchmarks` makes the latter case
measurable.

Release Notes:

- Improved the performance of applying edits to large buffers

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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