1use crate::model::Phases;
2use serde::Serialize;
3
4#[derive(Debug, Clone, Serialize)]
5pub struct PhaseBreakdown {
6 pub blocked: Option<f64>,
7 pub dns: Option<f64>,
8 pub connect: Option<f64>,
9 pub ssl: Option<f64>,
10 pub send: f64,
11 pub wait: f64,
12 pub receive: f64,
13}
14
15impl PhaseBreakdown {
16 pub fn from_phases(p: &Phases) -> Self {
17 PhaseBreakdown {
18 blocked: p.blocked,
19 dns: p.dns,
20 connect: p.connect,
21 ssl: p.ssl,
22 send: p.send,
23 wait: p.wait,
24 receive: p.receive,
25 }
26 }
27}
28
29pub fn classify_bottleneck(p: &Phases) -> &'static str {
31 let candidates = [
32 ("queueing/blocked", p.blocked.unwrap_or(0.0)),
33 ("DNS", p.dns.unwrap_or(0.0)),
34 ("TCP connect", p.connect.unwrap_or(0.0)),
35 ("TLS handshake", p.ssl.unwrap_or(0.0)),
36 ("request upload", p.send),
37 ("server wait/TTFB", p.wait),
38 ("download/receive", p.receive),
39 ];
40 let mut best: (&'static str, f64) = ("unknown", 0.0);
41 for (label, v) in candidates {
42 if v > best.1 {
43 best = (label, v);
44 }
45 }
46 best.0
47}
48
49#[cfg(test)]
50mod tests {
51 use super::{PhaseBreakdown, classify_bottleneck};
52 use crate::model::Phases;
53
54 #[test]
55 fn picks_dominant_phase() {
56 let p = Phases {
57 wait: 500.0,
58 receive: 10.0,
59 send: 1.0,
60 ..Phases::default()
61 };
62 assert_eq!(classify_bottleneck(&p), "server wait/TTFB");
63 }
64
65 #[test]
66 fn dns_dominant() {
67 let p = Phases {
68 dns: Some(300.0),
69 wait: 5.0,
70 ..Phases::default()
71 };
72 assert_eq!(classify_bottleneck(&p), "DNS");
73 }
74
75 #[test]
76 fn all_zero_is_unknown() {
77 assert_eq!(classify_bottleneck(&Phases::default()), "unknown");
78 }
79
80 #[test]
81 fn breakdown_copies_phases() {
82 let p = Phases {
83 dns: Some(3.0),
84 wait: 9.0,
85 ..Phases::default()
86 };
87 let b = PhaseBreakdown::from_phases(&p);
88 assert_eq!(b.dns, Some(3.0));
89 assert_eq!(b.wait, 9.0);
90 }
91}