input_latency_ui.rs

  1use gpui::{App, Global, InputLatencySnapshot, Window, actions};
  2use hdrhistogram::Histogram;
  3
  4actions!(
  5    dev,
  6    [
  7        /// Opens a buffer showing the input-to-frame latency histogram for the current window.
  8        DumpInputLatencyHistogram,
  9    ]
 10);
 11
 12/// Generates a formatted text report of the input-to-frame latency histogram
 13/// for the given window. If a previous report was generated (tracked via a
 14/// global on the `App`), includes a delta section showing changes since that
 15/// report.
 16pub fn format_input_latency_report(window: &Window, cx: &mut App) -> String {
 17    let snapshot = window.input_latency_snapshot();
 18    let state = cx.default_global::<ReporterState>();
 19    let report = format_report(&snapshot, state);
 20
 21    state.previous_snapshot = Some(snapshot);
 22    state.previous_timestamp = Some(chrono::Local::now());
 23
 24    report
 25}
 26
 27#[derive(Default)]
 28struct ReporterState {
 29    previous_snapshot: Option<InputLatencySnapshot>,
 30    previous_timestamp: Option<chrono::DateTime<chrono::Local>>,
 31}
 32
 33impl Global for ReporterState {}
 34
 35fn format_report(snapshot: &InputLatencySnapshot, previous: &ReporterState) -> String {
 36    let histogram = &snapshot.latency_histogram;
 37    let total = histogram.len();
 38
 39    if total == 0 {
 40        return "No input latency samples recorded yet.\n\nTry typing or clicking in a buffer first.".to_string();
 41    }
 42
 43    let percentiles: &[(&str, f64)] = &[
 44        ("min  ", 0.0),
 45        ("p50  ", 0.50),
 46        ("p75  ", 0.75),
 47        ("p90  ", 0.90),
 48        ("p95  ", 0.95),
 49        ("p99  ", 0.99),
 50        ("p99.9", 0.999),
 51        ("max  ", 1.0),
 52    ];
 53
 54    let now = chrono::Local::now();
 55
 56    let mut report = String::new();
 57    report.push_str("Input Latency Histogram\n");
 58    report.push_str("=======================\n");
 59
 60    let timestamp = now.format("%Y-%m-%d %H:%M:%S %Z");
 61    report.push_str(&format!("Timestamp: {timestamp}\n"));
 62    report.push_str(&format!("Samples: {total}\n"));
 63    if snapshot.mid_draw_events_dropped > 0 {
 64        report.push_str(&format!(
 65            "Mid-draw events excluded: {}\n",
 66            snapshot.mid_draw_events_dropped
 67        ));
 68    }
 69
 70    write_latency_percentiles(&mut report, "Percentiles", histogram, percentiles);
 71    write_latency_distribution(&mut report, "Distribution", histogram);
 72
 73    let coalesce = &snapshot.events_per_frame_histogram;
 74    let coalesce_total = coalesce.len();
 75    if coalesce_total > 0 {
 76        report.push('\n');
 77        report.push_str("Events coalesced per frame:\n");
 78        for (label, quantile) in percentiles {
 79            let value = if *quantile == 0.0 {
 80                coalesce.min()
 81            } else if *quantile == 1.0 {
 82                coalesce.max()
 83            } else {
 84                coalesce.value_at_quantile(*quantile)
 85            };
 86            report.push_str(&format!("  {label}: {value:>6} events\n"));
 87        }
 88
 89        report.push('\n');
 90        report.push_str("Distribution:\n");
 91        let bar_width = 30usize;
 92        let max_count = coalesce.max();
 93        for n in 1..=max_count {
 94            let count = coalesce
 95                .iter_recorded()
 96                .filter(|value| value.value_iterated_to() == n)
 97                .map(|value| value.count_at_value())
 98                .sum::<u64>();
 99            if count == 0 {
100                continue;
101            }
102            let fraction = count as f64 / coalesce_total as f64;
103            let bar_len = (fraction * bar_width as f64) as usize;
104            let bar = "\u{2588}".repeat(bar_len);
105            report.push_str(&format!(
106                "  {n:>6} events: {count:>6} ({:>5.1}%) {bar}\n",
107                fraction * 100.0,
108            ));
109        }
110    }
111
112    // Delta section: compare against the previous report's snapshot.
113    if let (Some(prev_snapshot), Some(prev_timestamp)) =
114        (&previous.previous_snapshot, &previous.previous_timestamp)
115    {
116        let prev_latency = &prev_snapshot.latency_histogram;
117        let prev_total = prev_latency.len();
118        let delta_total = total - prev_total;
119
120        report.push('\n');
121        report.push_str("Delta Since Last Report\n");
122        report.push_str("-----------------------\n");
123        let prev_ts = prev_timestamp.format("%Y-%m-%d %H:%M:%S %Z");
124        let elapsed_secs = (now - *prev_timestamp).num_seconds().max(0);
125        report.push_str(&format!(
126            "Previous report: {prev_ts} ({elapsed_secs}s ago)\n"
127        ));
128        report.push_str(&format!("New samples: {delta_total}\n"));
129
130        if delta_total > 0 {
131            let mut delta_histogram = histogram.clone();
132            delta_histogram.subtract(prev_latency).ok();
133
134            write_latency_percentiles(
135                &mut report,
136                "Percentiles (new samples only)",
137                &delta_histogram,
138                percentiles,
139            );
140            write_latency_distribution(
141                &mut report,
142                "Distribution (new samples only)",
143                &delta_histogram,
144            );
145        }
146    }
147
148    report
149}
150
151fn write_latency_percentiles(
152    report: &mut String,
153    heading: &str,
154    histogram: &Histogram<u64>,
155    percentiles: &[(&str, f64)],
156) {
157    let ns_to_ms = |ns: u64| ns as f64 / 1_000_000.0;
158
159    report.push('\n');
160    report.push_str(heading);
161    report.push_str(":\n");
162    for (label, quantile) in percentiles {
163        let value_ns = if *quantile == 0.0 {
164            histogram.min()
165        } else if *quantile == 1.0 {
166            histogram.max()
167        } else {
168            histogram.value_at_quantile(*quantile)
169        };
170        let hz = if value_ns > 0 {
171            1_000_000_000.0 / value_ns as f64
172        } else {
173            f64::INFINITY
174        };
175        report.push_str(&format!(
176            "  {label}: {:>8.2}ms  ({:>7.1} Hz)\n",
177            ns_to_ms(value_ns),
178            hz
179        ));
180    }
181}
182
183fn write_latency_distribution(report: &mut String, heading: &str, histogram: &Histogram<u64>) {
184    const BUCKETS: &[(u64, u64, &str, &str)] = &[
185        (0, 4_000_000, "0\u{2013}4ms", "(excellent)"),
186        (4_000_000, 8_000_000, "4\u{2013}8ms", "(120fps)"),
187        (8_000_000, 16_000_000, "8\u{2013}16ms", "(60fps)"),
188        (16_000_000, 33_000_000, "16\u{2013}33ms", "(30fps)"),
189        (33_000_000, 100_000_000, "33\u{2013}100ms", ""),
190        (100_000_000, u64::MAX, "100ms+", "(sluggish)"),
191    ];
192    let bar_width = 30usize;
193    let total = histogram.len() as f64;
194
195    report.push('\n');
196    report.push_str(heading);
197    report.push_str(":\n");
198    for (low, high, range, note) in BUCKETS {
199        let count: u64 = histogram
200            .iter_recorded()
201            .filter(|value| value.value_iterated_to() >= *low && value.value_iterated_to() < *high)
202            .map(|value| value.count_at_value())
203            .sum();
204        let fraction = if total > 0.0 {
205            count as f64 / total
206        } else {
207            0.0
208        };
209        let bar_len = (fraction * bar_width as f64) as usize;
210        let bar = "\u{2588}".repeat(bar_len);
211        report.push_str(&format!(
212            "  {range:>8}  {note:<11}: {count:>6} ({:>5.1}%) {bar}\n",
213            fraction * 100.0,
214        ));
215    }
216}