wowlab_types/stats/summary/
aggregate.rs1use hdrhistogram::Histogram;
2
3use crate::constants::{
4 HUNDRED, QUANTILE_P01, QUANTILE_P05, QUANTILE_P10, QUANTILE_P25, QUANTILE_P50, QUANTILE_P75,
5 QUANTILE_P90, QUANTILE_P95, QUANTILE_P99, TEN_THOUSAND,
6};
7
8const Z_SCORE_95: f64 = 1.96;
9
10#[inline]
12#[expect(
13 clippy::too_many_arguments,
14 reason = "Welford merge requires the running and chunk state"
15)]
16#[expect(
17 clippy::cast_precision_loss,
18 reason = "Welford aggregation requires integer sample counts in the f64 calculation domain"
19)]
20pub fn merge_dps_stats(
22 running_n: &mut u64,
23 running_wsum: &mut u64,
24 running_m2_bits: &mut u64,
25 running_min: &mut u32,
26 running_max: &mut u32,
27 chunk_n: u32,
28 chunk_mean_x10: u32,
29 chunk_m2_bits: u64,
30 chunk_min: u32,
31 chunk_max: u32,
32) {
33 if chunk_n == 0 {
34 return;
35 }
36
37 let n_a = *running_n;
38 let n_b = u64::from(chunk_n);
39
40 let m2_a = f64::from_bits(*running_m2_bits);
41 let m2_b = f64::from_bits(chunk_m2_bits);
42
43 let mean_a = if n_a == 0 {
44 0.0
45 } else {
46 *running_wsum as f64 / n_a as f64
47 };
48
49 let delta = f64::from(chunk_mean_x10) - mean_a;
50 let n_new = n_a + n_b;
51
52 let wsum_new_128 = u128::from(*running_wsum) + u128::from(n_b) * u128::from(chunk_mean_x10);
53 let wsum_new = u64::try_from(wsum_new_128).unwrap_or(u64::MAX);
54
55 let m2_new = m2_a + m2_b + delta * delta * n_a as f64 * n_b as f64 / n_new as f64;
56
57 *running_n = n_new;
58 *running_wsum = wsum_new;
59 *running_m2_bits = m2_new.to_bits();
60
61 if n_a == 0 {
62 *running_min = chunk_min;
63 *running_max = chunk_max;
64 } else {
65 *running_min = (*running_min).min(chunk_min);
66 *running_max = (*running_max).max(chunk_max);
67 }
68}
69
70#[must_use]
72#[expect(
73 clippy::cast_precision_loss,
74 reason = "result statistics require integer totals in the f64 calculation domain"
75)]
76pub fn compute_result_core(
77 iterations_total: u64,
78 weighted_mean_num_x10: u64,
79 m2_total_bits: u64,
80) -> (u32, u32, u32) {
81 if iterations_total == 0 {
82 return (0, 0, 0);
83 }
84
85 let n = iterations_total as f64;
86 let grand_mean_x10 = weighted_mean_num_x10 as f64 / n;
87 let m2 = f64::from_bits(m2_total_bits);
88
89 let mean_dps_x10 = crate::numeric::f64_to_u32_saturating_round(grand_mean_x10);
90
91 let var_sample = if iterations_total > 1 {
92 m2 / (n - 1.0)
93 } else {
94 0.0
95 };
96 let std = var_sample.sqrt();
97 let std_dps_x10 = crate::numeric::f64_to_u32_saturating_round(std);
98
99 let ci95_half_pct_x10000 = if iterations_total > 1 && grand_mean_x10 > 0.0 {
100 let ci95_half_fraction = Z_SCORE_95 * std / (n.sqrt() * grand_mean_x10);
101
102 crate::numeric::f64_to_u32_saturating_round(ci95_half_fraction * HUNDRED * TEN_THOUSAND)
103 } else {
104 0
105 };
106
107 (mean_dps_x10, std_dps_x10, ci95_half_pct_x10000)
108}
109
110#[must_use]
112pub fn histogram_percentile_from_hdr(hdr: &Histogram<u64>, p: f64) -> Option<u32> {
113 if hdr.is_empty() {
114 return None;
115 }
116
117 let q = p.clamp(0.0, 1.0);
118 let value = hdr.value_at_quantile(q);
119
120 Some(u32::try_from(value).unwrap_or(u32::MAX))
121}
122
123#[must_use]
125pub fn compute_all_percentiles_from_hdr(hdr: &Histogram<u64>) -> crate::proto::Percentiles {
126 let pct = |frac: f64| -> u32 { histogram_percentile_from_hdr(hdr, frac).unwrap_or(0) };
127
128 crate::proto::Percentiles {
129 p01_x10: pct(QUANTILE_P01),
130 p05_x10: pct(QUANTILE_P05),
131 p10_x10: pct(QUANTILE_P10),
132 p25_x10: pct(QUANTILE_P25),
133 p50_x10: pct(QUANTILE_P50),
134 p75_x10: pct(QUANTILE_P75),
135 p90_x10: pct(QUANTILE_P90),
136 p95_x10: pct(QUANTILE_P95),
137 p99_x10: pct(QUANTILE_P99),
138 }
139}
140
141#[must_use]
143#[expect(
144 clippy::cast_precision_loss,
145 reason = "bucket averages require integer totals in the f64 calculation domain"
146)]
147pub fn compute_bucket_averages_x10(sums_x10: &[u64], samples: &[u64]) -> Vec<u32> {
148 let len = sums_x10.len().max(samples.len());
149
150 (0..len)
151 .map(|i| {
152 let s = sums_x10.get(i).copied().unwrap_or(0);
153 let n = samples.get(i).copied().unwrap_or(0);
154
155 if n == 0 {
156 0
157 } else {
158 crate::numeric::f64_to_u32_saturating_round(s as f64 / n as f64)
159 }
160 })
161 .collect()
162}