Skip to main content

har/analysis/
slowest.rs

1use crate::filter::Filter;
2use crate::model::Capture;
3use crate::render::human_ms;
4use crate::timing::{PhaseBreakdown, classify_bottleneck};
5use serde::Serialize;
6
7#[derive(Debug, Serialize)]
8pub struct SlowestResult {
9    pub entries: Vec<SlowRow>,
10}
11
12#[derive(Debug, Serialize)]
13pub struct SlowRow {
14    pub id: String,
15    pub method: String,
16    pub host: String,
17    pub norm_path: String,
18    pub status: i64,
19    pub duration_ms: f64,
20    pub phases: PhaseBreakdown,
21    pub bottleneck: String,
22}
23
24/// Top-N slowest requests globally, with timing breakdown and bottleneck label.
25pub fn compute_slowest(cap: &Capture, filter: &Filter, top: usize) -> SlowestResult {
26    let mut entries: Vec<SlowRow> = cap
27        .entries
28        .iter()
29        .filter(|e| filter.matches(e))
30        .map(|e| SlowRow {
31            id: e.id.clone(),
32            method: e.method.to_ascii_uppercase(),
33            host: e.host.clone(),
34            norm_path: e.norm_path.clone(),
35            status: e.status,
36            duration_ms: e.duration_ms,
37            phases: PhaseBreakdown::from_phases(&e.timings),
38            bottleneck: classify_bottleneck(&e.timings).to_string(),
39        })
40        .collect();
41
42    entries.sort_by(|a, b| {
43        b.duration_ms
44            .partial_cmp(&a.duration_ms)
45            .unwrap_or(std::cmp::Ordering::Equal)
46            .then(a.id.cmp(&b.id))
47    });
48    entries.truncate(top);
49    SlowestResult { entries }
50}
51
52/// Render slowest requests as deterministic terminal text.
53pub fn render_slowest_text(r: &SlowestResult) -> String {
54    let mut out = String::new();
55    out.push_str("== wiretrail slowest ==\n");
56    for e in &r.entries {
57        out.push_str(&format!(
58            "\n{:>8}  {} {} {}{}  [{}]\n",
59            human_ms(e.duration_ms),
60            e.id,
61            e.method,
62            e.host,
63            e.norm_path,
64            e.status
65        ));
66        out.push_str(&format!("  bottleneck: {}\n", e.bottleneck));
67        out.push_str(&format!(
68            "  phases: wait {} / receive {} / send {} / connect {} / dns {} / ssl {} / blocked {}\n",
69            human_ms(e.phases.wait),
70            human_ms(e.phases.receive),
71            human_ms(e.phases.send),
72            human_ms(e.phases.connect.unwrap_or(0.0)),
73            human_ms(e.phases.dns.unwrap_or(0.0)),
74            human_ms(e.phases.ssl.unwrap_or(0.0)),
75            human_ms(e.phases.blocked.unwrap_or(0.0)),
76        ));
77    }
78    out
79}
80
81#[cfg(test)]
82mod tests {
83    use super::compute_slowest;
84    use crate::filter::Filter;
85    use crate::model::{Phases, sample_capture, sample_entry};
86
87    fn cap() -> crate::model::Capture {
88        let mut fast = sample_entry(0, "h", "GET", "/fast", 200);
89        fast.duration_ms = 5.0;
90        let mut slow = sample_entry(1, "h", "GET", "/slow", 200);
91        slow.duration_ms = 900.0;
92        slow.timings = Phases {
93            wait: 850.0,
94            receive: 40.0,
95            ..Phases::default()
96        };
97        sample_capture(vec![fast, slow])
98    }
99
100    #[test]
101    fn orders_by_duration_desc_with_bottleneck() {
102        let r = compute_slowest(&cap(), &Filter::parse(&[]).unwrap(), 10);
103        assert_eq!(r.entries[0].norm_path, "/slow");
104        assert_eq!(r.entries[0].duration_ms, 900.0);
105        assert_eq!(r.entries[0].bottleneck, "server wait/TTFB");
106    }
107
108    #[test]
109    fn top_bounds_list() {
110        let r = compute_slowest(&cap(), &Filter::parse(&[]).unwrap(), 1);
111        assert_eq!(r.entries.len(), 1);
112        assert_eq!(r.entries[0].norm_path, "/slow");
113    }
114}