Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-sim/src/clock.rs
Line
Count
Source
1
// Copyright 2026 Trace Machina, Inc. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    See LICENSE file for details
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
//! One virtual clock for every component: tokio's, paused. The scheduler,
16
//! its db and the servers read it through `SimInstant` and `SimClock::now`;
17
//! the workers and the simulated actions sleep on it directly. A jump moves
18
//! what `now` reads, the way an NTP step does, and no timer sees it.
19
//!
20
//! On the memory backend the clock moves only when every task is waiting on
21
//! a timer, so work costs no virtual time and a timeout fires exactly when
22
//! its duration has passed. The fake Redis is reached over a socket, and a
23
//! paused runtime that is idle while a reply is on its way jumps straight
24
//! to the next timer, a connect timeout included; there the runner keeps
25
//! the runtime busy and steps time itself, holding each step until the
26
//! store stops answering.
27
28
use core::cell::Cell;
29
use core::future::Future;
30
use core::time::Duration;
31
use std::sync::Arc;
32
use std::time::{SystemTime, UNIX_EPOCH};
33
34
use nativelink_util::instant_wrapper::InstantWrapper;
35
use tokio::time::Instant;
36
37
/// Where virtual time starts. Not zero: a zero timestamp reads as unset in
38
/// places.
39
pub const EPOCH_OFFSET: Duration = Duration::from_secs(1_000_000);
40
41
thread_local! {
42
    static START: Cell<Option<Instant>> = const { Cell::new(None) };
43
    static JUMPED: Cell<Duration> = const { Cell::new(Duration::ZERO) };
44
}
45
46
/// How far one manual step moves the clock.
47
pub const STEP: Duration = Duration::from_millis(10);
48
49
/// The most rounds of yields a step waits for a busy store to go quiet.
50
const MAX_QUIET_ROUNDS: usize = 1_000;
51
52
/// A counter of work outside the runtime's view, such as the commands a
53
/// store has answered; a manual step waits while it moves.
54
pub type ActivityProbe = Arc<dyn Fn() -> u64 + Send + Sync>;
55
56
#[derive(Clone, Default)]
57
pub struct SimClock {
58
    /// Set when time is stepped by hand.
59
    activity: Option<ActivityProbe>,
60
}
61
62
impl core::fmt::Debug for SimClock {
63
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
64
0
        f.debug_struct("SimClock")
65
0
            .field("stepped", &self.activity.is_some())
66
0
            .finish()
67
0
    }
68
}
69
70
impl SimClock {
71
    /// Starts virtual time on this thread. Call once per run, inside the
72
    /// paused runtime, before anything reads the time.
73
185
    pub fn start() -> Self {
74
185
        START.set(Some(Instant::now()));
75
185
        JUMPED.set(Duration::ZERO);
76
185
        Self { activity: None }
77
185
    }
78
79
    /// Steps time by hand, waiting at each step until `activity` stops
80
    /// moving. The caller keeps the runtime from going idle.
81
    #[must_use]
82
68
    pub fn stepped(mut self, activity: ActivityProbe) -> Self {
83
68
        self.activity = Some(activity);
84
68
        self
85
68
    }
86
87
18.7M
    fn elapsed() -> Duration {
88
18.7M
        START.get().map_or(Duration::ZERO, |start| start.elapsed()) + JUMPED.get()
89
18.7M
    }
90
91
    /// Milliseconds since the run started, jumps included.
92
160k
    pub fn now_ms() -> u64 {
93
160k
        u64::try_from(Self::elapsed().as_millis()).unwrap_or(u64::MAX)
94
160k
    }
95
96
    /// What the scheduler and the servers read as now.
97
18.6M
    pub fn now() -> SystemTime {
98
18.6M
        UNIX_EPOCH + EPOCH_OFFSET + Self::elapsed()
99
18.6M
    }
100
101
    /// Lets every runnable task run without moving time.
102
1.52M
    pub async fn yield_all() {
103
1.52M
        for _ in 0..8 {
104
12.1M
            tokio::task::yield_now().await;
105
        }
106
1.52M
    }
107
108
    /// Lets `by` of virtual time pass: timer by timer, each as soon as
109
    /// nothing is left to run, or in manual steps.
110
43.6k
    pub async fn advance(&self, by: Duration) {
111
43.6k
        let Some(
activity22.4k
) = &self.activity else {
112
21.1k
            tokio::time::sleep(by).await;
113
21.1k
            return;
114
        };
115
22.4k
        let mut left = by;
116
522k
        while !left.is_zero() {
117
500k
            let step = left.min(STEP);
118
500k
            tokio::time::advance(step).await;
119
500k
            Self::yield_all().await;
120
500k
            for _ in 0..MAX_QUIET_ROUNDS {
121
1.00M
                let before = activity();
122
1.00M
                Self::yield_all().await;
123
1.00M
                if activity() == before {
124
500k
                    break;
125
502k
                }
126
            }
127
500k
            left -= step;
128
        }
129
43.6k
    }
130
131
    /// Moves what `now` reads forward by `by`; timers do not see it. Forward
132
    /// only, as a clock that steps back breaks `Instant` arithmetic.
133
4
    pub fn jump(by: Duration) {
134
4
        JUMPED.set(JUMPED.get() + by);
135
4
    }
136
}
137
138
/// `InstantWrapper` over the virtual clock, for the scheduler and its db:
139
/// production's `SystemTime` wrapper with `SimClock::now` in place of the
140
/// wall clock, method for method. `from_secs(0)` is the epoch, so a fresh
141
/// subscriber's keepalive is long overdue, as in production, and `elapsed`
142
/// sees a jump, as `SystemTime` sees an NTP step.
143
#[derive(Debug, Clone, Copy)]
144
pub struct SimInstant(SystemTime);
145
146
impl SimInstant {
147
8.49M
    pub fn now() -> Self {
148
8.49M
        Self(SimClock::now())
149
8.49M
    }
150
}
151
152
impl InstantWrapper for SimInstant {
153
3.08M
    fn from_secs(secs: u64) -> Self {
154
3.08M
        Self(UNIX_EPOCH + Duration::from_secs(secs))
155
3.08M
    }
156
157
0
    fn unix_timestamp(&self) -> u64 {
158
0
        self.0.duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs())
159
0
    }
160
161
6.84M
    fn now(&self) -> SystemTime {
162
6.84M
        SimClock::now()
163
6.84M
    }
164
165
3.15M
    fn elapsed(&self) -> Duration {
166
3.15M
        SimClock::now().duration_since(self.0).unwrap_or_default()
167
3.15M
    }
168
169
1.63M
    fn sleep(self, duration: Duration) -> impl Future<Output = ()> + Send + Sync + 'static {
170
1.63M
        tokio::time::sleep(duration)
171
1.63M
    }
172
}