ic_cdk_timers/
state.rs

1use std::{
2    cell::{Cell, RefCell},
3    cmp::Ordering,
4    collections::BinaryHeap,
5    pin::Pin,
6    time::Duration,
7};
8
9use slotmap::{SlotMap, new_key_type};
10
11// To ensure that tasks are removable seamlessly, there are two separate concepts here:
12// tasks, for the actual function being called, and timers, the scheduled execution of tasks.
13// As this is an implementation detail, lib.rs exposes TaskId under the name TimerId.
14
15thread_local! {
16    pub(crate) static TIMER_COUNTER: Cell<u128> = const { Cell::new(0) };
17    pub(crate) static TASKS: RefCell<SlotMap<TaskId, Task>> = RefCell::default();
18    pub(crate) static TIMERS: RefCell<BinaryHeap<Timer>> = RefCell::default();
19    static MOST_RECENT: Cell<Option<u64>> = const { Cell::new(None) };
20    pub(crate) static ALL_CALLS: Cell<usize> = const { Cell::new(0) };
21}
22
23pub(crate) enum Task {
24    Once(Pin<Box<dyn Future<Output = ()>>>),
25    Repeated {
26        func: Box<dyn FnMut() -> Pin<Box<dyn Future<Output = ()>>>>,
27        interval: Duration,
28        concurrent_calls: usize,
29    },
30    RepeatedSerial {
31        func: Box<dyn SerialClosure>,
32        interval: Duration,
33    },
34    RepeatedSerialBusy {
35        interval: Duration,
36    },
37    Invalid,
38}
39
40pub(crate) trait SerialClosure {
41    fn call<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = ()> + 'a>>;
42}
43
44impl<F: AsyncFnMut()> SerialClosure for F {
45    fn call<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = ()> + 'a>> {
46        Box::pin(self())
47    }
48}
49
50new_key_type! {
51    #[expect(missing_docs)] // documented in lib.rs
52    pub struct TaskId;
53}
54
55#[derive(Debug)]
56pub(crate) struct Timer {
57    pub(crate) task: TaskId,
58    pub(crate) time: u64,
59    pub(crate) counter: u128,
60}
61
62// Timers are sorted first by time, then by insertion order to ensure deterministic ordering.
63// The ordering is reversed (earlier timer > later) for use in BinaryHeap which is a max-heap.
64
65impl Ord for Timer {
66    fn cmp(&self, other: &Self) -> Ordering {
67        self.time
68            .cmp(&other.time)
69            .then_with(|| self.counter.cmp(&other.counter))
70            .reverse()
71    }
72}
73
74impl PartialOrd for Timer {
75    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
76        Some(self.cmp(other))
77    }
78}
79
80impl PartialEq for Timer {
81    fn eq(&self, other: &Self) -> bool {
82        self.time == other.time
83    }
84}
85
86impl Eq for Timer {}
87
88pub(crate) fn next_counter() -> u128 {
89    TIMER_COUNTER.with(|c| {
90        let v = c.get();
91        c.set(v + 1);
92        v
93    })
94}
95
96/// Calls `ic0.global_timer_set` with the soonest timer in [`TIMERS`]. This is needed after inserting a timer, and after executing one.
97pub(crate) fn update_ic0_timer() {
98    TIMERS.with_borrow(|timers| {
99        let soonest_timer = timers.peek().map(|timer| timer.time);
100        let should_change = match (soonest_timer, MOST_RECENT.get()) {
101            (Some(timer), Some(recent)) => timer < recent,
102            (Some(_), None) => true,
103            _ => false,
104        };
105        if should_change {
106            ic0::global_timer_set(soonest_timer.unwrap());
107            MOST_RECENT.set(soonest_timer);
108        }
109    });
110}
111
112/// Like [`update_ic0_timer`], but forces updating unconditionally. Should only be called from canister_global_timer.
113pub(crate) fn update_ic0_timer_clean() {
114    MOST_RECENT.set(None);
115    update_ic0_timer();
116}
117
118impl Task {
119    pub(crate) fn increment_concurrent(&mut self) {
120        if let Task::Repeated {
121            concurrent_calls, ..
122        } = self
123        {
124            *concurrent_calls += 1;
125        }
126    }
127    pub(crate) fn decrement_concurrent(&mut self) {
128        if let Task::Repeated {
129            concurrent_calls, ..
130        } = self
131        {
132            if *concurrent_calls > 0 {
133                *concurrent_calls -= 1;
134            }
135        }
136    }
137}
138
139pub(crate) fn increment_all_calls() {
140    ALL_CALLS.set(ALL_CALLS.get() + 1);
141}
142
143pub(crate) fn decrement_all_calls() {
144    let current = ALL_CALLS.get();
145    if current > 0 {
146        ALL_CALLS.set(current - 1);
147    }
148}