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}