Skip to main content

tacet_core/preflight/
resolution.rs

1//! Timer resolution check.
2//!
3//! This check detects when the timer resolution is too coarse relative to
4//! the operation being measured, which leads to unreliable statistics.
5//!
6//! On ARM (aarch64), the virtual timer runs at ~24 MHz (~41ns resolution),
7//! not at CPU frequency. For operations faster than the timer resolution,
8//! most measurements will be 0 or 1 tick, making statistical analysis meaningless.
9//!
10//! **Severity**: Mixed
11//!
12//! - `InsufficientResolution`: ResultUndermining - measurements are too quantized
13//! - `HighQuantization`: Informational - some quantization but still useful
14
15extern crate alloc;
16
17use alloc::string::String;
18use alloc::vec::Vec;
19
20use crate::result::{PreflightCategory, PreflightSeverity, PreflightWarningInfo};
21
22/// Warning from the resolution check.
23#[derive(Debug, Clone)]
24#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
25pub enum ResolutionWarning {
26    /// Timer resolution is too coarse for the operation being measured.
27    ///
28    /// **Severity**: ResultUndermining
29    ///
30    /// The statistical analysis will be unreliable because most measurements
31    /// are quantized to the same few values.
32    InsufficientResolution {
33        /// Number of unique timing values observed.
34        unique_values: usize,
35        /// Total number of samples.
36        total_samples: usize,
37        /// Fraction of samples that were exactly zero.
38        zero_fraction: f64,
39        /// Estimated timer resolution in nanoseconds.
40        timer_resolution_ns: f64,
41    },
42
43    /// Many samples have identical timing values.
44    ///
45    /// **Severity**: Informational
46    ///
47    /// This may indicate quantization effects from coarse timer resolution,
48    /// but the statistical analysis is still valid.
49    HighQuantization {
50        /// Number of unique timing values observed.
51        unique_values: usize,
52        /// Total number of samples.
53        total_samples: usize,
54    },
55}
56
57impl ResolutionWarning {
58    /// Check if this warning undermines result confidence.
59    pub fn is_result_undermining(&self) -> bool {
60        matches!(self, ResolutionWarning::InsufficientResolution { .. })
61    }
62
63    /// Get the severity of this warning.
64    pub fn severity(&self) -> PreflightSeverity {
65        match self {
66            ResolutionWarning::InsufficientResolution { .. } => {
67                PreflightSeverity::ResultUndermining
68            }
69            ResolutionWarning::HighQuantization { .. } => PreflightSeverity::Informational,
70        }
71    }
72
73    /// Get a human-readable description of the warning.
74    pub fn description(&self) -> String {
75        match self {
76            ResolutionWarning::InsufficientResolution {
77                unique_values,
78                total_samples,
79                zero_fraction,
80                timer_resolution_ns,
81            } => {
82                alloc::format!(
83                    "Timer resolution (~{:.0}ns) is too coarse for this operation. \
84                     Only {} unique values in {} samples ({:.0}% are zero).",
85                    timer_resolution_ns,
86                    unique_values,
87                    total_samples,
88                    zero_fraction * 100.0
89                )
90            }
91            ResolutionWarning::HighQuantization {
92                unique_values,
93                total_samples,
94            } => {
95                alloc::format!(
96                    "High quantization: only {} unique values in {} samples. \
97                     Timer resolution may be affecting measurement quality.",
98                    unique_values,
99                    total_samples
100                )
101            }
102        }
103    }
104
105    /// Get guidance for addressing this warning.
106    ///
107    /// Note: This provides generic guidance. For context-aware recommendations
108    /// (e.g., based on timer fallback reason), see the output formatting layer.
109    pub fn guidance(&self) -> Option<String> {
110        match self {
111            ResolutionWarning::InsufficientResolution { .. } => Some(
112                "Consider: (1) measuring multiple iterations per sample, \
113                 (2) using a more complex operation, or \
114                 (3) enabling cycle-accurate timing for ~0.3ns resolution."
115                    .into(),
116            ),
117            ResolutionWarning::HighQuantization { .. } => {
118                Some("Timer resolution may be limiting measurement quality.".into())
119            }
120        }
121    }
122
123    /// Convert to a PreflightWarningInfo.
124    pub fn to_warning_info(&self) -> PreflightWarningInfo {
125        match self.guidance() {
126            Some(guidance) => PreflightWarningInfo::with_guidance(
127                PreflightCategory::Resolution,
128                self.severity(),
129                self.description(),
130                guidance,
131            ),
132            None => PreflightWarningInfo::new(
133                PreflightCategory::Resolution,
134                self.severity(),
135                self.description(),
136            ),
137        }
138    }
139}
140
141/// Minimum unique values expected per 1000 samples for reliable analysis.
142const MIN_UNIQUE_PER_1000: usize = 20;
143
144/// Fraction of zero values that triggers critical warning.
145const CRITICAL_ZERO_FRACTION: f64 = 0.5;
146
147/// Perform resolution check on timing samples.
148///
149/// Detects when timer resolution is too coarse by checking:
150/// 1. How many unique timing values exist
151/// 2. What fraction of samples are exactly zero
152///
153/// # Arguments
154///
155/// * `samples` - Timing samples in nanoseconds
156/// * `timer_resolution_ns` - Estimated timer resolution (e.g., from cycles_per_ns)
157///
158/// # Returns
159///
160/// A warning if resolution issues are detected, None otherwise.
161pub fn resolution_check(samples: &[f64], timer_resolution_ns: f64) -> Option<ResolutionWarning> {
162    if samples.len() < 100 {
163        return None; // Not enough samples to assess
164    }
165
166    // Count unique values (with small tolerance for floating point)
167    let mut sorted: Vec<f64> = samples.to_vec();
168    sorted.sort_by(|a, b| a.total_cmp(b));
169
170    let mut unique_count = 1;
171    let mut last_value = sorted[0];
172    for &val in &sorted[1..] {
173        // Consider values different if they differ by more than 0.1ns
174        if (val - last_value).abs() > 0.1 {
175            unique_count += 1;
176            last_value = val;
177        }
178    }
179
180    // Count zeros
181    let zero_count = samples.iter().filter(|&&x| x.abs() < 0.1).count();
182    let zero_fraction = zero_count as f64 / samples.len() as f64;
183
184    // Check for critical issue: very few unique values AND many zeros
185    let expected_unique =
186        (samples.len() as f64 / 1000.0 * MIN_UNIQUE_PER_1000 as f64).max(10.0) as usize;
187
188    if unique_count < expected_unique && zero_fraction > CRITICAL_ZERO_FRACTION {
189        return Some(ResolutionWarning::InsufficientResolution {
190            unique_values: unique_count,
191            total_samples: samples.len(),
192            zero_fraction,
193            timer_resolution_ns,
194        });
195    }
196
197    // Check for high quantization (less severe)
198    if unique_count < expected_unique / 2 {
199        return Some(ResolutionWarning::HighQuantization {
200            unique_values: unique_count,
201            total_samples: samples.len(),
202        });
203    }
204
205    None
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_insufficient_resolution() {
214        // Simulated ARM-style data: mostly zeros with occasional 41ns ticks
215        let mut samples = alloc::vec![0.0; 800];
216        samples.extend(alloc::vec![41.0; 150]);
217        samples.extend(alloc::vec![82.0; 50]);
218
219        let result = resolution_check(&samples, 41.0);
220        assert!(result.is_some(), "Should detect insufficient resolution");
221        assert!(
222            result.as_ref().unwrap().is_result_undermining(),
223            "Should be result-undermining"
224        );
225        assert_eq!(
226            result.as_ref().unwrap().severity(),
227            PreflightSeverity::ResultUndermining
228        );
229    }
230
231    #[test]
232    fn test_severity() {
233        let insufficient = ResolutionWarning::InsufficientResolution {
234            unique_values: 3,
235            total_samples: 1000,
236            zero_fraction: 0.8,
237            timer_resolution_ns: 41.0,
238        };
239        assert_eq!(
240            insufficient.severity(),
241            PreflightSeverity::ResultUndermining
242        );
243        assert!(insufficient.is_result_undermining());
244
245        let high_quant = ResolutionWarning::HighQuantization {
246            unique_values: 5,
247            total_samples: 1000,
248        };
249        assert_eq!(high_quant.severity(), PreflightSeverity::Informational);
250        assert!(!high_quant.is_result_undermining());
251    }
252}