1use 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#[cfg(not(test))]
15#[must_use = "bind or clear the platform timer handle"]
16#[derive(Debug, Eq, PartialEq)]
17pub struct TimerHandle(CdkTimerId);
18
19#[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#[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#[cfg(not(test))]
44#[allow(clippy::needless_pass_by_value)] pub fn clear_timer(handle: TimerHandle) {
46 cdk_clear_timer(handle.0);
47}
48
49#[cfg(not(test))]
51pub fn time_ns() -> u64 {
52 ic0::time()
53}
54
55#[cfg(not(test))]
57pub fn canister_version() -> u64 {
58 ic0::canister_version()
59}
60
61#[cfg(not(test))]
63pub fn instruction_counter() -> u64 {
64 ic0::performance_counter(1)
65}
66
67#[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#[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)] 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}