loom_rs/mab/
calibration.rs1use serde::{Deserialize, Serialize};
13use std::time::Instant;
14
15use crate::bridge::RayonTask;
16
17#[derive(Clone, Debug)]
19pub struct CalibrationResult {
20 pub offload_overhead_us: f64,
23
24 pub p50_us: f64,
26
27 pub p99_us: f64,
29}
30
31#[derive(Clone, Debug, Serialize, Deserialize)]
33pub struct CalibrationConfig {
34 #[serde(default)]
37 pub enabled: bool,
38
39 #[serde(default = "default_warmup_iterations")]
42 pub warmup_iterations: usize,
43
44 #[serde(default = "default_sample_count")]
47 pub sample_count: usize,
48}
49
50fn default_warmup_iterations() -> usize {
51 100
52}
53
54fn default_sample_count() -> usize {
55 1000
56}
57
58impl Default for CalibrationConfig {
59 fn default() -> Self {
60 Self {
61 enabled: false,
62 warmup_iterations: default_warmup_iterations(),
63 sample_count: default_sample_count(),
64 }
65 }
66}
67
68impl CalibrationConfig {
69 pub fn new() -> Self {
71 Self::default()
72 }
73
74 pub fn enabled(mut self) -> Self {
76 self.enabled = true;
77 self
78 }
79
80 pub fn warmup_iterations(mut self, count: usize) -> Self {
82 self.warmup_iterations = count;
83 self
84 }
85
86 pub fn sample_count(mut self, count: usize) -> Self {
88 self.sample_count = count;
89 self
90 }
91}
92
93pub async fn calibrate(
98 rayon_pool: &rayon::ThreadPool,
99 config: &CalibrationConfig,
100) -> CalibrationResult {
101 for _ in 0..config.warmup_iterations {
103 let (task, completion) = RayonTask::<()>::new();
104 rayon_pool.spawn(move || {
105 completion.complete(());
106 });
107 task.await;
108 }
109
110 let mut samples = Vec::with_capacity(config.sample_count);
112 for _ in 0..config.sample_count {
113 let start = Instant::now();
114 let (task, completion) = RayonTask::<()>::new();
115 rayon_pool.spawn(move || {
116 std::hint::black_box(());
118 completion.complete(());
119 });
120 task.await;
121 samples.push(start.elapsed().as_secs_f64() * 1_000_000.0);
122 }
123
124 samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
126 let p50_us = percentile_sorted(&samples, 50.0);
127 let p99_us = percentile_sorted(&samples, 99.0);
128
129 CalibrationResult {
130 offload_overhead_us: p50_us, p50_us,
132 p99_us,
133 }
134}
135
136fn percentile_sorted(sorted: &[f64], pct: f64) -> f64 {
138 if sorted.is_empty() {
139 return 0.0;
140 }
141 let idx = ((pct / 100.0) * (sorted.len() - 1) as f64).round() as usize;
142 sorted[idx.min(sorted.len() - 1)]
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_calibration_config_defaults() {
151 let config = CalibrationConfig::default();
152 assert!(!config.enabled);
153 assert_eq!(config.warmup_iterations, 100);
154 assert_eq!(config.sample_count, 1000);
155 }
156
157 #[test]
158 fn test_calibration_config_builder() {
159 let config = CalibrationConfig::new()
160 .enabled()
161 .warmup_iterations(50)
162 .sample_count(500);
163
164 assert!(config.enabled);
165 assert_eq!(config.warmup_iterations, 50);
166 assert_eq!(config.sample_count, 500);
167 }
168
169 #[test]
170 fn test_percentile_sorted() {
171 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
172
173 assert!((percentile_sorted(&data, 0.0) - 0.0).abs() < 0.5);
174 assert!((percentile_sorted(&data, 50.0) - 50.0).abs() < 0.5);
175 assert!((percentile_sorted(&data, 100.0) - 99.0).abs() < 0.5);
176 }
177
178 #[test]
179 fn test_percentile_sorted_empty() {
180 let data: Vec<f64> = vec![];
181 assert_eq!(percentile_sorted(&data, 50.0), 0.0);
182 }
183
184 #[test]
185 fn test_calibration_config_serialization() {
186 let config = CalibrationConfig::default();
187 let json = serde_json::to_string(&config).unwrap();
188 let parsed: CalibrationConfig = serde_json::from_str(&json).unwrap();
189 assert_eq!(parsed.enabled, config.enabled);
190 assert_eq!(parsed.warmup_iterations, config.warmup_iterations);
191 }
192}