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
39pub 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 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 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 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
117pub 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}