Cargo.lock 🔗
@@ -8919,8 +8919,10 @@ name = "input_latency_ui"
version = "0.1.0"
dependencies = [
"chrono",
+ "collections",
"gpui",
"hdrhistogram",
+ "telemetry",
]
[[package]]
Katie Geer and Eric Holk created
Self-Review Checklist:
- [ ] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [ ] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [ ] Performance impact has been considered and is acceptable
Closes #ISSUE
Release Notes:
- N/A or Added/Fixed/Improved ...
---------
Co-authored-by: Eric Holk <eric@zed.dev>
Cargo.lock | 2
crates/input_latency_ui/Cargo.toml | 2
crates/input_latency_ui/src/input_latency_ui.rs | 108 ++++++++++++++++++
crates/zed/src/zed.rs | 16 ++
4 files changed, 127 insertions(+), 1 deletion(-)
@@ -8919,8 +8919,10 @@ name = "input_latency_ui"
version = "0.1.0"
dependencies = [
"chrono",
+ "collections",
"gpui",
"hdrhistogram",
+ "telemetry",
]
[[package]]
@@ -13,5 +13,7 @@ path = "src/input_latency_ui.rs"
[dependencies]
chrono.workspace = true
+collections.workspace = true
gpui = { workspace = true, features = ["input-latency-histogram"] }
hdrhistogram.workspace = true
+telemetry.workspace = true
@@ -1,5 +1,7 @@
-use gpui::{App, Global, InputLatencySnapshot, Window, actions};
+use collections::HashMap;
+use gpui::{App, Global, InputLatencySnapshot, Window, WindowId, actions};
use hdrhistogram::Histogram;
+use std::time::Instant;
actions!(
dev,
@@ -32,6 +34,110 @@ struct ReporterState {
impl Global for ReporterState {}
+/// Per-window state used for telemetry delta computation. Kept separate from
+/// `ReporterState` so the user-facing dump and the background telemetry flush
+/// maintain independent baselines.
+#[derive(Default)]
+struct TelemetryReporterState {
+ /// Keyed by window id. Each entry holds the cumulative snapshot at the time
+ /// of the last telemetry flush, plus the wall-clock time of that flush.
+ previous: HashMap<WindowId, (Instant, InputLatencySnapshot)>,
+}
+
+impl Global for TelemetryReporterState {}
+
+/// Nanosecond boundaries for the time-range buckets used in telemetry.
+/// These match the display distribution in format_report so the two stay in sync.
+const MS4_NS: u64 = 4_000_000;
+const MS8_NS: u64 = 8_000_000;
+const MS16_NS: u64 = 16_000_000;
+const MS33_NS: u64 = 33_000_000;
+const MS100_NS: u64 = 100_000_000;
+
+/// Minimum number of frames that must be present in the delta window for the
+/// telemetry report to be sent. Avoids sending noise for windows that are
+/// mostly idle.
+const MIN_FRAMES_TO_REPORT: u64 = 5_000;
+
+/// Computes and sends a `input_latency_report` telemetry event for the given
+/// window if enough frames have been recorded since the last report.
+///
+/// Call this periodically (e.g. every five minutes) from a spawned task. A
+/// separate baseline snapshot is kept per window so user-facing histogram dumps
+/// and telemetry never share state.
+pub fn report_input_latency_telemetry(window: &Window, cx: &mut App) {
+ let current = window.input_latency_snapshot();
+ let window_id = window.window_handle().window_id();
+
+ let state = cx.default_global::<TelemetryReporterState>();
+ let now = Instant::now();
+
+ let (delta_latency, delta_coalesce, report_window_seconds) =
+ if let Some((prev_instant, prev_snapshot)) = state.previous.get(&window_id) {
+ let mut delta_latency = current.latency_histogram.clone();
+ delta_latency
+ .subtract(&prev_snapshot.latency_histogram)
+ .ok();
+ let mut delta_coalesce = current.events_per_frame_histogram.clone();
+ delta_coalesce
+ .subtract(&prev_snapshot.events_per_frame_histogram)
+ .ok();
+ let elapsed = now.duration_since(*prev_instant).as_secs();
+ (delta_latency, delta_coalesce, elapsed)
+ } else {
+ // First report for this window: the full cumulative histogram is the
+ // delta from the empty starting state. We don't know how long the
+ // window has been open, so record 0 to signal that this is the
+ // initial accumulation period rather than a fixed-width window.
+ (
+ current.latency_histogram.clone(),
+ current.events_per_frame_histogram.clone(),
+ 0u64,
+ )
+ };
+
+ let total_frames = delta_latency.len();
+ if total_frames < MIN_FRAMES_TO_REPORT {
+ return;
+ }
+
+ state.previous.insert(window_id, (now, current));
+
+ let frames_sub4 = count_frames_in_range(&delta_latency, 0, MS4_NS);
+ let frames_4to8 = count_frames_in_range(&delta_latency, MS4_NS, MS8_NS);
+ let frames_8to16 = count_frames_in_range(&delta_latency, MS8_NS, MS16_NS);
+ let frames_16to33 = count_frames_in_range(&delta_latency, MS16_NS, MS33_NS);
+ let frames_33to100 = count_frames_in_range(&delta_latency, MS33_NS, MS100_NS);
+ // frames > 100 ms are implicitly total_frames - (sub4 + 4to8 + 8to16 + 16to33 + 33to100)
+
+ let frames_with_1_event = count_frames_in_range(&delta_coalesce, 1, 2);
+ let frames_with_2_events = count_frames_in_range(&delta_coalesce, 2, 3);
+ let frames_with_3_events = count_frames_in_range(&delta_coalesce, 3, 4);
+ // frames with 4+ events are implicitly total_frames - (1 + 2 + 3)
+
+ telemetry::event!(
+ "Latency Report",
+ frames_sub4 = frames_sub4,
+ frames_4to8 = frames_4to8,
+ frames_8to16 = frames_8to16,
+ frames_16to33 = frames_16to33,
+ frames_33to100 = frames_33to100,
+ total_frames = total_frames,
+ frames_with_1_event = frames_with_1_event,
+ frames_with_2_events = frames_with_2_events,
+ frames_with_3_events = frames_with_3_events,
+ report_window_seconds = report_window_seconds,
+ );
+}
+
+fn count_frames_in_range(histogram: &Histogram<u64>, low_ns: u64, high_ns: u64) -> u64 {
+ histogram
+ .iter_recorded()
+ .filter(|v| v.value_iterated_to() >= low_ns && v.value_iterated_to() < high_ns)
+ .map(|v| v.count_at_value())
+ .sum()
+}
+
fn format_report(snapshot: &InputLatencySnapshot, previous: &ReporterState) -> String {
let histogram = &snapshot.latency_histogram;
let total = histogram.len();
@@ -410,6 +410,22 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut App) {
.detach();
}
+ cx.spawn_in(window, async move |_this, cx| {
+ const TELEMETRY_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5 * 60);
+ loop {
+ cx.background_executor().timer(TELEMETRY_INTERVAL).await;
+ if cx
+ .update(|window, cx| {
+ input_latency_ui::report_input_latency_telemetry(window, cx);
+ })
+ .is_err()
+ {
+ break;
+ }
+ }
+ })
+ .detach();
+
let multi_workspace_handle = cx.entity().downgrade();
window.on_window_should_close(cx, move |window, cx| {
multi_workspace_handle