//! Timer handling for `setInterval` AVM timers. //! //! We tick the timers during our normal frame loop for deterministic operation. //! The timers are stored in a priority queue, where we check if the nearest timer //! is ready to tick each frame. //! //! TODO: Could we use this for AVM2 timers as well? use crate::avm1::object::search_prototype; use crate::avm1::{Activation, ActivationIdentifier, Object, TObject, Value}; use crate::context::UpdateContext; use gc_arena::Collect; use std::collections::{binary_heap::PeekMut, BinaryHeap}; /// Manages the collection of timers. pub struct Timers<'gc> { /// The collection of active timers. timers: BinaryHeap>, /// An increasing ID used for created timers. timer_counter: i32, /// The current global time. cur_time: u64, } impl<'gc> Timers<'gc> { /// Ticks all timers and runs necessary callbacks. pub fn update_timers(context: &mut UpdateContext<'_, 'gc, '_>, dt: f64) -> Option { context.timers.cur_time = context .timers .cur_time .wrapping_add((dt * Self::TIMER_SCALE) as u64); let num_timers = context.timers.num_timers(); if num_timers == 0 { return None; } let mut activation = Activation::from_nothing( context.reborrow(), ActivationIdentifier::root("[Timer Callback]"), context.swf.header().version, context.avm1.global_object_cell(), context.levels.get(&0).copied().unwrap(), ); // TODO: `this` is undefined for non-method timer callbacks, but our VM // currently doesn't allow `this` to be a Value. let undefined = Value::Undefined.coerce_to_object(&mut activation); let mut tick_count = 0; let cur_time = activation.context.timers.cur_time; // We have to be careful because the timer list can be mutated while updating; // a timer callback could add more timers, clear timers, etc. while activation .context .timers .peek() .map(|timer| timer.tick_time) .unwrap_or(cur_time) < cur_time { let timer = activation.context.timers.peek().unwrap(); // TODO: This is only really necessary because BinaryHeap lacks `remove` or `retain` on stable. // We can remove the timers straightaway in `clearInterval` once this is stable. if !timer.is_alive.get() { activation.context.timers.pop(); continue; } tick_count += 1; // SANITY: Only allow so many ticks per timer per update. if tick_count > Self::MAX_TICKS { // Reset our time to a little bit before the nearest timer. let next_time = activation.context.timers.peek_mut().unwrap().tick_time; activation.context.timers.cur_time = next_time.wrapping_sub(100); break; } // TODO: Can we avoid these clones? let params = timer.params.clone(); let callback = timer.callback.clone(); let callback = match callback { TimerCallback::Function(f) => Some((undefined, None, f)), TimerCallback::Method { this, method_name } => { // Fetch the callback method from the object. if let Ok((f, base_proto)) = search_prototype(Some(this), &method_name, &mut activation, this) { let f = f.coerce_to_object(&mut activation); Some((this, base_proto, f)) } else { None } } }; if let Some((this, base_proto, function)) = callback { let _ = function.call( "[Timer Callback]", &mut activation, this, base_proto, ¶ms, ); } let mut timer = activation.context.timers.peek_mut().unwrap(); if timer.is_timeout { // Timeouts only fire once. drop(timer); activation.context.timers.pop(); } else { // Reset setInterval timers. `peek_mut` re-sorts the timer in the priority queue. timer.tick_time = timer.tick_time.wrapping_add(timer.interval); } } // Return estimated time until next timer tick. activation .context .timers .peek() .map(|timer| (timer.tick_time.wrapping_sub(cur_time)) as f64 / Self::TIMER_SCALE) } /// The minimum interval we allow for timers. const MIN_INTERVAL: i32 = 10; /// The maximum timer ticks per call to `update_ticks`, for sanity. const MAX_TICKS: i32 = 10; /// The scale of the timers (microseconds). const TIMER_SCALE: f64 = 1000.0; /// Creates a new `Timers` collection. pub fn new() -> Self { Self { timers: Default::default(), timer_counter: 0, cur_time: 0, } } /// The number of timers currently active. pub fn num_timers(&self) -> usize { self.timers.len() } /// Registers a new timer and returns the timer ID. pub fn add_timer( &mut self, callback: TimerCallback<'gc>, interval: i32, params: Vec>, is_timeout: bool, ) -> i32 { // SANITY: Set a minimum interval so we don't spam too much. let interval = interval.max(Self::MIN_INTERVAL) as u64 * (Self::TIMER_SCALE as u64); self.timer_counter = self.timer_counter.wrapping_add(1); let id = self.timer_counter; let timer = Timer { id, callback, params, tick_time: self.cur_time + interval, interval, is_timeout, is_alive: std::cell::Cell::new(true), }; self.timers.push(timer); id } /// Removes a timer. pub fn remove(&mut self, id: i32) -> bool { // TODO: When `BinaryHeap::remove` is stable, we can remove it here directly. if let Some(timer) = self.timers.iter().find(|timer| timer.id == id) { timer.is_alive.set(false); true } else { false } } fn peek(&self) -> Option<&Timer<'gc>> { self.timers.peek() } fn peek_mut(&mut self) -> Option>> { self.timers.peek_mut() } fn pop(&mut self) -> Option> { self.timers.pop() } } impl Default for Timers<'_> { fn default() -> Self { Self::new() } } unsafe impl<'gc> Collect for Timers<'gc> { fn trace(&self, cc: gc_arena::CollectionContext) { for timer in &self.timers { timer.trace(cc); } } } /// A timer created via `setInterval`/`setTimeout`. /// Runs a callback when it ticks. #[derive(Debug, Collect)] #[collect(no_drop)] struct Timer<'gc> { /// The ID of the timer. id: i32, /// The callback that this timer runs when it fires. /// A callback is either a function object, or a parent object with a method name. callback: TimerCallback<'gc>, /// The parameters to pass to the callback function. params: Vec>, /// The time when this timer should fire. tick_time: u64, /// The interval between timer ticks, in microseconds. interval: u64, /// This timer only fires once if `is_timeout` is true. is_timeout: bool, /// Whether this timer has been removed. is_alive: std::cell::Cell, } // Implement `Ord` so that timers can be stored in the BinaryHeap (as a min-heap). impl PartialEq for Timer<'_> { fn eq(&self, other: &Self) -> bool { self.tick_time == other.tick_time } } impl Eq for Timer<'_> {} impl PartialOrd for Timer<'_> { fn partial_cmp(&self, other: &Self) -> Option { self.tick_time .partial_cmp(&other.tick_time) .map(|o| o.reverse()) } } impl Ord for Timer<'_> { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.tick_time.cmp(&other.tick_time).reverse() } } /// A callback fired by a `setInterval`/`setTimeout` timer. #[derive(Debug, Collect, Clone)] #[collect(no_drop)] pub enum TimerCallback<'gc> { Function(Object<'gc>), Method { this: Object<'gc>, method_name: String, }, }