Skip to main content

kozan_scheduler/
task.rs

1//! Task types — Chrome's `base::OnceClosure` + `base::TaskTraits`.
2//!
3//! A [`Task`] is a unit of work posted to the scheduler.
4//! Like Chrome's `PostTask(FROM_HERE, base::BindOnce(&DoWork))`.
5//!
6//! # Performance
7//!
8//! - `Task` is a thin wrapper around `Box<dyn FnOnce()>`.
9//! - No allocations beyond the initial boxing.
10//! - `TaskPriority` is a u8 — fits in a register, branchless comparison.
11//!
12//! # Chrome mapping
13//!
14//! | Chrome                     | Kozan                |
15//! |----------------------------|----------------------|
16//! | `base::OnceClosure`        | `Task.callback`      |
17//! | `base::TaskTraits`         | `TaskPriority`       |
18//! | `base::Location`           | (not needed in Rust) |
19//! | `base::TimeDelta`          | `Task.delay`         |
20
21use core::fmt;
22use std::time::{Duration, Instant};
23
24/// Priority levels for tasks — determines scheduling order.
25///
26/// Ordered from highest to lowest. The scheduler always picks from the
27/// highest non-empty priority level (with anti-starvation for lower levels).
28///
29/// # Chrome mapping
30///
31/// | Kozan          | Chrome equivalent                    |
32/// |----------------|--------------------------------------|
33/// | `Input`        | Input task source (highest)          |
34/// | `UserBlocking` | `base::TaskPriority::USER_BLOCKING`  |
35/// | `Normal`       | `base::TaskPriority::USER_VISIBLE`   |
36/// | `Timer`        | Timer task source (throttleable)     |
37/// | `BestEffort`   | `base::TaskPriority::BEST_EFFORT`    |
38/// | `Idle`         | `requestIdleCallback` task source    |
39#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
40#[repr(u8)]
41pub enum TaskPriority {
42    /// Input events: mouse, keyboard, touch, pointer.
43    /// Always processed first — responsiveness is critical.
44    Input = 0,
45
46    /// User is actively waiting for the result.
47    /// Example: loading a resource the user just clicked on.
48    UserBlocking = 1,
49
50    /// Normal DOM work, network callbacks, general application logic.
51    /// The default priority for most tasks.
52    Normal = 2,
53
54    /// Timer callbacks (`setTimeout`/`setInterval` equivalent).
55    /// Can be throttled for background windows.
56    Timer = 3,
57
58    /// Background work the user won't notice if delayed.
59    /// Example: prefetching, metrics, analytics.
60    BestEffort = 4,
61
62    /// Idle tasks — only run when the frame budget has spare time.
63    /// Example: `requestIdleCallback` equivalent, GC-like cleanup.
64    Idle = 5,
65}
66
67impl TaskPriority {
68    /// Total number of priority levels.
69    /// Used to size the per-priority queue array.
70    pub const COUNT: usize = 6;
71
72    /// Convert to array index (0 = highest priority).
73    #[inline]
74    #[must_use]
75    pub const fn as_index(self) -> usize {
76        self as usize
77    }
78
79    /// Convert from array index. Returns `None` if out of range.
80    #[inline]
81    #[must_use]
82    pub const fn from_index(index: usize) -> Option<Self> {
83        match index {
84            0 => Some(Self::Input),
85            1 => Some(Self::UserBlocking),
86            2 => Some(Self::Normal),
87            3 => Some(Self::Timer),
88            4 => Some(Self::BestEffort),
89            5 => Some(Self::Idle),
90            _ => None,
91        }
92    }
93}
94
95impl fmt::Display for TaskPriority {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        match self {
98            Self::Input => write!(f, "Input"),
99            Self::UserBlocking => write!(f, "UserBlocking"),
100            Self::Normal => write!(f, "Normal"),
101            Self::Timer => write!(f, "Timer"),
102            Self::BestEffort => write!(f, "BestEffort"),
103            Self::Idle => write!(f, "Idle"),
104        }
105    }
106}
107
108impl Default for TaskPriority {
109    #[inline]
110    fn default() -> Self {
111        Self::Normal
112    }
113}
114
115/// A unit of work to be executed by the scheduler.
116///
117/// Like Chrome's `base::OnceClosure` wrapped with `base::TaskTraits`.
118/// Each task has a priority and an optional delay.
119///
120/// # Lifecycle
121///
122/// 1. Created via [`Task::new()`] or [`Task::delayed()`]
123/// 2. Posted to [`Scheduler`](crate::Scheduler) via `post_task()`
124/// 3. Scheduler puts it in the correct priority queue
125/// 4. Event loop picks highest-priority ready task
126/// 5. Task executes (callback consumed)
127pub struct Task {
128    /// The work to execute. Consumed on run.
129    callback: Box<dyn FnOnce()>,
130
131    /// Scheduling priority.
132    priority: TaskPriority,
133
134    /// Earliest time this task can run.
135    /// `None` = ready immediately.
136    /// Used for `setTimeout`/`setInterval` equivalent.
137    run_at: Option<Instant>,
138}
139
140impl Task {
141    /// Create a task with the given priority.
142    ///
143    /// ```ignore
144    /// Task::new(TaskPriority::Normal, || {
145    ///     println!("hello from task");
146    /// });
147    /// ```
148    #[inline]
149    pub fn new(priority: TaskPriority, callback: impl FnOnce() + 'static) -> Self {
150        Self {
151            callback: Box::new(callback),
152            priority,
153            run_at: None,
154        }
155    }
156
157    /// Create a delayed task.
158    ///
159    /// Like Chrome's `PostDelayedTask()`. The task won't execute until
160    /// `delay` has elapsed. Equivalent to `setTimeout(callback, delay)`.
161    ///
162    /// ```ignore
163    /// Task::delayed(TaskPriority::Timer, Duration::from_millis(100), || {
164    ///     println!("fires after 100ms");
165    /// });
166    /// ```
167    #[inline]
168    pub fn delayed(
169        priority: TaskPriority,
170        delay: Duration,
171        callback: impl FnOnce() + 'static,
172    ) -> Self {
173        Self {
174            callback: Box::new(callback),
175            priority,
176            run_at: Some(Instant::now() + delay),
177        }
178    }
179
180    /// The task's priority level.
181    #[inline]
182    #[must_use]
183    pub fn priority(&self) -> TaskPriority {
184        self.priority
185    }
186
187    /// Whether this task is ready to execute (delay has elapsed).
188    #[inline]
189    #[must_use]
190    pub fn is_ready(&self) -> bool {
191        match self.run_at {
192            None => true,
193            Some(at) => Instant::now() >= at,
194        }
195    }
196
197    /// Time remaining until this task is ready.
198    /// Returns `Duration::ZERO` if already ready.
199    #[inline]
200    #[must_use]
201    pub fn time_until_ready(&self) -> Duration {
202        match self.run_at {
203            None => Duration::ZERO,
204            Some(at) => at.saturating_duration_since(Instant::now()),
205        }
206    }
207
208    /// The scheduled run time, if delayed.
209    #[inline]
210    #[must_use]
211    pub fn run_at(&self) -> Option<Instant> {
212        self.run_at
213    }
214
215    /// Execute this task, consuming the callback.
216    #[inline]
217    pub fn run(self) {
218        (self.callback)();
219    }
220}
221
222impl fmt::Debug for Task {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        f.debug_struct("Task")
225            .field("priority", &self.priority)
226            .field("run_at", &self.run_at)
227            .finish_non_exhaustive()
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use std::cell::Cell;
235    use std::rc::Rc;
236
237    #[test]
238    fn task_executes_callback() {
239        let called = Rc::new(Cell::new(false));
240        let called2 = called.clone();
241        let task = Task::new(TaskPriority::Normal, move || called2.set(true));
242        assert!(!called.get());
243        task.run();
244        assert!(called.get());
245    }
246
247    #[test]
248    fn task_ready_when_no_delay() {
249        let task = Task::new(TaskPriority::Input, || {});
250        assert!(task.is_ready());
251        assert_eq!(task.time_until_ready(), Duration::ZERO);
252        assert!(task.run_at().is_none());
253    }
254
255    #[test]
256    fn task_not_ready_with_future_delay() {
257        let task = Task::delayed(TaskPriority::Timer, Duration::from_secs(60), || {});
258        assert!(!task.is_ready());
259        assert!(task.time_until_ready() > Duration::ZERO);
260        assert!(task.run_at().is_some());
261    }
262
263    #[test]
264    fn task_ready_with_zero_delay() {
265        let task = Task::delayed(TaskPriority::Timer, Duration::ZERO, || {});
266        assert!(task.is_ready());
267    }
268
269    #[test]
270    fn priority_ordering() {
271        assert!(TaskPriority::Input < TaskPriority::UserBlocking);
272        assert!(TaskPriority::UserBlocking < TaskPriority::Normal);
273        assert!(TaskPriority::Normal < TaskPriority::Timer);
274        assert!(TaskPriority::Timer < TaskPriority::BestEffort);
275        assert!(TaskPriority::BestEffort < TaskPriority::Idle);
276    }
277
278    #[test]
279    fn priority_index_roundtrip() {
280        for i in 0..TaskPriority::COUNT {
281            let p = TaskPriority::from_index(i).unwrap();
282            assert_eq!(p.as_index(), i);
283        }
284        assert!(TaskPriority::from_index(TaskPriority::COUNT).is_none());
285    }
286
287    #[test]
288    fn default_priority_is_normal() {
289        assert_eq!(TaskPriority::default(), TaskPriority::Normal);
290    }
291
292    #[test]
293    fn task_debug_format() {
294        let task = Task::new(TaskPriority::Input, || {});
295        let debug = format!("{:?}", task);
296        assert!(debug.contains("Input"));
297    }
298}