359 lines · 12.9 KB · Rust
RawDownload
use collections::HashMap;use gpui::{App, Global, InputLatencySnapshot, Window, WindowId, actions};use hdrhistogram::Histogram;use std::time::Instant; actions!(    dev,    [        /// Opens a buffer showing the input-to-frame latency histogram for the current window.        DumpInputLatencyHistogram,    ]); /// 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 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(timestamp);     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)]struct ReporterState {    previous_snapshot: Option<InputLatencySnapshot>,    previous_timestamp: Option<chrono::DateTime<chrono::Local>>,} 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(data: &InputLatencyReportData) -> String {    let snapshot = &data.snapshot;    let histogram = &snapshot.latency_histogram;    let total = histogram.len();     if total == 0 {        return "No input latency samples recorded yet.\n\nTry typing or clicking in a buffer first.".to_string();    }     let percentiles: &[(&str, f64)] = &[        ("min  ", 0.0),        ("p50  ", 0.50),        ("p75  ", 0.75),        ("p90  ", 0.90),        ("p95  ", 0.95),        ("p99  ", 0.99),        ("p99.9", 0.999),        ("max  ", 1.0),    ];     let now = data.timestamp;     let mut report = String::new();    report.push_str("Input Latency Histogram\n");    report.push_str("=======================\n");     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!(            "Mid-draw events excluded: {}\n",            snapshot.mid_draw_events_dropped        ));    }     write_latency_percentiles(&mut report, "Percentiles", histogram, percentiles);    write_latency_distribution(&mut report, "Distribution", histogram);     let coalesce = &snapshot.events_per_frame_histogram;    let coalesce_total = coalesce.len();    if coalesce_total > 0 {        report.push('\n');        report.push_str("Events coalesced per frame:\n");        for (label, quantile) in percentiles {            let value = if *quantile == 0.0 {                coalesce.min()            } else if *quantile == 1.0 {                coalesce.max()            } else {                coalesce.value_at_quantile(*quantile)            };            report.push_str(&format!("  {label}: {value:>6} events\n"));        }         report.push('\n');        report.push_str("Distribution:\n");        let bar_width = 30usize;        let max_count = coalesce.max();        for n in 1..=max_count {            let count = coalesce                .iter_recorded()                .filter(|value| value.value_iterated_to() == n)                .map(|value| value.count_at_value())                .sum::<u64>();            if count == 0 {                continue;            }            let fraction = count as f64 / coalesce_total as f64;            let bar_len = (fraction * bar_width as f64) as usize;            let bar = "\u{2588}".repeat(bar_len);            report.push_str(&format!(                "  {n:>6} events: {count:>6} ({:>5.1}%) {bar}\n",                fraction * 100.0,            ));        }    }     // Delta section: compare against the previous report's snapshot.    if let (Some(prev_snapshot), Some(prev_timestamp)) =        (&data.previous_snapshot, &data.previous_timestamp)    {        let prev_latency = &prev_snapshot.latency_histogram;        let prev_total = prev_latency.len();        let delta_total = total - prev_total;         report.push('\n');        report.push_str("Delta Since Last Report\n");        report.push_str("-----------------------\n");        let prev_ts = prev_timestamp.format("%Y-%m-%d %H:%M:%S %Z");        let elapsed_secs = (now - *prev_timestamp).num_seconds().max(0);        report.push_str(&format!(            "Previous report: {prev_ts} ({elapsed_secs}s ago)\n"        ));        report.push_str(&format!("New samples: {delta_total}\n"));         if delta_total > 0 {            let mut delta_histogram = histogram.clone();            delta_histogram.subtract(prev_latency).ok();             write_latency_percentiles(                &mut report,                "Percentiles (new samples only)",                &delta_histogram,                percentiles,            );            write_latency_distribution(                &mut report,                "Distribution (new samples only)",                &delta_histogram,            );        }    }     report} fn write_latency_percentiles(    report: &mut String,    heading: &str,    histogram: &Histogram<u64>,    percentiles: &[(&str, f64)],) {    let ns_to_ms = |ns: u64| ns as f64 / 1_000_000.0;     report.push('\n');    report.push_str(heading);    report.push_str(":\n");    for (label, quantile) in percentiles {        let value_ns = if *quantile == 0.0 {            histogram.min()        } else if *quantile == 1.0 {            histogram.max()        } else {            histogram.value_at_quantile(*quantile)        };        let hz = if value_ns > 0 {            1_000_000_000.0 / value_ns as f64        } else {            f64::INFINITY        };        report.push_str(&format!(            "  {label}: {:>8.2}ms  ({:>7.1} Hz)\n",            ns_to_ms(value_ns),            hz        ));    }} fn write_latency_distribution(report: &mut String, heading: &str, histogram: &Histogram<u64>) {    const BUCKETS: &[(u64, u64, &str, &str)] = &[        (0, 4_000_000, "0\u{2013}4ms", "(excellent)"),        (4_000_000, 8_000_000, "4\u{2013}8ms", "(120fps)"),        (8_000_000, 16_000_000, "8\u{2013}16ms", "(60fps)"),        (16_000_000, 33_000_000, "16\u{2013}33ms", "(30fps)"),        (33_000_000, 100_000_000, "33\u{2013}100ms", ""),        (100_000_000, u64::MAX, "100ms+", "(sluggish)"),    ];    let bar_width = 30usize;    let total = histogram.len() as f64;     report.push('\n');    report.push_str(heading);    report.push_str(":\n");    for (low, high, range, note) in BUCKETS {        let count: u64 = histogram            .iter_recorded()            .filter(|value| value.value_iterated_to() >= *low && value.value_iterated_to() < *high)            .map(|value| value.count_at_value())            .sum();        let fraction = if total > 0.0 {            count as f64 / total        } else {            0.0        };        let bar_len = (fraction * bar_width as f64) as usize;        let bar = "\u{2588}".repeat(bar_len);        report.push_str(&format!(            "  {range:>8}  {note:<11}: {count:>6} ({:>5.1}%) {bar}\n",            fraction * 100.0,        ));    }}