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
24pub 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
52pub 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}