Skip to main content

estimate_timeline/
lib.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use std::collections::VecDeque;
6
7const NANOS_PER_MILLISECOND: i64 = 1_000_000;
8const TIMELINE_DRIFT_THRESHOLD_MS: TimeOffsetMillis = TimeOffsetMillis(200);
9const TIMELINE_BOOT_TIME_THRESHOLD_MS: TimeOffsetMillis = TimeOffsetMillis(200);
10const TIMELINE_MAX_SIZE: usize = 400;
11const TIMELINE_TRIM_SIZE: usize = 200;
12
13/// Represents a time offset in milliseconds.
14#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
15struct TimeOffsetMillis(i64);
16
17impl From<zx::BootInstant> for TimeOffsetMillis {
18    fn from(value: zx::BootInstant) -> Self {
19        Self(value.into_nanos() / NANOS_PER_MILLISECOND)
20    }
21}
22
23impl From<zx::MonotonicInstant> for TimeOffsetMillis {
24    fn from(value: zx::MonotonicInstant) -> Self {
25        Self(value.into_nanos() / NANOS_PER_MILLISECOND)
26    }
27}
28
29impl TimeOffsetMillis {
30    fn to_nanos(&self) -> i64 {
31        self.0 * NANOS_PER_MILLISECOND
32    }
33}
34
35impl std::ops::Add for TimeOffsetMillis {
36    type Output = Self;
37
38    fn add(self, rhs: Self) -> Self::Output {
39        Self(self.0 + rhs.0)
40    }
41}
42
43impl std::ops::Sub for TimeOffsetMillis {
44    type Output = Self;
45
46    fn sub(self, rhs: Self) -> Self::Output {
47        Self(self.0 - rhs.0)
48    }
49}
50
51// Time fetcher interface allowing for zero-overhead fakes of time
52// for testing purposes. In production code this is static (not dyn,
53// ensuring no runtime overhead in production caused by tests).
54// The fuchsia_async fake time can't be used as Starnix has unique
55// runtime constraints and we can't rely on there being an active async context.
56pub trait TimeFetcher {
57    // Fetches the current monotonic time
58    fn get_monotonic(&self) -> zx::MonotonicInstant;
59
60    // Fetches the current boot time
61    fn get_boot(&self) -> zx::BootInstant;
62}
63
64#[derive(Debug, Default)]
65pub struct DefaultFetcher;
66
67impl TimeFetcher for DefaultFetcher {
68    fn get_monotonic(&self) -> zx::MonotonicInstant {
69        zx::MonotonicInstant::get()
70    }
71
72    fn get_boot(&self) -> zx::BootInstant {
73        zx::BootInstant::get()
74    }
75}
76
77/// A single entry in the timeline, mapping a boot time to a monotonic offset.
78type TimelineEntry = (zx::BootInstant, TimeOffsetMillis);
79
80#[derive(Debug, Default)]
81pub struct TimelineEstimator<T: TimeFetcher> {
82    /// A timeline of (boot_time_nanos, monotonic_offset_millis) pairs.
83    /// The monotonic offset is calculated as (boot_time_nanos - monotonic_time_nanos) / 1,000,000.
84    timeline: VecDeque<TimelineEntry>,
85    /// Max timeline size value
86    max_timeline_size: u64,
87    /// Number of times the timeline has overflowed
88    timeline_overflows: u64,
89    time_fetcher: T,
90}
91
92impl<T: TimeFetcher> TimelineEstimator<T> {
93    pub fn new(time_fetcher: T) -> Self {
94        Self {
95            max_timeline_size: 0,
96            timeline_overflows: 0,
97            timeline: VecDeque::new(),
98            time_fetcher,
99        }
100    }
101
102    pub fn max_timeline_size(&self) -> u64 {
103        self.max_timeline_size
104    }
105
106    pub fn timeline_overflows(&self) -> u64 {
107        self.timeline_overflows
108    }
109
110    /// Converts a boot time in nanoseconds to a monotonic time in nanoseconds.
111    pub fn boot_time_to_monotonic_time(
112        &mut self,
113        boot_time: zx::BootInstant,
114    ) -> zx::MonotonicInstant {
115        let timeline = &mut self.timeline;
116        let current_boot_time = self.time_fetcher.get_boot();
117        let mut current_offset = TimeOffsetMillis::from(self.time_fetcher.get_boot())
118            - TimeOffsetMillis::from(self.time_fetcher.get_monotonic());
119        // Initialize time if needed
120        if timeline.is_empty() {
121            timeline.push_back((current_boot_time, current_offset));
122        }
123        let (last_recorded_boot_time, prev_offset) =
124            timeline.back().expect("timeline must have at least one entry after initialization");
125        if current_offset - *prev_offset > TIMELINE_DRIFT_THRESHOLD_MS {
126            // Monotonic drift has changed, insert new record.
127            // Note that this measurement may be erroneous on CQ bots
128            // which can be preempted for multiple seconds at a time.
129            // We need to check for that before doing an update
130            let new_boot_time = TimeOffsetMillis::from(self.time_fetcher.get_boot());
131            let prev_boot_time = TimeOffsetMillis::from(current_boot_time);
132            if new_boot_time - prev_boot_time < TIMELINE_BOOT_TIME_THRESHOLD_MS {
133                if !((current_boot_time < boot_time) || boot_time < *last_recorded_boot_time) {
134                    // We tend to be one of the last components to wakeup and many other components
135                    // tend to log first. If we get a log message with an earlier timestamp, assume
136                    // that the timestamp on that log is in our current time offset.
137                    if boot_time != current_boot_time {
138                        timeline.push_back((boot_time, current_offset));
139                    }
140                }
141                timeline.push_back((current_boot_time, current_offset));
142            }
143            if timeline.len() > TIMELINE_MAX_SIZE {
144                // Keep only the first 200 elements.
145                while timeline.len() > TIMELINE_TRIM_SIZE {
146                    timeline.pop_front();
147                }
148                self.timeline_overflows += 1;
149            }
150            if timeline.len() > self.max_timeline_size as usize {
151                self.max_timeline_size = timeline.len() as u64;
152            }
153        }
154
155        // Find the offset that was active at or before the given boot_time.
156        for (boot, offset) in timeline.iter() {
157            if *boot > boot_time {
158                break;
159            }
160            current_offset = *offset;
161        }
162        // Monotonic time = Boot time - Offset
163        zx::MonotonicInstant::from_nanos(
164            (TimeOffsetMillis::from(boot_time) - current_offset).to_nanos(),
165        )
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use std::sync::Arc;
172
173    use super::*;
174
175    #[derive(Clone)]
176    struct FakeFetcher {
177        inner: Arc<std::sync::Mutex<FakeFetcherInner>>,
178    }
179
180    struct FakeFetcherInner {
181        monotonic: zx::MonotonicInstant,
182        boot: zx::BootInstant,
183    }
184
185    impl FakeFetcher {
186        fn new() -> Self {
187            Self {
188                inner: Arc::new(std::sync::Mutex::new(FakeFetcherInner {
189                    monotonic: zx::MonotonicInstant::from_nanos(0),
190                    boot: zx::BootInstant::from_nanos(0),
191                })),
192            }
193        }
194
195        fn set_times(&self, boot: zx::BootInstant, monotonic: zx::MonotonicInstant) {
196            let mut inner = self.inner.lock().unwrap();
197            inner.boot = boot;
198            inner.monotonic = monotonic;
199        }
200    }
201
202    impl TimeFetcher for FakeFetcher {
203        fn get_monotonic(&self) -> zx::MonotonicInstant {
204            self.inner.lock().unwrap().monotonic
205        }
206
207        fn get_boot(&self) -> zx::BootInstant {
208            self.inner.lock().unwrap().boot
209        }
210    }
211
212    #[test]
213    fn test_boot_time_to_monotonic_time() {
214        let fetcher = FakeFetcher::new();
215        // Initial state: boot=0, mono=0.
216        // We want to start with something non-zero to be interesting.
217        fetcher.set_times(
218            zx::BootInstant::from_nanos(1000 * NANOS_PER_MILLISECOND),
219            zx::MonotonicInstant::from_nanos(100 * NANOS_PER_MILLISECOND),
220        ); // boot=1000ms, mono=100ms
221
222        let mut state = TimelineEstimator::new(fetcher.clone());
223
224        // First call initializes timeline.
225        // boot=1000, mono=100. Offset=900.
226        // Record: (1000, 900).
227        let mono = state
228            .boot_time_to_monotonic_time(zx::BootInstant::from_nanos(1000 * NANOS_PER_MILLISECOND));
229        assert_eq!(mono.into_nanos(), 100 * NANOS_PER_MILLISECOND);
230
231        // Advance 100ms (no drift change).
232        // boot=1100, mono=200. Offset=900.
233        fetcher.set_times(
234            zx::BootInstant::from_nanos(1100 * NANOS_PER_MILLISECOND),
235            zx::MonotonicInstant::from_nanos(200 * NANOS_PER_MILLISECOND),
236        );
237
238        // Query for current time.
239        let mono = state
240            .boot_time_to_monotonic_time(zx::BootInstant::from_nanos(1100 * NANOS_PER_MILLISECOND));
241        assert_eq!(mono.into_nanos(), 200 * NANOS_PER_MILLISECOND);
242
243        // Query for past time (1050ms).
244        // Should use offset 900. 1050 - 900 = 150.
245        let mono = state
246            .boot_time_to_monotonic_time(zx::BootInstant::from_nanos(1050 * NANOS_PER_MILLISECOND));
247        assert_eq!(mono.into_nanos(), 150 * NANOS_PER_MILLISECOND);
248    }
249
250    #[test]
251    fn test_boot_time_to_monotonic_time_suspend() {
252        let fetcher = FakeFetcher::new();
253
254        fetcher.set_times(
255            zx::BootInstant::from_nanos(1000 * NANOS_PER_MILLISECOND),
256            zx::MonotonicInstant::from_nanos(100 * NANOS_PER_MILLISECOND),
257        );
258
259        let mut state = TimelineEstimator::new(fetcher.clone());
260
261        // Initialize
262        state
263            .boot_time_to_monotonic_time(zx::BootInstant::from_nanos(1000 * NANOS_PER_MILLISECOND));
264
265        // Suspend: boot + 1000ms, mono + 0ms.
266        // boot=2000, mono=100. Offset=1900.
267        fetcher.set_times(
268            zx::BootInstant::from_nanos(2000 * NANOS_PER_MILLISECOND),
269            zx::MonotonicInstant::from_nanos(100 * NANOS_PER_MILLISECOND),
270        );
271
272        // Call to update timeline.
273        // This should detect drift (1900 - 900 = 1000 > 200).
274        // Pushes (2000, 1900).
275        let mono = state
276            .boot_time_to_monotonic_time(zx::BootInstant::from_nanos(2000 * NANOS_PER_MILLISECOND));
277        assert_eq!(mono.into_nanos(), 100 * NANOS_PER_MILLISECOND);
278
279        assert_eq!(state.max_timeline_size, 2);
280
281        // Check past time lookups.
282        // At 1500 (during suspend). uses offset 900 -> 600.
283        let mono = state
284            .boot_time_to_monotonic_time(zx::BootInstant::from_nanos(1500 * NANOS_PER_MILLISECOND));
285        assert_eq!(mono.into_nanos(), 600 * NANOS_PER_MILLISECOND);
286
287        // At 2000 (just woke up). uses offset 1900 -> 100.
288        let mono = state
289            .boot_time_to_monotonic_time(zx::BootInstant::from_nanos(2000 * NANOS_PER_MILLISECOND));
290        assert_eq!(mono.into_nanos(), 100 * NANOS_PER_MILLISECOND);
291    }
292
293    #[test]
294    fn test_boot_time_to_monotonic_time_overflow() {
295        let fetcher = FakeFetcher::new();
296        fetcher.set_times(zx::BootInstant::from_nanos(0), zx::MonotonicInstant::from_nanos(0));
297
298        let mut state = TimelineEstimator::new(fetcher.clone());
299        state.boot_time_to_monotonic_time(zx::BootInstant::from_nanos(0));
300
301        // We want to trigger overflow. Max size is 400.
302
303        let mut current_boot = 0;
304        let current_mono = 0;
305
306        for _ in 0..500 {
307            // Drift needs to change.
308            // Increase boot by 300ms, mono by 0.
309            current_boot += 300 * NANOS_PER_MILLISECOND;
310            fetcher.set_times(
311                zx::BootInstant::from_nanos(current_boot),
312                zx::MonotonicInstant::from_nanos(current_mono),
313            );
314
315            state.boot_time_to_monotonic_time(zx::BootInstant::from_nanos(current_boot));
316        }
317
318        // Should have trimmed.
319        assert!(state.timeline.len() <= TIMELINE_MAX_SIZE);
320        assert!(state.timeline.len() >= TIMELINE_TRIM_SIZE);
321        assert_eq!(state.max_timeline_size, (TIMELINE_MAX_SIZE) as u64);
322
323        // 300 items expected (200 retained + 100 added).
324        assert_eq!(state.timeline.len(), 300);
325
326        for _ in 0..200 {
327            current_boot += 300 * NANOS_PER_MILLISECOND;
328            fetcher.set_times(
329                zx::BootInstant::from_nanos(current_boot),
330                zx::MonotonicInstant::from_nanos(current_mono),
331            );
332            state.boot_time_to_monotonic_time(zx::BootInstant::from_nanos(current_boot));
333        }
334        // 300 + 200 = 500 total in loop.
335        // At 401 trimmed to 200. Added 99 -> 299.
336        assert_eq!(state.timeline.len(), 299);
337    }
338
339    #[test]
340    fn test_boot_time_to_monotonic_time_intermediate_update() {
341        let fetcher = FakeFetcher::new();
342        // Initial: boot=2000, mono=1000. Offset=1000.
343        fetcher.set_times(
344            zx::BootInstant::from_nanos(2000 * NANOS_PER_MILLISECOND),
345            zx::MonotonicInstant::from_nanos(1000 * NANOS_PER_MILLISECOND),
346        );
347
348        let mut state = TimelineEstimator::new(fetcher.clone());
349        // This has the side effect of performing a timeline transformation
350        // (when the clock is set to a specific value). This has the side-effect of updating
351        // the estimation state to a value that we want it in for subsequent tests.
352        // For intermediate updates, we look at not just clock values,
353        // but also timestamps from logs. This simulates a log coming in at 2 seconds after
354        // the system boots.
355        state
356            .boot_time_to_monotonic_time(zx::BootInstant::from_nanos(2000 * NANOS_PER_MILLISECOND));
357
358        // Update with small boot delta (<200ms) but large drift (>200ms).
359        // boot=2150 (+150ms). mono=600 (-400ms).
360        // offset=1550. Drift=550.
361        fetcher.set_times(
362            zx::BootInstant::from_nanos(2150 * NANOS_PER_MILLISECOND),
363            zx::MonotonicInstant::from_nanos(600 * NANOS_PER_MILLISECOND),
364        );
365
366        // Query with intermediate time 2100.
367        // Should insert (2100, 1550) and (2150, 1550).
368        let mono = state
369            .boot_time_to_monotonic_time(zx::BootInstant::from_nanos(2100 * NANOS_PER_MILLISECOND));
370
371        // Expected mono = 2100 - 1550 = 550.
372        assert_eq!(mono.into_nanos(), 550 * NANOS_PER_MILLISECOND);
373
374        // Check timeline behavior.
375        // Query 2050 (before 2100). Should use old offset 1000.
376        // 2050 - 1000 = 1050.
377        let mono_prev = state
378            .boot_time_to_monotonic_time(zx::BootInstant::from_nanos(2050 * NANOS_PER_MILLISECOND));
379        assert_eq!(mono_prev.into_nanos(), 1050 * NANOS_PER_MILLISECOND);
380
381        // Query 2125 (after 2100). Should use new offset 1550.
382        // 2125 - 1550 = 575.
383        let mono_next = state
384            .boot_time_to_monotonic_time(zx::BootInstant::from_nanos(2125 * NANOS_PER_MILLISECOND));
385        assert_eq!(mono_next.into_nanos(), 575 * NANOS_PER_MILLISECOND);
386
387        // Query 2150 (current). Offset 1550.
388        // 2150 - 1550 = 600.
389        let mono_curr = state
390            .boot_time_to_monotonic_time(zx::BootInstant::from_nanos(2150 * NANOS_PER_MILLISECOND));
391        assert_eq!(mono_curr.into_nanos(), 600 * NANOS_PER_MILLISECOND);
392
393        // Ensure size is correct (should be 3: 2000, 2100, 2150).
394        assert_eq!(state.max_timeline_size, 3);
395    }
396
397    #[test]
398    fn test_boot_time_to_monotonic_time_retroactive_update() {
399        let fetcher = FakeFetcher::new();
400        // Initial: boot=1000, mono=100. Offset=900.
401        fetcher.set_times(
402            zx::BootInstant::from_nanos(1000 * NANOS_PER_MILLISECOND),
403            zx::MonotonicInstant::from_nanos(100 * NANOS_PER_MILLISECOND),
404        );
405
406        let mut state = TimelineEstimator::new(fetcher.clone());
407        state
408            .boot_time_to_monotonic_time(zx::BootInstant::from_nanos(1000 * NANOS_PER_MILLISECOND));
409
410        // Advance to boot=5000, mono=2000. Offset=3000.
411        // Drift = 3000 - 900 = 2100 > 200.
412        fetcher.set_times(
413            zx::BootInstant::from_nanos(5000 * NANOS_PER_MILLISECOND),
414            zx::MonotonicInstant::from_nanos(2000 * NANOS_PER_MILLISECOND),
415        );
416
417        // Query with boot=4500.
418        // 1000 < 4500 < 5000.
419        // Should trigger retroactive update. Timeline should have (4500, 3000).
420        // Result should use offset 3000.
421        // 4500 - 3000 = 1500.
422        let mono = state
423            .boot_time_to_monotonic_time(zx::BootInstant::from_nanos(4500 * NANOS_PER_MILLISECOND));
424        assert_eq!(mono.into_nanos(), 1500 * NANOS_PER_MILLISECOND);
425
426        // Verify the timeline explicitly if possible, or via another query.
427        // If we query 4600, it should also use offset 3000.
428        let mono_next = state
429            .boot_time_to_monotonic_time(zx::BootInstant::from_nanos(4600 * NANOS_PER_MILLISECOND));
430        assert_eq!(mono_next.into_nanos(), 1600 * NANOS_PER_MILLISECOND);
431
432        // If we query 4400, it should use offset 900 (from first record).
433        // 4400 - 900 = 3500.
434        let mono_prev = state
435            .boot_time_to_monotonic_time(zx::BootInstant::from_nanos(4400 * NANOS_PER_MILLISECOND));
436        assert_eq!(mono_prev.into_nanos(), 3500 * NANOS_PER_MILLISECOND);
437    }
438}