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.
381 lines
12 KiB
Rust
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);
|