chronix_analytics/preprocess/
clock_drift.rs1#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
5pub enum ClockDriftStrategy {
6 #[default]
8 None,
9 Monotonic,
12 Smooth,
27}
28
29#[derive(Debug, Clone)]
31pub struct ClockDriftReport {
32 pub max_drift_ns: i64,
34 pub affected_ranges: Vec<(usize, usize)>,
36 pub corrections_applied: usize,
38}
39
40pub struct ClockDriftDetector;
42
43impl ClockDriftDetector {
44 pub fn correct(
48 timestamps: &[i64],
49 expected_interval_ns: i64,
50 strategy: ClockDriftStrategy,
51 ) -> (Vec<i64>, ClockDriftReport) {
52 let result = match strategy {
53 ClockDriftStrategy::None => (
54 timestamps.to_vec(),
55 ClockDriftReport {
56 max_drift_ns: 0,
57 affected_ranges: Vec::new(),
58 corrections_applied: 0,
59 },
60 ),
61 ClockDriftStrategy::Monotonic => Self::monotonic(timestamps),
62 ClockDriftStrategy::Smooth => Self::smooth(timestamps, expected_interval_ns),
63 };
64 metrics::counter!("chronix_clock_drift_corrections_total")
65 .increment(result.1.corrections_applied as u64);
66 result
67 }
68
69 fn monotonic(timestamps: &[i64]) -> (Vec<i64>, ClockDriftReport) {
71 if timestamps.is_empty() {
72 return (
73 Vec::new(),
74 ClockDriftReport {
75 max_drift_ns: 0,
76 affected_ranges: Vec::new(),
77 corrections_applied: 0,
78 },
79 );
80 }
81
82 let mut corrected = Vec::with_capacity(timestamps.len());
83 corrected.push(timestamps[0]);
84
85 let mut max_drift: i64 = 0;
86 let mut corrections = 0usize;
87 let mut ranges: Vec<(usize, usize)> = Vec::new();
88 let mut in_range = false;
89 let mut range_start = 0usize;
90
91 for i in 1..timestamps.len() {
92 if timestamps[i] <= corrected[i - 1] {
93 let drift = corrected[i - 1] - timestamps[i] + 1;
94 max_drift = max_drift.max(drift);
95 corrected.push(corrected[i - 1] + 1);
96 corrections += 1;
97
98 if !in_range {
99 range_start = i;
100 in_range = true;
101 }
102 } else {
103 corrected.push(timestamps[i]);
104 if in_range {
105 ranges.push((range_start, i - 1));
106 in_range = false;
107 }
108 }
109 }
110
111 if in_range {
112 ranges.push((range_start, timestamps.len() - 1));
113 }
114
115 (
116 corrected,
117 ClockDriftReport {
118 max_drift_ns: max_drift,
119 affected_ranges: ranges,
120 corrections_applied: corrections,
121 },
122 )
123 }
124
125 fn smooth(timestamps: &[i64], expected_interval_ns: i64) -> (Vec<i64>, ClockDriftReport) {
128 if timestamps.len() < 3 {
129 return (
130 timestamps.to_vec(),
131 ClockDriftReport {
132 max_drift_ns: 0,
133 affected_ranges: Vec::new(),
134 corrections_applied: 0,
135 },
136 );
137 }
138
139 let n = timestamps.len();
140 let deltas: Vec<f64> = timestamps
141 .windows(2)
142 .map(|w| (w[1] - w[0]) as f64)
143 .collect();
144
145 let n_d = deltas.len() as f64;
148 let sum_x: f64 = (0..deltas.len()).map(|i| i as f64).sum();
149 let sum_y: f64 = deltas.iter().sum();
150 let sum_xy: f64 = deltas.iter().enumerate().map(|(i, &d)| i as f64 * d).sum();
151 let sum_xx: f64 = (0..deltas.len()).map(|i| (i as f64).powi(2)).sum();
152
153 let denom = n_d * sum_xx - sum_x * sum_x;
154 if denom.abs() < 1e-10 {
155 return (
156 timestamps.to_vec(),
157 ClockDriftReport {
158 max_drift_ns: 0,
159 affected_ranges: Vec::new(),
160 corrections_applied: 0,
161 },
162 );
163 }
164
165 let slope = (n_d * sum_xy - sum_x * sum_y) / denom;
166 let expected = expected_interval_ns as f64;
167
168 if slope.abs() < expected * 0.001 {
170 return (
171 timestamps.to_vec(),
172 ClockDriftReport {
173 max_drift_ns: 0,
174 affected_ranges: Vec::new(),
175 corrections_applied: 0,
176 },
177 );
178 }
179
180 let threshold = expected;
185 let mut corrected = Vec::with_capacity(n);
186 corrected.push(timestamps[0]);
187 let mut max_drift: i64 = 0;
188 for i in 1..n {
189 let ideal =
190 timestamps[0].saturating_add((i as i64).saturating_mul(expected_interval_ns));
191 let local_drift = (timestamps[i] - ideal).abs();
192 max_drift = max_drift.max(local_drift);
193 let alpha = if threshold > 0.0 {
194 (local_drift as f64 / threshold).clamp(0.0, 1.0)
195 } else {
196 0.0
197 };
198 let blended =
200 ((1.0 - alpha) * timestamps[i] as f64 + alpha * ideal as f64).round() as i64;
201 corrected.push(blended);
202 }
203
204 let corrections = (0..n).filter(|&i| timestamps[i] != corrected[i]).count();
206
207 (
208 corrected,
209 ClockDriftReport {
210 max_drift_ns: max_drift,
211 affected_ranges: if max_drift > 0 {
212 vec![(0, n - 1)]
213 } else {
214 Vec::new()
215 },
216 corrections_applied: corrections,
217 },
218 )
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn no_correction() {
228 let ts = vec![0, 100, 200, 300];
229 let (corrected, report) = ClockDriftDetector::correct(&ts, 100, ClockDriftStrategy::None);
230 assert_eq!(corrected, ts);
231 assert_eq!(report.corrections_applied, 0);
232 }
233
234 #[test]
235 fn monotonic_no_issue() {
236 let ts = vec![0, 100, 200, 300];
237 let (corrected, report) =
238 ClockDriftDetector::correct(&ts, 100, ClockDriftStrategy::Monotonic);
239 assert_eq!(corrected, ts);
240 assert_eq!(report.corrections_applied, 0);
241 }
242
243 #[test]
244 fn monotonic_backward_timestamps() {
245 let ts = vec![0, 100, 50, 200]; let (corrected, report) =
247 ClockDriftDetector::correct(&ts, 100, ClockDriftStrategy::Monotonic);
248 assert!(corrected[2] > corrected[1]); assert_eq!(corrected[2], 101); assert_eq!(report.corrections_applied, 1);
251 assert_eq!(report.affected_ranges.len(), 1);
252 }
253
254 #[test]
255 fn monotonic_duplicate_timestamps() {
256 let ts = vec![0, 100, 100, 100, 200];
257 let (corrected, report) =
258 ClockDriftDetector::correct(&ts, 100, ClockDriftStrategy::Monotonic);
259 assert_eq!(corrected[2], 101);
261 assert_eq!(corrected[3], 102);
262 assert_eq!(report.corrections_applied, 2);
263 }
264
265 #[test]
266 fn smooth_gradual_drift() {
267 let expected = 100;
269 let ts: Vec<i64> = (0..20)
270 .map(|i| i * expected + i * i * 5) .collect();
272 let (corrected, report) =
273 ClockDriftDetector::correct(&ts, expected, ClockDriftStrategy::Smooth);
274 let original_var: f64 = ts
276 .windows(2)
277 .map(|w| ((w[1] - w[0]) as f64 - expected as f64).powi(2))
278 .sum();
279 let corrected_var: f64 = corrected
280 .windows(2)
281 .map(|w| ((w[1] - w[0]) as f64 - expected as f64).powi(2))
282 .sum();
283 assert!(
284 corrected_var < original_var,
285 "corrected intervals should have less variance"
286 );
287 assert!(report.corrections_applied > 0);
288 }
289
290 #[test]
291 fn smooth_no_drift() {
292 let ts: Vec<i64> = (0..10).map(|i| i * 1000).collect();
293 let (corrected, report) =
294 ClockDriftDetector::correct(&ts, 1000, ClockDriftStrategy::Smooth);
295 assert_eq!(corrected, ts);
296 assert_eq!(report.corrections_applied, 0);
297 }
298
299 #[test]
300 fn empty_timestamps() {
301 let (corrected, report) =
302 ClockDriftDetector::correct(&[], 100, ClockDriftStrategy::Monotonic);
303 assert!(corrected.is_empty());
304 assert_eq!(report.corrections_applied, 0);
305 }
306}