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}