Files
zed/crates/benchmarks/benches/editor_render.rs
T
Kirill Bulatov b6490c5fbd Stop re-resolving editorconfig sections on every language settings lookup (#63323)
Closes https://github.com/zed-industries/zed/issues/59689

Symbolicating the reporter's profile (21s of typing, Windows) showed
`Window::draw` taking 63% of the main thread at ~62 redraws/s (p95 draw
= 40ms), and 30.9% of the *entire* main thread inside
`EditorconfigStore::properties()`:
every `LanguageSettings` lookup — several per frame via
`DisplayMap::tab_size`, `MultiBuffer::language_settings`, indent guides,
and `LspStore::supports_range_formatting` — re-ran the full editorconfig
resolution (ancestor walk plus `ec4rs::Section::apply_to` over every key
of every section) and then cloned the whole `LanguageSettings` struct.
Real-world `.editorconfig` files carry hundreds of irrelevant keys
(`dotnet_*`, `ij_*`, ...), so the per-call map assembly dominated:
`RtlAllocateHeap` 7.3%, `memcmp` 7.0%, `memcpy` 6.6%,
`Properties::find_idx` 5.9% self time.

## Commit 1: resolve only the editorconfig properties Zed reads

- `EditorconfigStore::properties()` now applies only the 7 keys Zed
consumes (`indent_style`, `indent_size`, `tab_width`, `end_of_line`,
`max_line_length`, `insert_final_newline`, `trim_trailing_whitespace`)
instead of materializing every key of every matching section.
- Worktrees with no editorconfig at all now return `None` early instead
of allocating an empty `Properties` and forcing a `LanguageSettings`
clone on every lookup.
- Adds an `editor_render_with_editorconfig` benchmark: maximized window,
file-backed buffer at `src/app/components/editor_pane.ts`, a root
`.editorconfig` shaped after [the second
report](https://github.com/zed-industries/zed/issues/59689#issuecomment-5475124891)
(no `root = true`, a `[*]` section with ~3200 keys, 25 sections with
large `{...}` alternation lists) plus a 4-level nested chain, and a
typing workload (insert/delete + cursor moves).

```
main (f034409b6a):      time: [37.905 ms 38.005 ms 38.107 ms]
                        time: [38.069 ms 38.157 ms 38.248 ms]
this commit:            time: [2.8547 ms 2.8618 ms 2.8702 ms]
                        time: [2.8372 ms 2.8435 ms 2.8510 ms]
```

The candidate is ~13x faster.
GPUI frame report over the same workload: window draw p50 12.71 ms →
0.76 ms, p99 23.38 ms → 1.22 ms; frame budget overruns 556 → 0.

## Commit 2: cache resolved `LanguageSettings` per buffer

- `Buffer` and `BufferSnapshot` now hold `resolved_settings:
Arc<LanguageSettings>`, recomputed by a `SettingsStore` global observer.
- Global methods for retrieving the settings are still there, but buffer
only uses them for multi buffer's `language_settings_at` with an
injected language different from the buffer's primary; cold scopes such
as extension API, LSP workspace configuration, server startup still use
the global resolution

Same benchmark, same method, on top of commit 1 (alternated runs, quiet
machine):

```
commit 1:               time: [2.8547 ms 2.8618 ms 2.8702 ms]
                        time: [2.8372 ms 2.8435 ms 2.8510 ms]
this commit:            time: [1.6256 ms 1.6325 ms 1.6403 ms]
                        change: [-42.685% -42.270% -41.854%] (p = 0.00 < 0.05)
                        time: [1.6221 ms 1.6284 ms 1.6352 ms]
```

Window draw p50 0.76 ms → 0.47 ms, p99 1.22 ms → 0.69 ms, max 1.02 ms;
zero frame-budget overruns on both sides.
Combined, the benchmark's per-keystroke cost drops ~23x vs `main`
(37.9–38.2 ms → 1.62–1.64 ms).


Release Notes:

- Improved typing and rendering performance in projects that use
`.editorconfig` files.
2026-08-31 11:58:50 +00:00

381 lines
12 KiB
Rust

use std::{path::PathBuf, sync::Arc};
use editor::{
Editor, EditorMode, MultiBuffer,
actions::{DeleteToPreviousWordStart, SelectAll, SplitSelectionIntoLines},
};
use gpui::{App, AppContext as _, BenchAppContext, BorrowAppContext as _, Focusable as _};
use indoc::{formatdoc, indoc};
use language::{Buffer, Capability, DiskState, File, LocalFile};
use rand::{Rng as _, SeedableRng as _, rngs::StdRng};
use settings::{LocalSettingsKind, LocalSettingsPath, SettingsStore, WorktreeId};
use util::{RandomCharIter, paths::PathStyle, rel_path::RelPath};
use zed_actions::editor::{MoveDown, MoveUp};
struct BenchFile {
path: Arc<RelPath>,
}
impl File for BenchFile {
fn as_local(&self) -> Option<&dyn LocalFile> {
None
}
fn disk_state(&self) -> DiskState {
DiskState::New
}
fn path(&self) -> &Arc<RelPath> {
&self.path
}
fn full_path(&self, _: &App) -> PathBuf {
PathBuf::from("root").join(self.path.as_std_path())
}
fn path_style(&self, _: &App) -> PathStyle {
PathStyle::local()
}
fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
self.path.file_name().unwrap_or("root")
}
fn worktree_id(&self, _: &App) -> WorktreeId {
WorktreeId::from_usize(0)
}
fn to_proto(&self, _: &App) -> rpc::proto::File {
unimplemented!()
}
fn is_private(&self) -> bool {
false
}
}
#[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;
});
}
#[gpui::bench]
fn editor_render_with_editorconfig(cx: &mut BenchAppContext) {
init_context(cx);
let worktree_id = WorktreeId::from_usize(0);
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
let nested_configs = [
("", jetbrains_editorconfig()),
(
"src",
indoc! {"
[*.{ts,tsx}]
indent_size = 2
"}
.to_string(),
),
(
"src/app",
indoc! {"
[*]
trim_trailing_whitespace = false
[*.ts]
max_line_length = 100
"}
.to_string(),
),
(
"src/app/components",
indoc! {"
[*.{ts,tsx}]
indent_style = space
"}
.to_string(),
),
];
for (directory, content) in nested_configs {
store
.set_local_settings(
worktree_id,
LocalSettingsPath::InWorktree(Arc::from(
RelPath::from_unix_str(directory).unwrap(),
)),
LocalSettingsKind::Editorconfig,
Some(&content),
cx,
)
.unwrap();
}
});
});
let buffer = cx.update(|cx| {
let text = indented_code_text(3000);
let file: Arc<dyn File> = Arc::new(BenchFile {
path: RelPath::from_unix_str("src/app/components/editor_pane.ts")
.unwrap()
.into(),
});
let buffer = cx.new(|cx| {
Buffer::build(
text::Buffer::new(
text::ReplicaId::LOCAL,
cx.entity_id().as_non_zero_u64().into(),
text,
),
Some(file),
Capability::ReadWrite,
cx,
)
});
cx.new(|cx| MultiBuffer::singleton(buffer, 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 insert = true;
cx.bench_renderer(editor, move |editor, window, cx| {
if insert {
editor.handle_input("x", window, cx);
} else {
editor.backspace(&editor::actions::Backspace, window, cx);
}
insert = !insert;
editor.move_down(&MoveDown, window, cx);
editor.move_up(&MoveUp, window, cx);
});
}
fn indented_code_text(line_count: usize) -> String {
let mut text = String::new();
for block in 0..line_count / 10 {
text.push_str(&format!("export function component{block:04}() {{\n"));
text.push_str(" const state = {\n");
text.push_str(" items: [],\n");
text.push_str(" selection: null,\n");
text.push_str(" };\n");
text.push_str(" if (state.items.length > 0) {\n");
text.push_str(" for (const item of state.items) {\n");
text.push_str(" console.log(item, state.selection);\n");
text.push_str(" }\n");
text.push_str(" }\n");
text.push_str("}\n");
}
text
}
fn jetbrains_editorconfig() -> String {
let mut content = indoc! {"
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
max_line_length = 150
tab_width = 4
trim_trailing_whitespace = false
"}
.to_string();
for key_index in 0..750 {
content.push_str(&format!("ij_continuation_option_{key_index:04} = false\n"));
}
for key_index in 0..1500 {
content.push_str(&format!(
"dotnet_diagnostic.ca{key_index:04}.severity = warning\n"
));
}
for key_index in 0..750 {
content.push_str(&format!("resharper_style_option_{key_index:04} = true\n"));
}
let sections = [
"*.css",
"*.feature",
"*.less",
"*.properties",
"*.proto",
"*.sass",
"*.scss",
"*.vue",
".editorconfig",
"{*.ant,*.appxmanifest,*.axml,*.cscfg,*.csdef,*.disco,*.filelayout,*.fxml,*.jhm,*.jnlp,*.jrxml,*.manifest,*.myapp,*.nuspec,*.rng,*.stylecop,*.svcmap,*.tld,*.tps,*.wadcfgx,*.webref,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul,StyleCop.Cache}",
"{*.ats,*.ts}",
"{*.bash,*.sh,*.zsh}",
"{*.cjs,*.js}",
"{*.cjsx,*.coffee}",
"{*.har,*.inputactions,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.stylelintrc,bowerrc,jest.config}",
"{*.hcl,*.nomad}",
"{*.htm,*.html,*.ng,*.sht,*.shtm,*.shtml}",
"{*.markdown,*.md}",
"{*.pb,*.textproto}",
"{*.ps1,*.psd1,*.psm1}",
"{*.tf,*.tfvars}",
"{*.yaml,*.yml}",
"*.js.map",
"*.{appxmanifest,asax,ascx,aspx,axaml,build,cg,cginc,compute,cs,cshtml,dtd,fs,fsi,fsscript,fsx,hlsl,hlsli,hlslinc,master,ml,mli,nuspec,paml,razor,resw,resx,shader,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}",
];
for (section_index, section) in sections.iter().enumerate() {
content.push_str(&formatdoc! {"
[{section}]
indent_size = 2
"});
for key_index in 0..40 {
content.push_str(&format!(
"ij_section_{section_index:02}_option_{key_index:02} = false\n"
));
}
}
content
}
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,
editor_render_with_editorconfig
);
gpui::bench_main!(benches);