Skip to main content

ixa/profiling/
computed_statistic.rs

1use std::fmt::Display;
2
3use serde::ser::{Serialize, Serializer};
4
5#[cfg(feature = "profiling")]
6use super::profiling_data;
7use super::ProfilingData;
8
9pub type CustomStatisticComputer<T> = Box<dyn (Fn(&ProfilingData) -> Option<T>) + Send + Sync>;
10pub type CustomStatisticPrinter<T> = Box<dyn Fn(T) + Send + Sync>;
11
12pub(super) enum ComputedStatisticFunctions {
13    USize {
14        computer: CustomStatisticComputer<usize>,
15        printer: CustomStatisticPrinter<usize>,
16    },
17    Int {
18        computer: CustomStatisticComputer<i64>,
19        printer: CustomStatisticPrinter<i64>,
20    },
21    Float {
22        computer: CustomStatisticComputer<f64>,
23        printer: CustomStatisticPrinter<f64>,
24    },
25}
26
27impl ComputedStatisticFunctions {
28    /// A type erased way to compute a statistic.
29    pub(super) fn compute(&self, container: &ProfilingData) -> Option<ComputedValue> {
30        match self {
31            ComputedStatisticFunctions::USize { computer, .. } => {
32                computer(container).map(ComputedValue::USize)
33            }
34            ComputedStatisticFunctions::Int { computer, .. } => {
35                computer(container).map(ComputedValue::Int)
36            }
37            ComputedStatisticFunctions::Float { computer, .. } => {
38                computer(container).map(ComputedValue::Float)
39            }
40        }
41    }
42
43    /// A type erased way to print a statistic.
44    pub(super) fn print(&self, value: ComputedValue) {
45        match value {
46            ComputedValue::USize(value) => {
47                let ComputedStatisticFunctions::USize { printer, .. } = self else {
48                    unreachable!(
49                        "Ixa internal error: computed statistic value and printer type disagree",
50                    )
51                };
52                (printer)(value);
53            }
54            ComputedValue::Int(value) => {
55                let ComputedStatisticFunctions::Int { printer, .. } = self else {
56                    unreachable!(
57                        "Ixa internal error: computed statistic value and printer type disagree",
58                    )
59                };
60                (printer)(value);
61            }
62            ComputedValue::Float(value) => {
63                let ComputedStatisticFunctions::Float { printer, .. } = self else {
64                    unreachable!(
65                        "Ixa internal error: computed statistic value and printer type disagree",
66                    )
67                };
68                (printer)(value);
69            }
70        }
71    }
72}
73
74pub(super) struct ComputedStatistic {
75    /// The label used for the statistic in the JSON report.
76    pub label: &'static str,
77    /// Description of the statistic. Used in the JSON report.
78    pub description: &'static str,
79    /// The computed value of the statistic.
80    pub value: Option<ComputedValue>,
81    /// The two functions used to compute the statistic and to print it to the console.
82    pub functions: ComputedStatisticFunctions,
83}
84
85// This trick makes it so client code can _use_ `ComputableType` but not _implement_ it.
86mod sealed {
87    pub(super) trait SealedComputableType {}
88}
89#[allow(private_bounds)]
90pub trait ComputableType: sealed::SealedComputableType
91where
92    Self: Sized,
93{
94    // This method is only callable from within this crate.
95    #[allow(private_interfaces)]
96    fn new_functions(
97        computer: CustomStatisticComputer<Self>,
98        printer: CustomStatisticPrinter<Self>,
99    ) -> ComputedStatisticFunctions;
100}
101impl sealed::SealedComputableType for usize {}
102impl ComputableType for usize {
103    #[allow(private_interfaces)]
104    fn new_functions(
105        computer: CustomStatisticComputer<Self>,
106        printer: CustomStatisticPrinter<Self>,
107    ) -> ComputedStatisticFunctions {
108        ComputedStatisticFunctions::USize { computer, printer }
109    }
110}
111impl sealed::SealedComputableType for i64 {}
112impl ComputableType for i64 {
113    #[allow(private_interfaces)]
114    fn new_functions(
115        computer: CustomStatisticComputer<Self>,
116        printer: CustomStatisticPrinter<Self>,
117    ) -> ComputedStatisticFunctions {
118        ComputedStatisticFunctions::Int { computer, printer }
119    }
120}
121impl sealed::SealedComputableType for f64 {}
122impl ComputableType for f64 {
123    #[allow(private_interfaces)]
124    fn new_functions(
125        computer: CustomStatisticComputer<Self>,
126        printer: CustomStatisticPrinter<Self>,
127    ) -> ComputedStatisticFunctions {
128        ComputedStatisticFunctions::Float { computer, printer }
129    }
130}
131
132/// The computed value of a statistic. The "computer" returns a value of this type.
133#[derive(Copy, Clone, PartialEq, Debug)]
134pub(super) enum ComputedValue {
135    USize(usize),
136    Int(i64),
137    Float(f64),
138}
139
140impl Serialize for ComputedValue {
141    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
142        match self {
143            ComputedValue::USize(v) => serializer.serialize_u64(*v as u64),
144            ComputedValue::Int(v) => serializer.serialize_i64(*v),
145            ComputedValue::Float(v) => serializer.serialize_f64(*v),
146        }
147    }
148}
149
150impl Display for ComputedValue {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        match self {
153            ComputedValue::USize(value) => {
154                write!(f, "{}", value)
155            }
156
157            ComputedValue::Int(value) => {
158                write!(f, "{}", value)
159            }
160
161            ComputedValue::Float(value) => {
162                write!(f, "{}", value)
163            }
164        }
165    }
166}
167
168#[cfg(feature = "profiling")]
169pub fn add_computed_statistic<T: ComputableType>(
170    label: &'static str,
171    description: &'static str,
172    computer: CustomStatisticComputer<T>,
173    printer: CustomStatisticPrinter<T>,
174) {
175    let mut container = profiling_data();
176    container.add_computed_statistic(label, description, computer, printer);
177}
178#[cfg(not(feature = "profiling"))]
179pub fn add_computed_statistic<T: ComputableType>(
180    _label: &'static str,
181    _description: &'static str,
182    _computer: CustomStatisticComputer<T>,
183    _printer: CustomStatisticPrinter<T>,
184) {
185}
186
187#[cfg(all(test, feature = "profiling"))]
188mod tests {
189    use std::sync::atomic::{AtomicBool, Ordering};
190
191    use super::*;
192    use crate::profiling::{get_profiling_data, increment_named_count};
193
194    #[test]
195    fn test_computed_statistic_usize() {
196        increment_named_count("comp_stat_events_usize_test");
197        increment_named_count("comp_stat_events_usize_test");
198        increment_named_count("comp_stat_events_usize_test");
199
200        add_computed_statistic::<usize>(
201            "comp_stat_total_events",
202            "Total number of events",
203            Box::new(|data| data.get_named_count("comp_stat_events_usize_test")),
204            Box::new(|value| println!("Total events: {}", value)),
205        );
206
207        let data = get_profiling_data();
208
209        let stat = data
210            .computed_statistics
211            .iter()
212            .find_map(|s| {
213                s.as_ref()
214                    .filter(|stat| stat.label == "comp_stat_total_events")
215            })
216            .expect("total_events statistic not found");
217        let computed = stat.functions.compute(&data);
218        assert_eq!(computed, Some(ComputedValue::USize(3)));
219    }
220
221    #[test]
222    fn test_computed_statistic_i64() {
223        increment_named_count("comp_stat_positive_i64_test");
224        increment_named_count("comp_stat_positive_i64_test");
225        increment_named_count("comp_stat_negative_i64_test");
226
227        add_computed_statistic::<i64>(
228            "comp_stat_difference",
229            "Difference between positive and negative",
230            Box::new(|data| {
231                let pos = data
232                    .get_named_count("comp_stat_positive_i64_test")
233                    .unwrap_or(0) as i64;
234                let neg = data
235                    .get_named_count("comp_stat_negative_i64_test")
236                    .unwrap_or(0) as i64;
237                Some(pos - neg)
238            }),
239            Box::new(|value| println!("Difference: {}", value)),
240        );
241
242        let data = get_profiling_data();
243        let stat = data
244            .computed_statistics
245            .iter()
246            .find_map(|s| {
247                s.as_ref()
248                    .filter(|stat| stat.label == "comp_stat_difference")
249            })
250            .expect("difference statistic not found");
251        let computed = stat.functions.compute(&data);
252        assert_eq!(computed, Some(ComputedValue::Int(1)));
253    }
254
255    #[test]
256    fn test_computed_statistic_f64() {
257        {
258            let mut data = get_profiling_data();
259            *data
260                .counts
261                .entry("comp_stat_successes_f64_test")
262                .or_insert(0) += 3;
263            *data.counts.entry("comp_stat_total_f64_test").or_insert(0) += 4;
264            data.add_computed_statistic::<f64>(
265                "comp_stat_success_rate",
266                "Success rate as percentage",
267                Box::new(|data| {
268                    let successes = data.get_named_count("comp_stat_successes_f64_test")? as f64;
269                    let total = data.get_named_count("comp_stat_total_f64_test")? as f64;
270                    Some(successes / total * 100.0)
271                }),
272                Box::new(|value| println!("Success rate: {:.2}%", value)),
273            );
274
275            let stat = data
276                .computed_statistics
277                .iter()
278                .find_map(|s| {
279                    s.as_ref()
280                        .filter(|stat| stat.label == "comp_stat_success_rate")
281                })
282                .expect("comp_stat_success_rate statistic not found");
283            let computed = stat.functions.compute(&data);
284            if let Some(ComputedValue::Float(value)) = computed {
285                assert!((value - 75.0).abs() < 0.01);
286            } else {
287                panic!("Expected Float value, got {:?}", computed);
288            }
289        }
290    }
291
292    #[test]
293    fn test_computed_statistic_returns_none() {
294        add_computed_statistic::<usize>(
295            "comp_stat_missing_data",
296            "Statistic with missing data",
297            Box::new(|data| data.get_named_count("comp_stat_nonexistent")),
298            Box::new(|value| println!("Value: {}", value)),
299        );
300
301        let data = get_profiling_data();
302        let stat = data
303            .computed_statistics
304            .iter()
305            .find_map(|s| {
306                s.as_ref()
307                    .filter(|stat| stat.label == "comp_stat_missing_data")
308            })
309            .expect("comp_stat_missing_data statistic not found");
310        let computed = stat.functions.compute(&data);
311        assert_eq!(computed, None);
312    }
313
314    #[test]
315    fn test_computed_value_display() {
316        let usize_val = ComputedValue::USize(42);
317        assert_eq!(format!("{}", usize_val), "42");
318
319        let int_val = ComputedValue::Int(-100);
320        assert_eq!(format!("{}", int_val), "-100");
321
322        let float_val = ComputedValue::Float(std::f64::consts::PI);
323        assert_eq!(format!("{}", float_val), "3.141592653589793");
324    }
325
326    #[test]
327    fn test_computed_statistic_print_functions() {
328        static PRINTED: AtomicBool = AtomicBool::new(false);
329
330        // Reset the static variable
331        PRINTED.store(false, Ordering::SeqCst);
332
333        increment_named_count("comp_stat_test_print_func");
334
335        add_computed_statistic::<usize>(
336            "comp_stat_test_stat",
337            "Test statistic",
338            Box::new(|data| data.get_named_count("comp_stat_test_print_func")),
339            Box::new(|_value| {
340                PRINTED.store(true, Ordering::SeqCst);
341            }),
342        );
343
344        let data = get_profiling_data();
345        let stat = data
346            .computed_statistics
347            .iter()
348            .find_map(|s| {
349                s.as_ref()
350                    .filter(|stat| stat.label == "comp_stat_test_stat")
351            })
352            .expect("test_stat statistic not found");
353        let value = stat.functions.compute(&data).unwrap();
354        stat.functions.print(value);
355
356        assert!(PRINTED.load(Ordering::SeqCst));
357    }
358
359    #[test]
360    #[should_panic(
361        expected = "Ixa internal error: computed statistic value and printer type disagree"
362    )]
363    fn mismatched_computed_statistic_printer_panics() {
364        let functions = ComputedStatisticFunctions::USize {
365            computer: Box::new(|_| Some(0)),
366            printer: Box::new(|_| {}),
367        };
368
369        functions.print(ComputedValue::Int(0));
370    }
371}