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(e) = ext.tick(ctx) {
277                eprintln!("[EventLoop] extension error: {e}");
278            }
279        }
280    }
281
282    pub fn run_until_complete(
283        &mut self,
284        ctx: &mut JSContext,
285        timeout_ms: Option<u64>,
286    ) -> Result<EventLoopResult, String> {
287        let start = Instant::now();
288        let timeout = timeout_ms.map(Duration::from_millis);
289        let mut iterations = 0u64;
290
291        let mut macrotasks_executed = 0usize;
292        let mut timers_fired = 0usize;
293        let mut animation_callbacks_executed = 0usize;
294
295        loop {
296            iterations += 1;
297            if iterations > self.max_iterations {
298                return Err(format!(
299                    "Event loop iteration limit exceeded ({})",
300                    self.max_iterations
301                ));
302            }
303
304            if let Some(timeout_dur) = &timeout {
305                if start.elapsed() > *timeout_dur {
306                    return Ok(EventLoopResult {
307                        completed: false,
308                        macrotasks_executed,
309                        timers_fired,
310                        animation_callbacks_executed,
311                        macrotasks_remaining: self.macrotask_queue.len(),
312                        timers_remaining: self.timer_heap.len(),
313                        animation_callbacks_remaining: self.animation_queue.len(),
314                    });
315                }
316            }
317
318            self.run_microtasks(ctx);
319
320            self.tick_extensions(ctx);
321
322            timers_fired += self.fire_ready_timers();
323
324            let animation_callbacks: Vec<AnimationCallback> =
325                self.animation_queue.drain(..).collect();
326            for anim_cb in animation_callbacks {
327                let timestamp = Instant::now().elapsed().as_nanos() as f64;
328                let timestamp_val = JSValue::new_float(timestamp / 1_000_000.0);
329
330                if anim_cb.callback.is_function() {
331                    let _ = self.execute_callback(ctx, anim_cb.callback, &[timestamp_val]);
332                    animation_callbacks_executed += 1;
333                }
334            }
335
336            if let Some(macrotask) = self.macrotask_queue.pop_front() {
337                let timer_id = macrotask.timer_id;
338                let is_interval = macrotask.is_interval;
339                let interval = macrotask.interval;
340
341                if macrotask.callback.is_function() {
342                    match self.execute_callback(ctx, macrotask.callback, &macrotask.args) {
343                        Ok(_result) => {}
344                        Err(e) => {
345                            eprintln!("[EventLoop] Macrotask error: {}", e);
346                        }
347                    }
348                    macrotasks_executed += 1;
349                }
350
351                if is_interval {
352                    if let (Some(id), Some(interval_dur)) = (timer_id, interval) {
353                        if self.active_timers.contains_key(&id) {
354                            if let Some(stored_timer) = self.interval_timers.get(&id).cloned() {
355                                let new_timer = Timer {
356                                    id,
357                                    callback: stored_timer.callback,
358                                    args: stored_timer.args,
359                                    deadline: Instant::now() + interval_dur,
360                                    is_interval: true,
361                                    interval: Some(interval_dur),
362                                };
363                                self.timer_heap.push(new_timer);
364                            }
365                        }
366                    }
367                }
368
369                self.run_microtasks(ctx);
370                continue;
371            }
372
373            let has_microtasks = !ctx.microtask_is_empty();
374            let has_macrotasks = !self.macrotask_queue.is_empty();
375            let has_timers = !self.timer_heap.is_empty();
376            let has_animation = !self.animation_queue.is_empty();
377            let has_io = self.extensions.iter().any(|e| e.has_pending());
378
379            if !has_microtasks && !has_macrotasks && !has_timers && !has_animation && !has_io {
380                return Ok(EventLoopResult {
381                    completed: true,
382                    macrotasks_executed,
383                    timers_fired,
384                    animation_callbacks_executed,
385                    macrotasks_remaining: 0,
386                    timers_remaining: 0,
387                    animation_callbacks_remaining: 0,
388                });
389            }
390
391            let exit_early = if has_microtasks || has_macrotasks || has_animation {
392                false
393            } else if has_timers {
394                if let Some(next_timer) = self.timer_heap.peek() {
395                    let wait = next_timer
396                        .deadline
397                        .saturating_duration_since(Instant::now());
398                    wait > Duration::from_millis(100)
399                } else {
400                    true
401                }
402            } else if has_io {
403                false
404            } else {
405                true
406            };
407
408            if exit_early {
409                if has_io {
410                    self.tick_extensions(ctx);
411                    if !self.macrotask_queue.is_empty() {
412                        continue;
413                    }
414                }
415                return Ok(EventLoopResult {
416                    completed: !has_io,
417                    macrotasks_executed,
418                    timers_fired,
419                    animation_callbacks_executed,
420                    macrotasks_remaining: self.macrotask_queue.len(),
421                    timers_remaining: self.timer_heap.len(),
422                    animation_callbacks_remaining: self.animation_queue.len(),
423                });
424            }
425        }
426    }
427
428    pub fn tick(&mut self, ctx: &mut JSContext) -> Result<EventLoopResult, String> {
429        let mut macrotasks_executed = 0usize;
430        let mut timers_fired = 0usize;
431        let mut animation_callbacks_executed = 0usize;
432
433        self.run_microtasks(ctx);
434
435        self.tick_extensions(ctx);
436
437        timers_fired += self.fire_ready_timers();
438
439        let animation_callbacks: Vec<AnimationCallback> = self.animation_queue.drain(..).collect();
440        for anim_cb in animation_callbacks {
441            let timestamp = Instant::now().elapsed().as_nanos() as f64;
442            let timestamp_val = JSValue::new_float(timestamp / 1_000_000.0);
443
444            if anim_cb.callback.is_function() {
445                let _ = self.execute_callback(ctx, anim_cb.callback, &[timestamp_val]);
446                animation_callbacks_executed += 1;
447            }
448        }
449
450        if let Some(macrotask) = self.macrotask_queue.pop_front() {
451            if macrotask.callback.is_function() {
452                let _ = self.execute_callback(ctx, macrotask.callback, &macrotask.args);
453                macrotasks_executed += 1;
454            }
455
456            self.run_microtasks(ctx);
457        }
458
459        Ok(EventLoopResult {
460            completed: !self.has_pending_tasks()
461                && ctx.microtask_is_empty()
462                && !self.extensions.iter().any(|e| e.has_pending()),
463            macrotasks_executed,
464            timers_fired,
465            animation_callbacks_executed,
466            macrotasks_remaining: self.macrotask_queue.len(),
467            timers_remaining: self.timer_heap.len(),
468            animation_callbacks_remaining: self.animation_queue.len(),
469        })
470    }
471
472    pub fn advance_time(
473        &mut self,
474        ctx: &mut JSContext,
475        _duration_ms: u64,
476    ) -> Result<EventLoopResult, String> {
477        let mut timers_fired = 0usize;
478
479        while let Some(timer) = self.timer_heap.pop() {
480            if !self.active_timers.contains_key(&timer.id) {
481                continue;
482            }
483
484            timers_fired += 1;
485
486            self.macrotask_queue.push_back(Macrotask {
487                callback: timer.callback.clone(),
488                args: timer.args.clone(),
489                timer_id: Some(timer.id),
490                is_interval: timer.is_interval,
491                interval: timer.interval,
492            });
493
494            if timer.is_interval {
495                if let Some(interval) = timer.interval {
496                    let mut new_timer = timer;
497                    new_timer.deadline = Instant::now() + interval;
498                    self.timer_heap.push(new_timer);
499                }
500            } else {
501                self.active_timers.remove(&timer.id);
502            }
503        }
504
505        let result = self.run_until_complete(ctx, None)?;
506
507        Ok(EventLoopResult {
508            timers_fired,
509            ..result
510        })
511    }
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517
518    #[test]
519    fn test_event_loop_creation() {
520        let el = EventLoop::new();
521        assert!(!el.has_pending_tasks());
522        assert_eq!(el.macrotask_count(), 0);
523        assert_eq!(el.timer_count(), 0);
524    }
525
526    #[test]
527    fn test_schedule_macrotask() {
528        let mut el = EventLoop::new();
529        el.schedule_macrotask(JSValue::undefined(), vec![]);
530        assert!(el.has_pending_tasks());
531        assert_eq!(el.macrotask_count(), 1);
532    }
533
534    #[test]
535    fn test_schedule_timer() {
536        let mut el = EventLoop::new();
537        let id = el.schedule_timer(JSValue::undefined(), vec![], 100, false);
538        assert_eq!(id, 1);
539        assert!(el.has_pending_tasks());
540        assert_eq!(el.timer_count(), 1);
541        assert!(el.is_timer_active(id));
542    }
543
544    #[test]
545    fn test_clear_timer() {
546        let mut el = EventLoop::new();
547        let id = el.schedule_timer(JSValue::undefined(), vec![], 100, false);
548        assert!(el.is_timer_active(id));
549        el.clear_timer(id);
550        assert!(!el.is_timer_active(id));
551    }
552
553    #[test]
554    fn test_schedule_animation_callback() {
555        let mut el = EventLoop::new();
556        let id = el.schedule_animation_callback(JSValue::undefined());
557        assert_eq!(id, 1);
558        assert_eq!(el.animation_callback_count(), 1);
559    }
560
561    #[test]
562    fn test_cancel_animation_callback() {
563        let mut el = EventLoop::new();
564        let id = el.schedule_animation_callback(JSValue::undefined());
565        el.cancel_animation_callback(id);
566        assert_eq!(el.animation_callback_count(), 0);
567    }
568
569    #[test]
570    fn test_timer_ordering() {
571        let mut el = EventLoop::new();
572
573        let _id1 = el.schedule_timer(JSValue::undefined(), vec![], 300, false);
574        let id2 = el.schedule_timer(JSValue::undefined(), vec![], 100, false);
575        let _id3 = el.schedule_timer(JSValue::undefined(), vec![], 200, false);
576
577        let top = el.timer_heap.peek().unwrap();
578        assert_eq!(top.id, id2);
579    }
580}