1use serde::Serialize;
2
3#[non_exhaustive]
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
9pub struct DurationSummary {
10 pub sample_count: u64,
12 pub average_ms: Option<u64>,
14 pub p50_ms: Option<u64>,
16 pub p90_ms: Option<u64>,
18 pub p95_ms: Option<u64>,
20 pub p99_ms: Option<u64>,
22}
23
24impl DurationSummary {
25 pub fn from_samples(samples_ms: &[u64]) -> Self {
27 if samples_ms.is_empty() {
28 return Self::default();
29 }
30
31 let mut sorted = samples_ms.to_vec();
32 sorted.sort_unstable();
33
34 let average_ms = sorted
35 .iter()
36 .map(|&sample| u128::from(sample))
37 .sum::<u128>()
38 / sorted.len() as u128;
39
40 Self {
41 sample_count: sorted.len() as u64,
42 average_ms: Some(average_ms as u64),
43 p50_ms: Some(indexed_percentile(&sorted, 50)),
44 p90_ms: Some(indexed_percentile(&sorted, 90)),
45 p95_ms: Some(indexed_percentile(&sorted, 95)),
46 p99_ms: Some(indexed_percentile(&sorted, 99)),
47 }
48 }
49}
50
51fn indexed_percentile(sorted: &[u64], percentile: u8) -> u64 {
54 let index = (sorted.len() as u128 * u128::from(percentile) / 100) as usize;
55 sorted[index.min(sorted.len() - 1)]
56}
57
58#[cfg(test)]
59mod tests {
60 use super::DurationSummary;
61
62 #[test]
63 fn empty_samples_have_no_observed_metrics() {
64 assert_eq!(
65 DurationSummary::from_samples(&[]),
66 DurationSummary {
67 sample_count: 0,
68 average_ms: None,
69 p50_ms: None,
70 p90_ms: None,
71 p95_ms: None,
72 p99_ms: None,
73 }
74 );
75 }
76
77 #[test]
78 fn one_sample_populates_every_metric() {
79 assert_eq!(
80 DurationSummary::from_samples(&[17]),
81 DurationSummary {
82 sample_count: 1,
83 average_ms: Some(17),
84 p50_ms: Some(17),
85 p90_ms: Some(17),
86 p95_ms: Some(17),
87 p99_ms: Some(17),
88 }
89 );
90 }
91
92 #[test]
93 fn even_population_pins_existing_renderer_indices() {
94 assert_eq!(
95 DurationSummary::from_samples(&[10, 20, 30, 40]),
96 DurationSummary {
97 sample_count: 4,
98 average_ms: Some(25),
99 p50_ms: Some(30),
100 p90_ms: Some(40),
101 p95_ms: Some(40),
102 p99_ms: Some(40),
103 }
104 );
105 }
106
107 #[test]
108 fn input_order_does_not_change_summary() {
109 let ordered = DurationSummary::from_samples(&[1, 2, 3, 4, 5]);
110 let shuffled = DurationSummary::from_samples(&[5, 2, 4, 1, 3]);
111
112 assert_eq!(shuffled, ordered);
113 }
114
115 #[test]
116 fn average_does_not_overflow_u64() {
117 let summary = DurationSummary::from_samples(&[u64::MAX, u64::MAX]);
118
119 assert_eq!(summary.average_ms, Some(u64::MAX));
120 }
121}