Files
zed/crates/benchmarks/benches/editor_render.rs
T
RaniandAnthony Eid a41ccd1206 editor: Speed up multi cursor editing (#58510)
Early draft for #32051 (multi-cursor editing is very slow, and basically
hangs at high cursor counts). Opening it early like @Anthony-Eid
suggested so we can agree on direction before I go further.

## Root cause

Typing with a cursor on every line is mostly O(N)-per-keystroke work
spread across a few places. I profiled it with `sample` and per-phase
timers around `handle_input`. At 1000 cursors (~24ms before this PR):
the post-edit display-map sync is ~11.8ms, the CRDT edit
(`apply_local_edit`) ~3.7ms, resolving all N selections plus the
per-cursor input loop ~3.5ms, the post-edit selection round-trip ~2.6ms,
and `change_selections` plus transact machinery ~2.4ms. The display sync
re-runs per-edit `SumTree` work through every layer even when nothing
transforms (the plain-text case), and selections get re-resolved through
the full display round-trip several times per keystroke.

## What this does

- Display fast paths: `InlayMap::sync` (no inlays) and
`WrapMap::interpolate` (no soft-wrap) skip the O(edits) transform-tree
rebuild and return the passthrough snapshot, gated so a pending inlay
splice or existing wraps still take the slow path.
- Selection fast path: when nothing collapses buffer content
(`!has_folds() && !has_replacement_blocks()`), resolve `Anchor` to
`Point` to `Offset` batched and skip the per-selection display
round-trip (the `todo(lw)`).
- Render and autoscroll resolve only the first/last/newest selections
instead of all N per frame.
- A parameterized `Multi-cursor input/cursors/{1000,10000,100000}`
benchmark.

## Results

- Typing (`handle_input`): ~2.1x faster (24ms to 11.7ms at 1k).
- Type plus two word-deletes (criterion): 29% faster (89.9 to 63.8ms) at
1k, 37% (959 to 606ms) at 10k.
- Post-edit display sync alone: 11.8 to 3.8ms at 1k, 133 to 45ms at 10k.

All `editor` (759), `display_map` (67), `multi_buffer` (58), and `text`
(36) tests pass, and `./script/clippy` is clean.

## Direction

This is ~2x, and I can get the current architecture to roughly 3.5-4.5x
with a few more safe changes (hoisting the per-cursor
language/editability checks, cheaper snapshot clones). Genuine VS Code
numbers (~1-2us/cursor) aren't reachable while the buffer is a CRDT rope
of fragments with anchor selections and a 5-layer transform stack. That
would need a plain-offset cursor model and/or decoupling display layout
from the edit path, which is a bigger effort I'd want to design with
you. One thing I'd need your call on: `set_active_selections` sends an
`UpdateSelections` collab op every keystroke (building N anchors); can
that be debounced to transaction end, or do presence/follow-mode
features rely on per-keystroke cursor broadcast?

Release Notes:

- editor: Improved multi cursor editing performance

---------

Co-authored-by: Anthony Eid <anthony@zed.dev>
2026-06-17 07:57:05 +00:00

152 lines
4.6 KiB
Rust

use editor::{
Editor, EditorMode, MultiBuffer,
actions::{DeleteToPreviousWordStart, SelectAll, SplitSelectionIntoLines},
};
use gpui::{AppContext as _, BenchAppContext, Focusable as _};
use rand::{Rng as _, SeedableRng as _, rngs::StdRng};
use settings::SettingsStore;
use util::RandomCharIter;
use zed_actions::editor::{MoveDown, MoveUp};
#[gpui::bench(
inputs = multi_cursor_line_counts(),
group = "Multi-cursor input",
input_name = "cursors",
sample_size = 10
)]
fn editor_multi_cursor_input(line_count: &usize, cx: &mut BenchAppContext) {
init_context(cx);
let text = "line:\n".repeat(*line_count);
let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
let mut window = cx.add_empty_window();
let editor = window.update(|window, cx| {
let editor = cx.new(|cx| {
let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx);
editor.set_style(editor::EditorStyle::default(), window, cx);
editor.select_all(&SelectAll, window, cx);
editor.split_selection_into_lines(
&SplitSelectionIntoLines {
keep_selections: true,
},
window,
cx,
);
editor
});
window.focus(&editor.focus_handle(cx), cx);
editor
});
cx.bench_iter(|_| {
window.update(|window, cx| {
editor.update(cx, |editor, cx| {
editor.handle_input("hello world", window, cx);
editor.delete_to_previous_word_start(
&DeleteToPreviousWordStart {
ignore_newlines: false,
ignore_brackets: false,
},
window,
cx,
);
editor.delete_to_previous_word_start(
&DeleteToPreviousWordStart {
ignore_newlines: false,
ignore_brackets: false,
},
window,
cx,
);
});
})
});
}
#[gpui::bench]
fn open_editor_with_one_long_line(cx: &mut BenchAppContext) {
init_context(cx);
let text = String::from_iter(["char"; 1000]);
cx.bench_iter(move |cx| {
let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
let mut window = cx.add_empty_window();
window.update(|window, cx| {
let editor = cx.new(|cx| {
let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx);
editor.set_style(editor::EditorStyle::default(), window, cx);
editor
});
window.focus(&editor.focus_handle(cx), cx);
editor
});
});
}
#[gpui::bench]
fn editor_render(cx: &mut BenchAppContext) {
init_context(cx);
let buffer = cx.update(|cx| {
let mut rng = StdRng::seed_from_u64(1);
let text_len = rng.random_range(10000..90000);
if rng.random() {
let text = RandomCharIter::new(&mut rng)
.take(text_len)
.collect::<String>();
MultiBuffer::build_simple(&text, cx)
} else {
MultiBuffer::build_random(&mut rng, cx)
}
});
let mut window = cx.add_empty_window();
let editor = window.update(|window, cx| {
let editor = window.replace_root(cx, |window, cx| {
let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx);
editor.set_style(editor::EditorStyle::default(), window, cx);
editor
});
window.focus(&editor.focus_handle(cx), cx);
editor
});
let mut move_down = true;
cx.bench_renderer(editor, move |editor, window, cx| {
if move_down {
editor.move_down(&MoveDown, window, cx);
} else {
editor.move_up(&MoveUp, window, cx);
}
move_down = !move_down;
});
}
fn init_context(cx: &mut BenchAppContext) {
cx.update(|cx| {
let store = SettingsStore::test(cx);
cx.set_global(store);
assets::Assets.load_test_fonts(cx);
theme_settings::init(theme::LoadThemes::JustBase, cx);
editor::init(cx);
});
}
fn multi_cursor_line_counts() -> Vec<usize> {
let mut line_counts = vec![1000, 10000];
if std::env::var("ZED_BENCH_HUGE").is_ok() {
line_counts.push(100000);
}
line_counts
}
gpui::bench_group!(
benches,
editor_multi_cursor_input,
open_editor_with_one_long_line,
editor_render
);
gpui::bench_main!(benches);