1use rustev_contract::eval_report::{Metric, MetricValue};
8use rustev_contract::optimization::OptimizationOutcome;
9use rustev_contract::run::{RequestResult, RunRecord};
10
11#[derive(Debug, Default, Clone, Copy)]
12pub struct ExternalMeasurements<'a> {
13 pub cache_bytes: Option<u64>,
14 pub evictions: Option<u64>,
15 pub invalidation_lag_ms: Option<&'a [u64]>,
16 pub erasure_completion_ms: Option<&'a [u64]>,
17 pub judgment_equivalence: Option<(u64, u64)>,
19}
20
21#[derive(Debug, Clone, Copy)]
22pub struct OptimizationEvaluation<'a> {
23 pub cold: &'a [RunRecord],
24 pub warm: &'a [RunRecord],
25 pub cold_window_ms: Option<u64>,
26 pub warm_window_ms: Option<u64>,
27 pub external: ExternalMeasurements<'a>,
28}
29
30fn unknown(name: impl Into<String>, reason: &str) -> Metric {
31 Metric {
32 name: name.into(),
33 value: MetricValue::Unknown {
34 reason: reason.into(),
35 },
36 }
37}
38
39fn measured(name: impl Into<String>, value: f64, n: u64) -> Metric {
40 Metric {
41 name: name.into(),
42 value: MetricValue::Measured { value, n },
43 }
44}
45
46fn quantile(values: &[u64], percentile: u64) -> Option<u64> {
47 if values.is_empty() {
48 return None;
49 }
50 let mut values = values.to_vec();
51 values.sort_unstable();
52 let rank = (percentile
53 .saturating_mul(values.len() as u64)
54 .saturating_add(99)
55 / 100)
56 .max(1) as usize;
57 values.get(rank - 1).copied()
58}
59
60fn percentile_name(percentile: u64) -> String {
61 if percentile.is_multiple_of(10) {
62 format!("p0.{}", percentile / 10)
63 } else {
64 format!("p0.{percentile}")
65 }
66}
67
68fn phase_metrics(name: &str, runs: &[RunRecord], window_ms: Option<u64>) -> Vec<Metric> {
69 let service = runs
70 .iter()
71 .map(|run| run.timing.elapsed_ms.checked_sub(run.timing.queued_ms))
72 .collect::<Option<Vec<_>>>();
73 let requests = runs
74 .iter()
75 .flat_map(|run| run.requests.iter())
76 .collect::<Vec<_>>();
77 let denominator = requests.len() as u64;
78 let rate = |outcome| {
79 requests
80 .iter()
81 .filter(|request| {
82 request
83 .optimization
84 .as_ref()
85 .is_some_and(|observation| observation.outcome == outcome)
86 })
87 .count() as f64
88 / denominator.max(1) as f64
89 };
90 let mut metrics = Vec::new();
91 for percentile in [50, 95, 99] {
92 let metric_name = format!("{name}/latency_ms/{}", percentile_name(percentile));
93 match service
94 .as_deref()
95 .and_then(|values| quantile(values, percentile))
96 {
97 Some(value) => metrics.push(measured(metric_name, value as f64, runs.len() as u64)),
98 None => metrics.push(unknown(metric_name, "latency was not measured")),
99 }
100 }
101 match window_ms.filter(|window| *window > 0) {
102 _ if runs.is_empty() => metrics.push(unknown(
103 format!("{name}/throughput_per_second"),
104 "zero runs",
105 )),
106 Some(window) => metrics.push(measured(
107 format!("{name}/throughput_per_second"),
108 runs.len() as f64 * 1_000.0 / window as f64,
109 runs.len() as u64,
110 )),
111 None => metrics.push(unknown(
112 format!("{name}/throughput_per_second"),
113 "measurement window is missing",
114 )),
115 }
116 if denominator == 0 {
117 metrics.push(unknown(format!("{name}/hit_rate"), "zero requests"));
118 metrics.push(unknown(format!("{name}/batch_rate"), "zero requests"));
119 metrics.push(unknown(format!("{name}/failures"), "zero requests"));
120 } else {
121 metrics.push(measured(
122 format!("{name}/hit_rate"),
123 rate(OptimizationOutcome::Hit),
124 denominator,
125 ));
126 metrics.push(measured(
127 format!("{name}/batch_rate"),
128 rate(OptimizationOutcome::Batched),
129 denominator,
130 ));
131 metrics.push(measured(
132 format!("{name}/failures"),
133 requests
134 .iter()
135 .filter(|request| matches!(request.result, RequestResult::Failed(_)))
136 .count() as f64,
137 denominator,
138 ));
139 }
140 if runs.is_empty() {
142 metrics.push(unknown(
143 format!("{name}/unknown_liability_units"),
144 "zero runs",
145 ));
146 } else {
147 metrics.push(measured(
148 format!("{name}/unknown_liability_units"),
149 runs.iter().map(|run| run.cost.liability).sum::<u64>() as f64,
150 runs.len() as u64,
151 ));
152 }
153 metrics
154}
155
156fn external_count(name: &str, value: Option<u64>) -> Metric {
157 match value {
158 Some(value) => measured(name, value as f64, 1),
159 None => unknown(name, "measurement was not supplied"),
160 }
161}
162
163fn external_quantile(name: &str, values: Option<&[u64]>, percentile: u64) -> Metric {
164 match values.and_then(|values| quantile(values, percentile)) {
165 Some(value) => measured(name, value as f64, values.unwrap_or_default().len() as u64),
166 None => unknown(name, "measurement was not supplied"),
167 }
168}
169
170pub fn optimization_metrics(input: &OptimizationEvaluation<'_>) -> Vec<Metric> {
171 let mut metrics = phase_metrics("cold", input.cold, input.cold_window_ms);
172 metrics.extend(phase_metrics("warm", input.warm, input.warm_window_ms));
173 metrics.push(external_count("cache_bytes", input.external.cache_bytes));
174 metrics.push(external_count("evictions", input.external.evictions));
175 for percentile in [50, 95, 99] {
176 metrics.push(external_quantile(
177 &format!("invalidation_lag_ms/{}", percentile_name(percentile)),
178 input.external.invalidation_lag_ms,
179 percentile,
180 ));
181 metrics.push(external_quantile(
182 &format!("erasure_completion_ms/{}", percentile_name(percentile)),
183 input.external.erasure_completion_ms,
184 percentile,
185 ));
186 }
187 metrics.push(match input.external.judgment_equivalence {
188 Some((equivalent, compared)) if compared > 0 && equivalent <= compared => measured(
189 "judgment_equivalence_rate",
190 equivalent as f64 / compared as f64,
191 compared,
192 ),
193 Some(_) => unknown("judgment_equivalence_rate", "invalid comparison counts"),
194 None => unknown("judgment_equivalence_rate", "comparison was not supplied"),
195 });
196 metrics
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn missing_measurements_remain_unknown() {
205 let metrics = optimization_metrics(&OptimizationEvaluation {
206 cold: &[],
207 warm: &[],
208 cold_window_ms: None,
209 warm_window_ms: None,
210 external: ExternalMeasurements::default(),
211 });
212 for name in [
213 "cold/latency_ms/p0.5",
214 "warm/throughput_per_second",
215 "cache_bytes",
216 "evictions",
217 "invalidation_lag_ms/p0.95",
218 "erasure_completion_ms/p0.99",
219 "judgment_equivalence_rate",
220 "cold/unknown_liability_units",
221 "warm/unknown_liability_units",
222 "cold/hit_rate",
223 "warm/batch_rate",
224 ] {
225 assert!(metrics.iter().any(|metric| {
226 metric.name == name && matches!(metric.value, MetricValue::Unknown { .. })
227 }));
228 }
229 }
230
231 #[test]
232 fn an_empty_cohort_with_a_window_is_still_unknown() {
233 let metrics = optimization_metrics(&OptimizationEvaluation {
234 cold: &[],
235 warm: &[],
236 cold_window_ms: Some(1_000),
237 warm_window_ms: Some(1_000),
238 external: ExternalMeasurements::default(),
239 });
240 for metric in &metrics {
241 assert!(
242 !matches!(metric.value, MetricValue::Measured { n: 0, .. }),
243 "{} is a measured value over n=0",
244 metric.name
245 );
246 }
247 }
248
249 #[test]
250 fn quantile_is_nearest_rank() {
251 assert_eq!(quantile(&[], 50), None);
252 assert_eq!(quantile(&[7], 50), Some(7));
253 assert_eq!(quantile(&[7], 99), Some(7));
254 let hundred = (1..=100).rev().collect::<Vec<u64>>();
255 assert_eq!(quantile(&hundred, 50), Some(50));
256 assert_eq!(quantile(&hundred, 95), Some(95));
257 assert_eq!(quantile(&hundred, 99), Some(99));
259 let hundred_one = (1..=101).collect::<Vec<u64>>();
261 assert_eq!(quantile(&hundred_one, 99), Some(100));
262 }
263}