Skip to main content

pipa/runtime/
event_loop.rs

1use crate::runtime::context::JSContext;
2use crate::runtime::extension::MacroTaskExtension;
3use crate::value::JSValue;
4use std::cmp::Ordering;
5use std::collections::BinaryHeap;
6use std::collections::HashMap;
7use std::collections::VecDeque;
8use std::time::{Duration, Instant};
9
10pub type TimerId = u32;
11
12pub type AnimationCallbackId = u32;
13
14pub struct Macrotask {
15    pub callback: JSValue,
16
17    pub args: Vec<JSValue>,
18
19    pub timer_id: Option<TimerId>,
20
21    pub is_interval: bool,
22
23    pub interval: Option<Duration>,
24}
25
26#[derive(Clone)]
27pub struct Timer {
28    pub id: TimerId,
29
30    pub callback: JSValue,
31
32    pub args: Vec<JSValue>,
33
34    pub deadline: Instant,
35
36    pub is_interval: bool,
37
38    pub interval: Option<Duration>,
39}
40
41impl Ord for Timer {
42    fn cmp(&self, other: &Self) -> Ordering {
43        other.deadline.cmp(&self.deadline)
44    }
45}
46
47impl PartialOrd for Timer {
48    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
49        Some(self.cmp(other))
50    }
51}
52
53impl Eq for Timer {}
54
55impl PartialEq for Timer {
56    fn eq(&self, other: &Self) -> bool {
57        self.id == other.id
58    }
59}
60
61pub struct AnimationCallback {
62    pub id: AnimationCallbackId,
63
64    pub callback: JSValue,
65}
66
67#[derive(Debug, Clone)]
68pub struct EventLoopResult {
69    pub completed: bool,
70
71    pub macrotasks_executed: usize,
72
73    pub timers_fired: usize,
74
75    pub animation_callbacks_executed: usize,
76
77    pub macrotasks_remaining: usize,
78
79    pub timers_remaining: usize,
80
81    pub animation_callbacks_remaining: usize,
82}
83
84pub struct EventLoop {
85    macrotask_queue: VecDeque<Macrotask>,
86
87    timer_heap: BinaryHeap<Timer>,
88
89    active_timers: HashMap<TimerId, ()>,
90
91    interval_timers: HashMap<TimerId, Timer>,
92
93    animation_queue: VecDeque<AnimationCallback>,
94
95    next_timer_id: TimerId,
96
97    next_animation_id: AnimationCallbackId,
98
99    max_iterations: u64,
100
101    pub extensions: Vec<Box<dyn MacroTaskExtension>>,
102}
103
104impl Default for EventLoop {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110impl EventLoop {
111    pub fn new() -> Self {
112        EventLoop {
113            macrotask_queue: VecDeque::new(),
114            timer_heap: BinaryHeap::new(),
115            active_timers: HashMap::new(),
116            interval_timers: HashMap::new(),
117            animation_queue: VecDeque::new(),
118            next_timer_id: 1,
119            next_animation_id: 1,
120            max_iterations: 1_000_000,
121            extensions: Vec::new(),
122        }
123    }
124
125    pub fn set_max_iterations(&mut self, max: u64) {
126        self.max_iterations = max;
127    }
128
129    pub fn schedule_timer(
130        &mut self,
131        callback: JSValue,
132        args: Vec<JSValue>,
133        delay_ms: u64,
134        is_interval: bool,
135    ) -> TimerId {
136        let id = self.next_timer_id;
137        self.next_timer_id += 1;
138
139        let deadline = Instant::now() + Duration::from_millis(delay_ms);
140        let interval = if is_interval {
141            Some(Duration::from_millis(delay_ms))
142        } else {
143            None
144        };
145
146        let timer = Timer {
147            id,
148            callback,
149            args,
150            deadline,
151            is_interval,
152            interval,
153        };
154
155        self.timer_heap.push(timer.clone());
156        self.active_timers.insert(id, ());
157
158        if is_interval {
159            self.interval_timers.insert(id, timer);
160        }
161
162        id
163    }
164
165    pub fn clear_timer(&mut self, timer_id: TimerId) {
166        self.active_timers.remove(&timer_id);
167        self.interval_timers.remove(&timer_id);
168    }
169
170    pub fn is_timer_active(&self, timer_id: TimerId) -> bool {
171        self.active_timers.contains_key(&timer_id)
172    }
173
174    pub fn schedule_macrotask(&mut self, callback: JSValue, args: Vec<JSValue>) {
175        self.macrotask_queue.push_back(Macrotask {
176            callback,
177            args,
178            timer_id: None,
179            is_interval: false,
180            interval: None,
181        });
182    }
183
184    pub fn schedule_animation_callback(&mut self, callback: JSValue) -> AnimationCallbackId {
185        let id = self.next_animation_id;
186        self.next_animation_id += 1;
187
188        self.animation_queue
189            .push_back(AnimationCallback { id, callback });
190
191        id
192    }
193
194    pub fn cancel_animation_callback(&mut self, id: AnimationCallbackId) {
195        self.animation_queue.retain(|cb| cb.id != id);
196    }
197
198    pub fn has_pending_tasks(&self) -> bool {
199        !self.macrotask_queue.is_empty()
200            || !self.timer_heap.is_empty()
201            || !self.animation_queue.is_empty()
202    }
203
204    pub fn macrotask_count(&self) -> usize {
205        self.macrotask_queue.len()
206    }
207
208    pub fn timer_count(&self) -> usize {
209        self.timer_heap.len()
210    }
211
212    pub fn animation_callback_count(&self) -> usize {
213        self.animation_queue.len()
214    }
215
216    fn fire_ready_timers(&mut self) -> usize {
217        let mut fired = 0;
218        let now = Instant::now();
219
220        while let Some(timer) = self.timer_heap.pop() {
221            if timer.deadline > now {
222                self.timer_heap.push(timer);
223                break;
224            }
225
226            if !self.active_timers.contains_key(&timer.id) {
227                continue;
228            }
229
230            fired += 1;
231
232            self.macrotask_queue.push_back(Macrotask {
233                callback: timer.callback.clone(),
234                args: timer.args.clone(),
235                timer_id: Some(timer.id),
236                is_interval: timer.is_interval,
237                interval: timer.interval,
238            });
239
240            if !timer.is_interval {
241                self.active_timers.remove(&timer.id);
242            }
243        }
244
245        fired
246    }
247
248    fn execute_callback(
249        &self,
250        ctx: &mut JSContext,
251        callback: JSValue,
252        args: &[JSValue],
253    ) -> Result<JSValue, String> {
254        if !callback.is_function() {
255            return Ok(JSValue::undefined());
256        }
257
258        let vm_ptr = ctx
259            .get_register_vm_ptr()
260            .ok_or("No VM associated with context")?;
261        let vm = unsafe { &mut *(vm_ptr as *mut crate::runtime::vm::VM) };
262        vm.call_function(ctx, callback, args)
263    }
264
265    fn run_microtasks(&self, ctx: &mut JSContext) {
266        let vm_ptr = match ctx.get_register_vm_ptr() {
267            Some(ptr) => ptr,
268            None => return,
269        };
270        let vm = unsafe { &mut *(vm_ptr as *mut crate::runtime::vm::VM) };
271        crate::builtins::promise::run_microtasks_with_vm(ctx, vm);
272    }
273
274    fn tick_extensions(&mut self, ctx: &mut JSContext) {
275        for ext in &mut self.extensions {
276            if let Err(_) = ext.tick(ctx) {}
277        }
278    }
279
280    pub fn run_until_complete(
281        &mut self,
282        ctx: &mut JSContext,
283        timeout_ms: Option<u64>,
284    ) -> Result<EventLoopResult, String> {
285        let start = Instant::now();
286        let timeout = timeout_ms.map(Duration::from_millis);
287        let mut iterations = 0u64;
288
289        let mut macrotasks_executed = 0usize;
290        let mut timers_fired = 0usize;
291        let mut animation_callbacks_executed = 0usize;
292
293        loop {
294            iterations += 1;
295            if iterations > self.max_iterations {
296                return Err(format!(
297                    "Event loop iteration limit exceeded ({})",
298                    self.max_iterations
299                ));
300            }
301
302            if let Some(timeout_dur) = &timeout {
303                if start.elapsed() > *timeout_dur {
304                    return Ok(EventLoopResult {
305                        completed: false,
306                        macrotasks_executed,
307                        timers_fired,
308                        animation_callbacks_executed,
309                        macrotasks_remaining: self.macrotask_queue.len(),
310                        timers_remaining: self.timer_heap.len(),
311                        animation_callbacks_remaining: self.animation_queue.len(),
312                    });
313                }
314            }
315
316            self.run_microtasks(ctx);
317
318            self.tick_extensions(ctx);
319
320            timers_fired += self.fire_ready_timers();
321
322            let animation_callbacks: Vec<AnimationCallback> =
323                self.animation_queue.drain(..).collect();
324            for anim_cb in animation_callbacks {
325                let timestamp = Instant::now().elapsed().as_nanos() as f64;
326                let timestamp_val = JSValue::new_float(timestamp / 1_000_000.0);
327
328                if anim_cb.callback.is_function() {
329                    let _ = self.execute_callback(ctx, anim_cb.callback, &[timestamp_val]);
330                    animation_callbacks_executed += 1;
331                }
332            }
333
334            if let Some(macrotask) = self.macrotask_queue.pop_front() {
335                let timer_id = macrotask.timer_id;
336                let is_interval = macrotask.is_interval;
337                let interval = macrotask.interval;
338
339                if macrotask.callback.is_function() {
340                    match self.execute_callback(ctx, macrotask.callback, &macrotask.args) {
341                        Ok(_result) => {}
342                        Err(e) => {
343                            eprintln!("[EventLoop] Macrotask error: {}", e);
344                        }
345                    }
346                    macrotasks_executed += 1;
347                }
348
349                if is_interval {
350                    if let (Some(id), Some(interval_dur)) = (timer_id, interval) {
351                        if self.active_timers.contains_key(&id) {
352                            if let Some(stored_timer) = self.interval_timers.get(&id).cloned() {
353                                let new_timer = Timer {
354                                    id,
355                                    callback: stored_timer.callback,
356                                    args: stored_timer.args,
357                                    deadline: Instant::now() + interval_dur,
358                                    is_interval: true,
359                                    interval: Some(interval_dur),
360                                };
361                                self.timer_heap.push(new_timer);
362                            }
363                        }
364                    }
365                }
366
367                self.run_microtasks(ctx);
368                continue;
369            }
370
371            let has_microtasks = !ctx.microtask_is_empty();
372            let has_macrotasks = !self.macrotask_queue.is_empty();
373            let has_timers = !self.timer_heap.is_empty();
374            let has_animation = !self.animation_queue.is_empty();
375            let has_io = self.extensions.iter().any(|e| e.has_pending());
376
377            if !has_microtasks && !has_macrotasks && !has_timers && !has_animation && !has_io {
378                return Ok(EventLoopResult {
379                    completed: true,
380                    macrotasks_executed,
381                    timers_fired,
382                    animation_callbacks_executed,
383                    macrotasks_remaining: 0,
384                    timers_remaining: 0,
385                    animation_callbacks_remaining: 0,
386                });
387            }
388
389            let exit_early = if has_microtasks || has_macrotasks || has_animation {
390                false
391            } else if has_timers {
392                if let Some(next_timer) = self.timer_heap.peek() {
393                    let wait = next_timer
394                        .deadline
395                        .saturating_duration_since(Instant::now());
396                    wait > Duration::from_millis(100)
397                } else {
398                    true
399                }
400            } else if has_io {
401                false
402            } else {
403                true
404            };
405
406            if exit_early {
407                if has_io {
408                    self.tick_extensions(ctx);
409                    if !self.macrotask_queue.is_empty() {
410                        continue;
411                    }
412                }
413                return Ok(EventLoopResult {
414                    completed: !has_io,
415                    macrotasks_executed,
416                    timers_fired,
417                    animation_callbacks_executed,
418                    macrotasks_remaining: self.macrotask_queue.len(),
419                    timers_remaining: self.timer_heap.len(),
420                    animation_callbacks_remaining: self.animation_queue.len(),
421                });
422            }
423        }
424    }
425
426    pub fn tick(&mut self, ctx: &mut JSContext) -> Result<EventLoopResult, String> {
427        let mut macrotasks_executed = 0usize;
428        let mut timers_fired = 0usize;
429        let mut animation_callbacks_executed = 0usize;
430
431        self.run_microtasks(ctx);
432
433        self.tick_extensions(ctx);
434
435        timers_fired += self.fire_ready_timers();
436
437        let animation_callbacks: Vec<AnimationCallback> = self.animation_queue.drain(..).collect();
438        for anim_cb in animation_callbacks {
439            let timestamp = Instant::now().elapsed().as_nanos() as f64;
440            let timestamp_val = JSValue::new_float(timestamp / 1_000_000.0);
441
442            if anim_cb.callback.is_function() {
443                let _ = self.execute_callback(ctx, anim_cb.callback, &[timestamp_val]);
444                animation_callbacks_executed += 1;
445            }
446        }
447
448        if let Some(macrotask) = self.macrotask_queue.pop_front() {
449            if macrotask.callback.is_function() {
450                let _ = self.execute_callback(ctx, macrotask.callback, &macrotask.args);
451                macrotasks_executed += 1;
452            }
453
454            self.run_microtasks(ctx);
455        }
456
457        Ok(EventLoopResult {
458            completed: !self.has_pending_tasks()
459                && ctx.microtask_is_empty()
460                && !self.extensions.iter().any(|e| e.has_pending()),
461            macrotasks_executed,
462            timers_fired,
463            animation_callbacks_executed,
464            macrotasks_remaining: self.macrotask_queue.len(),
465            timers_remaining: self.timer_heap.len(),
466            animation_callbacks_remaining: self.animation_queue.len(),
467        })
468    }
469
470    pub fn advance_time(
471        &mut self,
472        ctx: &mut JSContext,
473        _duration_ms: u64,
474    ) -> Result<EventLoopResult, String> {
475        let mut timers_fired = 0usize;
476
477        while let Some(timer) = self.timer_heap.pop() {
478            if !self.active_timers.contains_key(&timer.id) {
479                continue;
480            }
481
482            timers_fired += 1;
483
484            self.macrotask_queue.push_back(Macrotask {
485                callback: timer.callback.clone(),
486                args: timer.args.clone(),
487                timer_id: Some(timer.id),
488                is_interval: timer.is_interval,
489                interval: timer.interval,
490            });
491
492            if timer.is_interval {
493                if let Some(interval) = timer.interval {
494                    let mut new_timer = timer;
495                    new_timer.deadline = Instant::now() + interval;
496                    self.timer_heap.push(new_timer);
497                }
498            } else {
499                self.active_timers.remove(&timer.id);
500            }
501        }
502
503        let result = self.run_until_complete(ctx, None)?;
504
505        Ok(EventLoopResult {
506            timers_fired,
507            ..result
508        })
509    }
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515
516    #[test]
517    fn test_event_loop_creation() {
518        let el = EventLoop::new();
519        assert!(!el.has_pending_tasks());
520        assert_eq!(el.macrotask_count(), 0);
521        assert_eq!(el.timer_count(), 0);
522    }
523
524    #[test]
525    fn test_schedule_macrotask() {
526        let mut el = EventLoop::new();
527        el.schedule_macrotask(JSValue::undefined(), vec![]);
528        assert!(el.has_pending_tasks());
529        assert_eq!(el.macrotask_count(), 1);
530    }
531
532    #[test]
533    fn test_schedule_timer() {
534        let mut el = EventLoop::new();
535        let id = el.schedule_timer(JSValue::undefined(), vec![], 100, false);
536        assert_eq!(id, 1);
537        assert!(el.has_pending_tasks());
538        assert_eq!(el.timer_count(), 1);
539        assert!(el.is_timer_active(id));
540    }
541
542    #[test]
543    fn test_clear_timer() {
544        let mut el = EventLoop::new();
545        let id = el.schedule_timer(JSValue::undefined(), vec![], 100, false);
546        assert!(el.is_timer_active(id));
547        el.clear_timer(id);
548        assert!(!el.is_timer_active(id));
549    }
550
551    #[test]
552    fn test_schedule_animation_callback() {
553        let mut el = EventLoop::new();
554        let id = el.schedule_animation_callback(JSValue::undefined());
555        assert_eq!(id, 1);
556        assert_eq!(el.animation_callback_count(), 1);
557    }
558
559    #[test]
560    fn test_cancel_animation_callback() {
561        let mut el = EventLoop::new();
562        let id = el.schedule_animation_callback(JSValue::undefined());
563        el.cancel_animation_callback(id);
564        assert_eq!(el.animation_callback_count(), 0);
565    }
566
567    #[test]
568    fn test_timer_ordering() {
569        let mut el = EventLoop::new();
570
571        let _id1 = el.schedule_timer(JSValue::undefined(), vec![], 300, false);
572        let id2 = el.schedule_timer(JSValue::undefined(), vec![], 100, false);
573        let _id3 = el.schedule_timer(JSValue::undefined(), vec![], 200, false);
574
575        let top = el.timer_heap.peek().unwrap();
576        assert_eq!(top.id, id2);
577    }
578}