Skip to main content

ic_timers/
platform.rs

1//! Private boundary to IC system facts and `ic-cdk-timers`.
2//!
3//! Recurrence, identity, arbitration, inventory, and consumer work belong to
4//! the canonical runtime above this module.
5
6use std::{future::Future, time::Duration};
7
8#[cfg(not(test))]
9use ic_cdk_timers::{
10    TimerId as CdkTimerId, clear_timer as cdk_clear_timer, set_timer as cdk_set_timer,
11};
12
13/// Linear handle for one armed platform timer.
14#[cfg(not(test))]
15#[must_use = "bind or clear the platform timer handle"]
16#[derive(Debug, Eq, PartialEq)]
17pub struct TimerHandle(CdkTimerId);
18
19/// Copy-only page extents returned by the private system-fact boundary.
20#[derive(Clone, Copy)]
21pub struct MemoryPages {
22    wasm: u64,
23    stable: u64,
24}
25
26impl MemoryPages {
27    pub(crate) const fn wasm(self) -> u64 {
28        self.wasm
29    }
30
31    pub(crate) const fn stable(self) -> u64 {
32        self.stable
33    }
34}
35
36/// Arm one asynchronous one-shot callback.
37#[cfg(not(test))]
38pub fn set_timer(delay: Duration, task: impl Future<Output = ()> + 'static) -> TimerHandle {
39    TimerHandle(cdk_set_timer(delay, task))
40}
41
42/// Clear one still-armed platform callback.
43#[cfg(not(test))]
44#[allow(clippy::needless_pass_by_value)] // Clearing consumes the linear handle.
45pub fn clear_timer(handle: TimerHandle) {
46    cdk_clear_timer(handle.0);
47}
48
49/// Return current IC message time in nanoseconds.
50#[cfg(not(test))]
51pub fn time_ns() -> u64 {
52    ic0::time()
53}
54
55/// Return the current canister version.
56#[cfg(not(test))]
57pub fn canister_version() -> u64 {
58    ic0::canister_version()
59}
60
61/// Return call-context instruction consumption on the IC.
62#[cfg(not(test))]
63pub fn instruction_counter() -> u64 {
64    ic0::performance_counter(1)
65}
66
67/// Return current Wasm and stable memory extents in 64 KiB pages without
68/// allocation.
69#[cfg(not(test))]
70pub fn memory_pages() -> MemoryPages {
71    #[cfg(target_arch = "wasm32")]
72    let wasm = core::arch::wasm32::memory_size::<0>() as u64;
73    #[cfg(not(target_arch = "wasm32"))]
74    let wasm = 0;
75
76    MemoryPages {
77        wasm,
78        stable: ic0::stable64_size(),
79    }
80}
81
82/// Abort the current message when an internal callback invariant is violated.
83#[cfg(not(test))]
84pub fn trap(message: &str) -> ! {
85    ic0::trap(message.as_bytes())
86}
87
88#[cfg(test)]
89pub use fake::{
90    TimerHandle, advance_instructions, canister_version, clear_timer, discard_next_due,
91    grow_memory_pages, instruction_counter, memory_pages, reset, run_next_due, set_time, set_timer,
92    time_ns, timer_count, trap,
93};
94
95#[cfg(test)]
96mod fake {
97    use super::*;
98    use std::{
99        cell::{Cell, RefCell},
100        collections::BTreeMap,
101        pin::Pin,
102        task::{Context, Poll, Waker},
103    };
104
105    type Task = Pin<Box<dyn Future<Output = ()>>>;
106
107    struct ScheduledTask {
108        deadline_ns: u64,
109        task: Task,
110    }
111
112    #[must_use = "bind or clear the platform timer handle"]
113    #[derive(Debug, Eq, PartialEq)]
114    pub struct TimerHandle(u64);
115
116    thread_local! {
117        static NOW_NS: Cell<u64> = const { Cell::new(0) };
118        static CANISTER_VERSION: Cell<u64> = const { Cell::new(0) };
119        static NEXT_HANDLE: Cell<u64> = const { Cell::new(0) };
120        static INSTRUCTIONS: Cell<u64> = const { Cell::new(0) };
121        static WASM_MEMORY_PAGES: Cell<u64> = const { Cell::new(1) };
122        static STABLE_MEMORY_PAGES: Cell<u64> = const { Cell::new(0) };
123        static TASKS: RefCell<BTreeMap<u64, ScheduledTask>> = const {
124            RefCell::new(BTreeMap::new())
125        };
126    }
127
128    pub fn set_timer(delay: Duration, task: impl Future<Output = ()> + 'static) -> TimerHandle {
129        advance_instructions(5);
130        let delay_ns = u64::try_from(delay.as_nanos()).unwrap_or(u64::MAX);
131        let deadline_ns = time_ns().saturating_add(delay_ns);
132        let handle = NEXT_HANDLE.with(|next| {
133            let handle = next.get().saturating_add(1);
134            next.set(handle);
135            handle
136        });
137        TASKS.with(|tasks| {
138            tasks.borrow_mut().insert(
139                handle,
140                ScheduledTask {
141                    deadline_ns,
142                    task: Box::pin(task),
143                },
144            );
145        });
146        TimerHandle(handle)
147    }
148
149    #[allow(clippy::needless_pass_by_value)] // Clearing consumes the linear handle.
150    pub fn clear_timer(handle: TimerHandle) {
151        advance_instructions(2);
152        TASKS.with(|tasks| {
153            tasks.borrow_mut().remove(&handle.0);
154        });
155    }
156
157    pub fn time_ns() -> u64 {
158        NOW_NS.with(Cell::get)
159    }
160
161    pub fn canister_version() -> u64 {
162        CANISTER_VERSION.with(Cell::get)
163    }
164
165    pub fn instruction_counter() -> u64 {
166        INSTRUCTIONS.with(Cell::get)
167    }
168
169    pub fn memory_pages() -> MemoryPages {
170        MemoryPages {
171            wasm: WASM_MEMORY_PAGES.with(Cell::get),
172            stable: STABLE_MEMORY_PAGES.with(Cell::get),
173        }
174    }
175
176    pub fn trap(message: &str) -> ! {
177        panic!("{message}")
178    }
179
180    pub fn advance_instructions(amount: u64) {
181        INSTRUCTIONS.with(|instructions| {
182            instructions.set(instructions.get().saturating_add(amount));
183        });
184    }
185
186    pub fn grow_memory_pages(wasm: u64, stable: u64) {
187        WASM_MEMORY_PAGES.with(|pages| pages.set(pages.get().saturating_add(wasm)));
188        STABLE_MEMORY_PAGES.with(|pages| pages.set(pages.get().saturating_add(stable)));
189    }
190
191    pub fn reset(now_ns: u64, canister_version: u64) {
192        NOW_NS.with(|now| now.set(now_ns));
193        CANISTER_VERSION.with(|version| version.set(canister_version));
194        NEXT_HANDLE.with(|next| next.set(0));
195        INSTRUCTIONS.with(|instructions| instructions.set(0));
196        WASM_MEMORY_PAGES.with(|pages| pages.set(1));
197        STABLE_MEMORY_PAGES.with(|pages| pages.set(0));
198        TASKS.with(|tasks| tasks.borrow_mut().clear());
199    }
200
201    pub fn set_time(now_ns: u64) {
202        NOW_NS.with(|now| now.set(now_ns));
203    }
204
205    pub fn timer_count() -> usize {
206        TASKS.with(|tasks| tasks.borrow().len())
207    }
208
209    pub fn run_next_due() -> bool {
210        let now_ns = time_ns();
211        let next_handle = next_due_handle(now_ns);
212        let Some(next_handle) = next_handle else {
213            return false;
214        };
215        let Some(mut scheduled) = TASKS.with(|tasks| tasks.borrow_mut().remove(&next_handle))
216        else {
217            return false;
218        };
219
220        let mut context = Context::from_waker(Waker::noop());
221        if matches!(scheduled.task.as_mut().poll(&mut context), Poll::Pending) {
222            TASKS.with(|tasks| {
223                tasks.borrow_mut().insert(next_handle, scheduled);
224            });
225        }
226        true
227    }
228
229    pub fn discard_next_due() -> bool {
230        let Some(next_handle) = next_due_handle(time_ns()) else {
231            return false;
232        };
233        TASKS.with(|tasks| tasks.borrow_mut().remove(&next_handle).is_some())
234    }
235
236    fn next_due_handle(now_ns: u64) -> Option<u64> {
237        TASKS.with(|tasks| {
238            tasks
239                .borrow()
240                .iter()
241                .filter(|(_, scheduled)| scheduled.deadline_ns <= now_ns)
242                .min_by_key(|(handle, scheduled)| (scheduled.deadline_ns, **handle))
243                .map(|(handle, _)| *handle)
244        })
245    }
246}