Disable miniprofiler by default (#54645)

Needs https://github.com/zed-industries/zed/pull/54635 for the profile
overrides added into default settings json to work.
Part of https://github.com/zed-industries/zed/issues/48968
Another part of the fix related seems to be
https://github.com/zed-industries/zed/pull/45669 ?

Using the steps from the issue and profiling on macOs had shown that Zed
has 2 memory "leaks" in play when a certain file is being rewritten a
lot of times.

* First, the thread profiler registers a lot of tasks' data and fills
its buffer to the limit:

<img width="3456" height="2158" alt="image"
src="https://github.com/user-attachments/assets/f183312d-4389-4072-8915-d54e60419b08"
/>

* Second, if the buffer gets open, the undo history fragments start to
creep up infinitely:

<img width="3456" height="2158" alt="image"
src="https://github.com/user-attachments/assets/61a2b66b-81fd-4973-9c3c-c339f886d9b2"
/>

The PR aims to solve the first issue by disabling the profiling by
default, yet leaving the way to turn in on quickly with settings.

The memory usage profiling shows that the memory usage is now
dynamically affected by the new setting:

<img width="2032" height="1136" alt="image"
src="https://github.com/user-attachments/assets/8a6c76b9-6fb7-44bc-ac1d-3c34afe7c575"
/>

While the test directory being thrashed with the script from the issue, 
* first, Zed starts with the profiling disabled
* then gets the profiling enabled which results in the memory growth
close to 1 minute mark of the screenshot
* last, the profiling gets disabled again, releasing all the memory
accumulated

Release Notes:

- Improved Zed's default memory usage
This commit is contained in:
Kirill Bulatov 2026-04-23 21:41:52 +03:00 committed by GitHub
parent 2f445d4059
commit fca4d60ce1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 194 additions and 2 deletions

2
Cargo.lock generated
View file

@ -10790,9 +10790,11 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
name = "miniprofiler_ui"
version = "0.1.0"
dependencies = [
"command_palette_hooks",
"gpui",
"rpc",
"serde_json",
"settings",
"smol",
"theme_settings",
"util",

View file

@ -2510,6 +2510,11 @@
// Mostly useful for developers who are managing multiple instances of Zed.
"nightly": {
// "theme": "Andromeda"
"instrumentation": {
"performance_profiler": {
"enabled": true,
},
},
},
// Settings overrides to use when using Zed Stable.
// Mostly useful for developers who are managing multiple instances of Zed.
@ -2520,6 +2525,11 @@
// Mostly useful for developers who are managing multiple instances of Zed.
"dev": {
// "theme": "Andromeda"
"instrumentation": {
"performance_profiler": {
"enabled": true,
},
},
},
// Settings overrides to use when using Linux.
"linux": {},
@ -2639,4 +2649,16 @@
//
// Example: {"log": {"client": "warn"}}
"log": {},
// Configuration for developer-oriented instrumentation tools that can be
// toggled at runtime.
"instrumentation": {
// Performance profiler, accessed via the `zed: open performance profiler`
// action. Collects timing data for foreground and background executor
// tasks. Enabling this may lead to increased memory usage, hence it's
// disabled by default for regular builds.
"performance_profiler": {
"enabled": false,
},
},
}

View file

@ -3,7 +3,10 @@ use std::{
cell::LazyCell,
collections::{HashMap, VecDeque},
hash::{DefaultHasher, Hash, Hasher},
sync::Arc,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
thread::ThreadId,
};
@ -348,6 +351,9 @@ impl Drop for ThreadTimings {
#[doc(hidden)]
pub fn add_task_timing(timing: TaskTiming) {
if !PROFILER_ENABLED.load(Ordering::Acquire) {
return;
}
THREAD_TIMINGS.with(|timings| {
timings.lock().add_task_timing(timing);
});
@ -357,3 +363,28 @@ pub fn add_task_timing(timing: TaskTiming) {
pub fn get_current_thread_task_timings() -> ThreadTaskTimings {
THREAD_TIMINGS.with(|timings| timings.lock().get_thread_task_timings())
}
static PROFILER_ENABLED: AtomicBool = AtomicBool::new(false);
/// Enables or disables task timing collection at runtime.
///
/// When transitioning from enabled to disabled, `add_task_timing` becomes a
/// no-op and the existing per-thread buffers are cleared so stale data isn't
/// reported after a later re-enable. Calls with the current value are a no-op.
pub fn set_enabled(enabled: bool) -> bool {
if PROFILER_ENABLED.swap(enabled, Ordering::AcqRel) == enabled {
return false;
}
if !enabled {
for global in GLOBAL_THREAD_TIMINGS.lock().iter() {
if let Some(timings) = global.timings.upgrade() {
let mut timings = timings.lock();
timings.timings.clear();
timings.timings.shrink_to_fit();
timings.total_pushed = 0;
}
}
}
true
}

View file

@ -12,8 +12,10 @@ workspace = true
path = "src/miniprofiler_ui.rs"
[dependencies]
command_palette_hooks.workspace = true
gpui.workspace = true
rpc.workspace = true
settings.workspace = true
theme_settings.workspace = true
zed_actions.workspace = true
workspace.workspace = true

View file

@ -5,14 +5,17 @@ use std::{
time::{Duration, Instant},
};
use command_palette_hooks::CommandPaletteFilter;
use gpui::{
App, AppContext, ClipboardItem, Context, Div, Entity, Hsla, InteractiveElement,
ParentElement as _, ProfilingCollector, Render, SerializedLocation, SerializedTaskTiming,
SerializedThreadTaskTimings, SharedString, StatefulInteractiveElement, Styled, Task,
ThreadTimingsDelta, TitlebarOptions, UniformListScrollHandle, WeakEntity, WindowBounds,
WindowOptions, div, prelude::FluentBuilder, px, relative, size, uniform_list,
WindowOptions, div, prelude::FluentBuilder, profiler, px, relative, size, uniform_list,
};
use rpc::{AnyProtoClient, proto};
use settings::{RegisterSetting, Settings, SettingsContent, SettingsStore};
use std::any::TypeId;
use util::ResultExt;
use workspace::{
Workspace,
@ -61,7 +64,49 @@ impl ProfileSource {
}
}
#[derive(Clone, Copy, Debug, Default, RegisterSetting)]
struct PerformanceProfilerSettings {
enabled: bool,
}
impl Settings for PerformanceProfilerSettings {
fn from_settings(content: &SettingsContent) -> Self {
let instrumentation = content.instrumentation.as_ref().unwrap();
let profiler = instrumentation.performance_profiler.as_ref().unwrap();
Self {
enabled: profiler.enabled.unwrap(),
}
}
}
pub fn init(startup_time: Instant, cx: &mut App) {
let initial_enabled = PerformanceProfilerSettings::get_global(cx).enabled;
profiler::set_enabled(initial_enabled);
update_command_palette_filter(initial_enabled, cx);
cx.observe_global::<SettingsStore>(|cx| {
let enabled = PerformanceProfilerSettings::get_global(cx).enabled;
// `set_enabled` reports whether the value actually changed, so skip the
// filter update and window cleanup on the common no-op path — the
// settings observer fires for every settings change.
if !profiler::set_enabled(enabled) {
return;
}
update_command_palette_filter(enabled, cx);
if !enabled {
for window in cx
.windows()
.into_iter()
.filter_map(|window| window.downcast::<ProfilerWindow>())
{
window
.update(cx, |_, window, _| window.remove_window())
.ok();
}
}
})
.detach();
cx.observe_new(move |workspace: &mut workspace::Workspace, _, cx| {
let workspace_handle = cx.entity().downgrade();
workspace.register_action(move |_workspace, _: &OpenPerformanceProfiler, window, cx| {
@ -71,6 +116,17 @@ pub fn init(startup_time: Instant, cx: &mut App) {
.detach();
}
fn update_command_palette_filter(enabled: bool, cx: &mut App) {
CommandPaletteFilter::update_global(cx, |filter, _| {
let action = [TypeId::of::<OpenPerformanceProfiler>()];
if enabled {
filter.show_action_types(&action);
} else {
filter.hide_action_types(&action);
}
});
}
fn open_performance_profiler(
startup_time: Instant,
workspace_handle: WeakEntity<Workspace>,

View file

@ -222,6 +222,7 @@ impl VsCodeSettings {
which_key: None,
modeline_lines: None,
feature_flags: None,
instrumentation: None,
}
}

View file

@ -223,6 +223,33 @@ pub struct SettingsContent {
/// Local overrides for feature flags, keyed by flag name.
pub feature_flags: Option<FeatureFlagsMap>,
/// Settings for developer-oriented instrumentation tools (profilers,
/// tracers, etc.) that can be toggled at runtime.
pub instrumentation: Option<InstrumentationSettingsContent>,
}
/// Configuration for developer-oriented instrumentation tools that collect
/// diagnostic data about a running Zed instance.
#[with_fallible_options]
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
pub struct InstrumentationSettingsContent {
/// Configuration for the performance profiler, accessed via the
/// `zed: open performance profiler` action.
pub performance_profiler: Option<PerformanceProfilerSettingsContent>,
}
/// Configuration for the performance profiler which collects timing data
/// for foreground and background executor tasks.
#[with_fallible_options]
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
pub struct PerformanceProfilerSettingsContent {
/// Whether to collect timing data for foreground and background executor
/// tasks. Enabling this may lead to increased memory usage, hence it's
/// disabled by default for regular builds.
///
/// Default: false
pub enabled: Option<bool>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, MergeFrom)]

View file

@ -100,6 +100,31 @@ fn developer_page() -> SettingsPage {
files: USER,
render: crate::pages::render_feature_flags_page,
}),
SettingsPageItem::SectionHeader("Instrumentation"),
SettingsPageItem::SettingItem(SettingItem {
title: "Performance Profiler",
description: "Collect timing data for foreground and background executor tasks so they can be inspected via `zed: open performance profiler`. May lead to increased memory usage.",
field: Box::new(SettingField {
json_path: Some("instrumentation.performance_profiler.enabled"),
pick: |settings_content| {
settings_content
.instrumentation
.as_ref()
.and_then(|i| i.performance_profiler.as_ref())
.and_then(|p| p.enabled.as_ref())
},
write: |settings_content, value| {
settings_content
.instrumentation
.get_or_insert_default()
.performance_profiler
.get_or_insert_default()
.enabled = value;
},
}),
metadata: None,
files: USER,
}),
]),
}
}

View file

@ -3089,6 +3089,32 @@ If you wish to exclude certain hosts from using the proxy, set the `NO_PROXY` en
}
```
## Instrumentation
- Description: Configuration for developer-oriented instrumentation tools (profilers, tracers, etc.) that can be toggled at runtime.
- Setting: `instrumentation`
- Default:
```json
{
"instrumentation": {
"performance_profiler": {
"enabled": false
}
}
}
```
### Performance Profiler
- Description: Collects timing data for foreground and background executor tasks so they can be inspected via the `zed: open performance profiler` action. Enabling this may lead to increased memory usage, hence it's disabled by default for regular builds.
- Setting: `instrumentation.performance_profiler.enabled`
- Default: `false`
**Options**
`boolean` values
## Profiles
- Description: Configuration profiles that can be temporarily applied on top of existing settings or Zed's defaults.