Skip to main content

har/analysis/
startup.rs

1use crate::filter::Filter;
2use crate::model::{Capture, Entry};
3use crate::render::human_ms;
4use serde::Serialize;
5
6#[derive(Debug, Serialize)]
7pub struct StartupResult {
8    pub window_ms: f64,
9    pub requests_in_window: usize,
10    pub max_concurrency: usize,
11    pub critical_path_ms: f64,
12    pub critical_path: Vec<StartupCall>,
13    pub slowest: Vec<StartupCall>,
14}
15
16#[derive(Debug, Clone, Serialize)]
17pub struct StartupCall {
18    pub id: String,
19    pub method: String,
20    pub host: String,
21    pub norm_path: String,
22    pub offset_ms: f64,
23    pub duration_ms: f64,
24    pub status: i64,
25}
26
27fn call_of(e: &Entry) -> StartupCall {
28    StartupCall {
29        id: e.id.clone(),
30        method: e.method.to_ascii_uppercase(),
31        host: e.host.clone(),
32        norm_path: e.norm_path.clone(),
33        offset_ms: e.started_offset_ms,
34        duration_ms: e.duration_ms,
35        status: e.status,
36    }
37}
38
39/// Profile the boot window: concurrency, sequential critical path, slow deps.
40/// `window_ms == 0` means "the whole capture".
41pub fn compute_startup(
42    cap: &Capture,
43    filter: &Filter,
44    window_ms: u64,
45    top: usize,
46) -> StartupResult {
47    let mut entries: Vec<&Entry> = cap
48        .entries
49        .iter()
50        .filter(|e| {
51            filter.matches(e) && (window_ms == 0 || e.started_offset_ms <= window_ms as f64)
52        })
53        .collect();
54    entries.sort_by(|a, b| {
55        a.started_offset_ms
56            .partial_cmp(&b.started_offset_ms)
57            .unwrap_or(std::cmp::Ordering::Equal)
58            .then(a.index.cmp(&b.index))
59    });
60
61    // max concurrency via a sweep line over start/end events.
62    let mut events: Vec<(f64, i32)> = Vec::with_capacity(entries.len() * 2);
63    for e in &entries {
64        events.push((e.started_offset_ms, 1));
65        events.push((e.started_offset_ms + e.duration_ms.max(0.0), -1));
66    }
67    // ends before starts at the same instant, so a touch-point isn't double-counted.
68    events.sort_by(|a, b| {
69        a.0.partial_cmp(&b.0)
70            .unwrap_or(std::cmp::Ordering::Equal)
71            .then(a.1.cmp(&b.1))
72    });
73    let mut cur = 0i32;
74    let mut max_concurrency = 0i32;
75    for (_, d) in &events {
76        cur += d;
77        max_concurrency = max_concurrency.max(cur);
78    }
79
80    // greedy sequential chain: each next call starts at/after the current one ends.
81    let mut chain: Vec<StartupCall> = Vec::new();
82    let mut chain_ms = 0.0;
83    let mut end = f64::MIN;
84    for e in &entries {
85        if e.started_offset_ms >= end {
86            chain.push(call_of(e));
87            chain_ms += e.duration_ms.max(0.0);
88            end = e.started_offset_ms + e.duration_ms.max(0.0);
89        }
90    }
91    let critical_path: Vec<StartupCall> = chain.iter().take(top).cloned().collect();
92
93    let mut slow: Vec<&Entry> = entries.clone();
94    slow.sort_by(|a, b| {
95        b.duration_ms
96            .partial_cmp(&a.duration_ms)
97            .unwrap_or(std::cmp::Ordering::Equal)
98    });
99    let slowest: Vec<StartupCall> = slow.iter().take(top).map(|e| call_of(e)).collect();
100
101    let window_ms_out = if window_ms == 0 {
102        entries.last().map(|e| e.started_offset_ms).unwrap_or(0.0)
103    } else {
104        window_ms as f64
105    };
106
107    StartupResult {
108        window_ms: window_ms_out,
109        requests_in_window: entries.len(),
110        max_concurrency: max_concurrency.max(0) as usize,
111        critical_path_ms: chain_ms,
112        critical_path,
113        slowest,
114    }
115}
116
117/// Render the startup profile as deterministic terminal text.
118pub fn render_startup_text(r: &StartupResult) -> String {
119    let mut out = String::new();
120    out.push_str("== wiretrail startup ==\n");
121    out.push_str(&format!(
122        "{} requests in {} · max concurrency {} · critical path {}\n",
123        r.requests_in_window,
124        human_ms(r.window_ms),
125        r.max_concurrency,
126        human_ms(r.critical_path_ms)
127    ));
128    out.push_str("\ncritical path (sequential spine):\n");
129    for c in &r.critical_path {
130        out.push_str(&format!(
131            "  {:>8}  {} {} {}{}  [{}]\n",
132            human_ms(c.duration_ms),
133            c.id,
134            c.method,
135            c.host,
136            c.norm_path,
137            c.status
138        ));
139    }
140    out.push_str("\nslowest in window:\n");
141    for c in &r.slowest {
142        out.push_str(&format!(
143            "  {:>8}  {} {} {}{}\n",
144            human_ms(c.duration_ms),
145            c.id,
146            c.method,
147            c.host,
148            c.norm_path
149        ));
150    }
151    out
152}
153
154#[cfg(test)]
155mod tests {
156    use super::compute_startup;
157    use crate::filter::Filter;
158    use crate::model::{Entry, sample_capture, sample_entry};
159
160    fn at(index: usize, path: &str, offset: f64, dur: f64) -> Entry {
161        let mut e = sample_entry(index, "h", "GET", path, 200);
162        e.started_offset_ms = offset;
163        e.duration_ms = dur;
164        e
165    }
166
167    #[test]
168    fn measures_max_concurrency() {
169        let cap = sample_capture(vec![
170            at(0, "/a", 0.0, 200.0),
171            at(1, "/b", 100.0, 100.0),
172            at(2, "/c", 120.0, 50.0),
173        ]);
174        let r = compute_startup(&cap, &Filter::parse(&[]).unwrap(), 0, 10);
175        assert_eq!(r.max_concurrency, 3);
176        assert_eq!(r.requests_in_window, 3);
177    }
178
179    #[test]
180    fn builds_sequential_critical_path() {
181        let cap = sample_capture(vec![
182            at(0, "/a", 0.0, 100.0),
183            at(1, "/b", 100.0, 100.0),
184            at(2, "/c", 200.0, 100.0),
185        ]);
186        let r = compute_startup(&cap, &Filter::parse(&[]).unwrap(), 0, 10);
187        assert_eq!(r.critical_path.len(), 3);
188        assert_eq!(r.critical_path_ms, 300.0);
189        assert_eq!(r.max_concurrency, 1);
190    }
191
192    #[test]
193    fn window_bounds_entries() {
194        let cap = sample_capture(vec![at(0, "/a", 0.0, 10.0), at(1, "/late", 60000.0, 10.0)]);
195        let r = compute_startup(&cap, &Filter::parse(&[]).unwrap(), 30000, 10);
196        assert_eq!(r.requests_in_window, 1);
197    }
198}