Fix crash when dumping debug buffers in a collab session (#61616)

The `dev: dump input latency histogram` and `dev: dump accessibility
tree` actions called `Project::create_local_buffer`, which panics on
remote projects — so running either one as a guest in a collab session
aborted the whole app. Both now use the async `Project::create_buffer` +
`set_text` instead (the same pattern `open_log_file` uses), which routes
through the collab protocol on remote projects.

Since the collab-created buffer is visible to other participants, the
latency report now includes a "Reported from <username>'s machine" line
when the project is shared, using the collab username so we don't expose
anything that isn't already shared with the session.

While restructuring the handler I also split snapshotting from
formatting: the histogram snapshot (and the delta baseline) is captured
synchronously when the action fires, and the report string is built on a
background task. The accessibility tree dump stays synchronous because
gpui doesn't currently expose the captured tree data outside the window
borrow, and adding public API for a debug action's JSON pretty-print
didn't seem worth it.

Release Notes:

- Fixed a crash when running the `dev: dump input latency histogram` or
`dev: dump accessibility tree` actions in a shared project.
This commit is contained in:
Eric Holk authored and GitHub committed 2026-07-26 01:19:29 +00:00
1 parent e2f35dd8c8
commit 5d2df6acd5
3 files changed
+117 -46

No files matched your search

+1
View File
@@ -1198,6 +1198,7 @@ impl InputRateTracker {
/// A point-in-time snapshot of the input-latency histograms for a window,
/// suitable for external formatting.
#[cfg(feature = "input-latency-histogram")]
#[derive(Clone)]
pub struct InputLatencySnapshot {
/// Histogram of input-to-frame latency samples, in nanoseconds.
pub latency_histogram: Histogram<u64>,
+48 -11
View File
@@ -11,19 +11,52 @@ actions!(
]
);
/// Generates a formatted text report of the input-to-frame latency histogram
/// for the given window. If a previous report was generated (tracked via a
/// global on the `App`), includes a delta section showing changes since that
/// report.
pub fn format_input_latency_report(window: &Window, cx: &mut App) -> String {
/// The data needed to format an input-latency report, captured synchronously
/// by [`snapshot_input_latency_report`]. Formatting via
/// [`format_input_latency_report`] is pure computation and can happen on a
/// background thread.
pub struct InputLatencyReportData {
snapshot: InputLatencySnapshot,
previous_snapshot: Option<InputLatencySnapshot>,
previous_timestamp: Option<chrono::DateTime<chrono::Local>>,
timestamp: chrono::DateTime<chrono::Local>,
/// The collab username of the user whose machine produced the data.
/// Set when the report will be placed in a buffer visible to other
/// collaborators, so it's clear whose latency was measured.
reported_by: Option<String>,
}
/// Captures the input-to-frame latency histogram for the given window, along
/// with the previous report's baseline (tracked via a global on the `App`) for
/// delta computation. Advances the baseline to this snapshot.
pub fn snapshot_input_latency_report(
window: &Window,
reported_by: Option<String>,
cx: &mut App,
) -> InputLatencyReportData {
let snapshot = window.input_latency_snapshot();
let timestamp = chrono::Local::now();
let state = cx.default_global::<ReporterState>();
let report = format_report(&snapshot, state);
let data = InputLatencyReportData {
snapshot: snapshot.clone(),
previous_snapshot: state.previous_snapshot.take(),
previous_timestamp: state.previous_timestamp.take(),
timestamp,
reported_by,
};
state.previous_snapshot = Some(snapshot);
state.previous_timestamp = Some(chrono::Local::now());
state.previous_timestamp = Some(timestamp);
report
data
}
/// Generates a formatted text report from a previously captured
/// [`InputLatencyReportData`]. If the capture included a previous snapshot,
/// includes a delta section showing changes since that report.
pub fn format_input_latency_report(data: &InputLatencyReportData) -> String {
format_report(data)
}
#[derive(Default)]
@@ -138,7 +171,8 @@ fn count_frames_in_range(histogram: &Histogram<u64>, low_ns: u64, high_ns: u64)
.sum()
}
fn format_report(snapshot: &InputLatencySnapshot, previous: &ReporterState) -> String {
fn format_report(data: &InputLatencyReportData) -> String {
let snapshot = &data.snapshot;
let histogram = &snapshot.latency_histogram;
let total = histogram.len();
@@ -157,7 +191,7 @@ fn format_report(snapshot: &InputLatencySnapshot, previous: &ReporterState) -> S
("max ", 1.0),
];
let now = chrono::Local::now();
let now = data.timestamp;
let mut report = String::new();
report.push_str("Input Latency Histogram\n");
@@ -165,6 +199,9 @@ fn format_report(snapshot: &InputLatencySnapshot, previous: &ReporterState) -> S
let timestamp = now.format("%Y-%m-%d %H:%M:%S %Z");
report.push_str(&format!("Timestamp: {timestamp}\n"));
if let Some(reported_by) = &data.reported_by {
report.push_str(&format!("Reported from {reported_by}'s machine\n"));
}
report.push_str(&format!("Samples: {total}\n"));
if snapshot.mid_draw_events_dropped > 0 {
report.push_str(&format!(
@@ -217,7 +254,7 @@ fn format_report(snapshot: &InputLatencySnapshot, previous: &ReporterState) -> S
// Delta section: compare against the previous report's snapshot.
if let (Some(prev_snapshot), Some(prev_timestamp)) =
(&previous.previous_snapshot, &previous.previous_timestamp)
(&data.previous_snapshot, &data.previous_timestamp)
{
let prev_latency = &prev_snapshot.latency_histogram;
let prev_total = prev_latency.len();
+68 -35
View File
@@ -918,15 +918,45 @@ fn register_actions(
_: &input_latency_ui::DumpInputLatencyHistogram,
window: &mut Window,
cx: &mut Context<Workspace>| {
let report =
input_latency_ui::format_input_latency_report(window, cx);
let project = workspace.project().clone();
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer(&report, None, true, cx)
});
let editor =
cx.new(|cx| Editor::for_buffer(buffer, Some(project), window, cx));
workspace.add_item_to_active_pane(Box::new(editor), None, true, window, cx);
// In a collab session the report buffer is visible to other
// participants, so attribute the data to this user's machine.
let reported_by = if project.read(cx).is_shared()
|| project.read(cx).is_via_collab()
{
workspace
.user_store()
.read(cx)
.current_user()
.map(|user| user.username.to_string())
} else {
None
};
let report_data = input_latency_ui::snapshot_input_latency_report(
window,
reported_by,
cx,
);
cx.spawn_in(window, async move |workspace, cx| {
let report = cx
.background_spawn(async move {
input_latency_ui::format_input_latency_report(&report_data)
})
.await;
let buffer = project
.update(cx, |project, cx| project.create_buffer(None, true, cx))
.await?;
buffer.update(cx, |buffer, cx| {
buffer.set_text(report, cx);
});
workspace.update_in(cx, |workspace, window, cx| {
let editor = cx
.new(|cx| Editor::for_buffer(buffer, Some(project), window, cx));
workspace
.add_item_to_active_pane(Box::new(editor), None, true, window, cx);
})
})
.detach_and_log_err(cx);
},
)
.register_action(
@@ -936,39 +966,42 @@ fn register_actions(
cx: &mut Context<Workspace>| {
let json = accessibility_tree_dump(window);
let language = workspace.app_state().languages.language_for_name("JSON");
let project = workspace.project().clone();
cx.spawn_in(window, async move |workspace, cx| {
let language = language.await.log_err();
workspace
.update_in(cx, |workspace, window, cx| {
let project = workspace.project().clone();
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer(&json, language, true, cx)
});
let title = "Accessibility Tree".to_string();
let buffer = cx.new(|cx| {
MultiBuffer::singleton(buffer, cx).with_title(title.clone())
});
let editor = cx.new(|cx| {
let mut editor = Editor::for_multibuffer(
buffer,
Some(project),
window,
cx,
);
editor.set_breadcrumb_header(title);
editor
});
workspace.add_item_to_active_pane(
Box::new(editor),
None,
true,
let buffer = project
.update(cx, |project, cx| {
project.create_buffer(language, true, cx)
})
.await?;
buffer.update(cx, |buffer, cx| {
buffer.set_text(json, cx);
});
workspace.update_in(cx, |workspace, window, cx| {
let title = "Accessibility Tree".to_string();
let buffer = cx.new(|cx| {
MultiBuffer::singleton(buffer, cx).with_title(title.clone())
});
let editor = cx.new(|cx| {
let mut editor = Editor::for_multibuffer(
buffer,
Some(project),
window,
cx,
);
})
.log_err();
editor.set_breadcrumb_header(title);
editor
});
workspace.add_item_to_active_pane(
Box::new(editor),
None,
true,
window,
cx,
);
})
})
.detach();
.detach_and_log_err(cx);
},
)
.register_action(