use super::{RenderStatistics, RenderWorker};
use crate::interop::thread_proxy::{self, RunOnThread};
use crate::interop::timer_helper;
use ferroui_base::rendering::{IRenderTimer, RenderTimerTick};
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, PoisonError, Weak};
use std::time::Duration;
pub struct BrowserRenderTimer {
this: Weak<BrowserRenderTimer>,
tick: Mutex<Option<RenderTimerTick>>,
started: AtomicBool,
runs_in_background: bool,
thread: AtomicUsize,
out_of_turn_requested: AtomicBool,
out_of_turn_before_start: AtomicBool,
platform: TimerPlatform,
}
#[derive(Clone, Copy)]
struct TimerPlatform {
render_worker_exists: fn() -> bool,
run_animation_frames: fn(),
current_thread: fn() -> usize,
run_on_thread: RunOnThread,
now: fn() -> f64,
proxied_calls: fn() -> Option<u64>,
last_proxied_function: fn() -> Option<i64>,
tick_ended: fn(Option<u64>, Option<i64>),
}
impl BrowserRenderTimer {
pub fn new(is_background: bool) -> Arc<Self> {
Self::with_platform(
is_background,
TimerPlatform {
render_worker_exists: RenderWorker::exists,
run_animation_frames: timer_helper::run_animation_frames,
current_thread: thread_proxy::current_thread,
run_on_thread: thread_proxy::run_on_thread,
now: timer_helper::now,
proxied_calls: thread_proxy::proxied_calls,
last_proxied_function: thread_proxy::last_proxied_function,
tick_ended: RenderStatistics::tick_ended,
},
)
}
fn with_platform(is_background: bool, platform: TimerPlatform) -> Arc<Self> {
Arc::new_cyclic(|this| Self {
this: this.clone(),
tick: Mutex::new(None),
started: AtomicBool::new(false),
runs_in_background: is_background,
thread: AtomicUsize::new(0),
out_of_turn_requested: AtomicBool::new(false),
out_of_turn_before_start: AtomicBool::new(false),
platform,
})
}
pub fn start_on_this_thread(&self) {
if !self.started.swap(true, Ordering::SeqCst) {
self.thread.store((self.platform.current_thread)(), Ordering::SeqCst);
let this = self.this.clone();
timer_helper::add_animation_frame(Rc::new(move |timestamp| {
if let Some(this) = this.upgrade() {
this.render_frame_callback(timestamp);
}
}));
(self.platform.run_animation_frames)();
if self.out_of_turn_before_start.swap(false, Ordering::SeqCst) {
self.render_frame_callback((self.platform.now)());
}
}
}
fn render_frame_callback(&self, timestamp: f64) {
let tick = self.tick.lock().unwrap_or_else(PoisonError::into_inner).clone();
let Some(tick) = tick else { return };
let time = Duration::from_secs_f64(timestamp.max(0.0) / 1000.0);
if !self.runs_in_background {
tick(time);
return;
}
let before = (self.platform.proxied_calls)();
tick(time);
let proxied = match (before, (self.platform.proxied_calls)()) {
(Some(before), Some(after)) => Some(after.saturating_sub(before)),
_ => None,
};
let last = if proxied.is_some_and(|proxied| proxied > 0) {
(self.platform.last_proxied_function)()
} else {
None
};
(self.platform.tick_ended)(proxied, last);
}
}
impl IRenderTimer for BrowserRenderTimer {
fn tick(&self) -> Option<RenderTimerTick> {
self.tick.lock().unwrap_or_else(PoisonError::into_inner).clone()
}
fn set_tick(&self, value: Option<RenderTimerTick>) {
if !self.runs_in_background && !(self.platform.render_worker_exists)() {
self.start_on_this_thread();
}
*self.tick.lock().unwrap_or_else(PoisonError::into_inner) = value;
}
fn runs_in_background(&self) -> bool {
self.runs_in_background
}
fn request_tick_out_of_turn(&self) {
let mut thread = self.thread.load(Ordering::SeqCst);
if thread == 0 {
if !self.runs_in_background {
return;
}
self.out_of_turn_before_start.store(true, Ordering::SeqCst);
thread = self.thread.load(Ordering::SeqCst);
if thread == 0 || !self.out_of_turn_before_start.swap(false, Ordering::SeqCst) {
return;
}
}
if thread == (self.platform.current_thread)() {
return;
}
if self.tick.lock().unwrap_or_else(PoisonError::into_inner).is_none() {
return;
}
if self.out_of_turn_requested.swap(true, Ordering::SeqCst) {
return;
}
let this = self.this.clone();
let queued = (self.platform.run_on_thread)(
thread,
Box::new(move || {
if let Some(this) = this.upgrade() {
this.out_of_turn_requested.store(false, Ordering::SeqCst);
this.render_frame_callback((this.platform.now)());
}
}),
);
if !queued {
self.out_of_turn_requested.store(false, Ordering::SeqCst);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::interop::thread_proxy::ThreadWork;
use std::cell::{Cell, RefCell};
ferroui_base::ferro_thread_local! {
static FRAME_LOOPS: Cell<u32> = const { Cell::new(0) };
static THREAD: Cell<usize> = const { Cell::new(0) };
}
fn count_frame_loop() {
FRAME_LOOPS.with(|loops| loops.set(loops.get() + 1));
}
fn frame_loops() -> u32 {
FRAME_LOOPS.with(|loops| loops.get())
}
fn test_thread() -> usize {
THREAD.with(|thread| thread.get())
}
fn refuse(_thread: usize, _work: ThreadWork) -> bool {
false
}
fn quarter_of_a_second() -> f64 {
250.0
}
fn no_render_worker() -> bool {
false
}
fn a_render_worker() -> bool {
true
}
ferroui_base::ferro_thread_local! {
static PROXIED: Cell<Option<u64>> = const { Cell::new(None) };
static TICKS: RefCell<Vec<(Option<u64>, Option<i64>)>> = const { RefCell::new(Vec::new()) };
}
fn proxied() -> Option<u64> {
PROXIED.with(|proxied| proxied.get())
}
fn function_seven() -> Option<i64> {
Some(7)
}
fn record_tick(proxied: Option<u64>, last: Option<i64>) {
TICKS.with(|ticks| ticks.borrow_mut().push((proxied, last)));
}
fn ticks() -> Vec<(Option<u64>, Option<i64>)> {
TICKS.with(|ticks| ticks.borrow().clone())
}
fn platform(run_on_thread: RunOnThread) -> TimerPlatform {
TimerPlatform {
render_worker_exists: no_render_worker,
run_animation_frames: count_frame_loop,
current_thread: test_thread,
run_on_thread,
now: quarter_of_a_second,
proxied_calls: proxied,
last_proxied_function: function_seven,
tick_ended: record_tick,
}
}
#[test]
fn frames_reach_the_tick_as_durations_and_the_loop_is_started_once() {
let timer = BrowserRenderTimer::with_platform(false, platform(refuse));
let seen = Arc::new(Mutex::new(Vec::new()));
assert!(!timer.runs_in_background());
assert!(timer.tick().is_none());
timer_helper::js_export_on_animation_frame(8.0);
let log = seen.clone();
timer.set_tick(Some(Arc::new(move |time| log.lock().unwrap().push(time))));
assert_eq!(1, frame_loops());
timer_helper::js_export_on_animation_frame(16.0);
timer_helper::js_export_on_animation_frame(1500.5);
timer.set_tick(None);
timer_helper::js_export_on_animation_frame(2000.0);
assert_eq!(1, frame_loops());
assert_eq!(vec![Duration::from_millis(16), Duration::from_micros(1_500_500)], *seen.lock().unwrap());
assert!(ticks().is_empty());
}
#[test]
fn the_ticks_of_a_background_timer_report_what_they_asked_of_the_main_thread() {
let timer = BrowserRenderTimer::with_platform(true, platform(refuse));
let step = Arc::new(Mutex::new(0));
let at = step.clone();
timer.set_tick(Some(Arc::new(move |_: Duration| {
let mut step = at.lock().unwrap();
*step += 1;
match *step {
1 => PROXIED.with(|proxied| proxied.set(Some(12))),
3 => PROXIED.with(|proxied| proxied.set(None)),
_ => {}
}
})));
timer.start_on_this_thread();
assert!(ticks().is_empty());
PROXIED.with(|proxied| proxied.set(Some(10)));
timer_helper::js_export_on_animation_frame(16.0);
timer_helper::js_export_on_animation_frame(32.0);
timer_helper::js_export_on_animation_frame(48.0);
assert_eq!(vec![(Some(2), Some(7)), (Some(0), None), (None, None)], ticks());
}
#[test]
fn a_background_timer_reports_itself_and_is_not_started_by_its_tick() {
let timer = BrowserRenderTimer::with_platform(true, platform(refuse));
let seen = Arc::new(Mutex::new(Vec::new()));
assert!(timer.runs_in_background());
let log = seen.clone();
timer.set_tick(Some(Arc::new(move |time| log.lock().unwrap().push(time))));
assert_eq!(0, frame_loops());
timer_helper::js_export_on_animation_frame(16.0);
assert!(seen.lock().unwrap().is_empty());
timer.start_on_this_thread();
timer.start_on_this_thread();
assert_eq!(1, frame_loops());
timer_helper::js_export_on_animation_frame(32.0);
assert_eq!(vec![Duration::from_millis(32)], *seen.lock().unwrap());
}
#[test]
fn with_a_render_worker_no_timer_is_started_by_its_tick() {
let platform = TimerPlatform { render_worker_exists: a_render_worker, ..platform(refuse) };
let timer = BrowserRenderTimer::with_platform(false, platform);
let seen = Arc::new(Mutex::new(Vec::new()));
assert!(!timer.runs_in_background());
let log = seen.clone();
timer.set_tick(Some(Arc::new(move |time| log.lock().unwrap().push(time))));
assert_eq!(0, frame_loops());
timer_helper::js_export_on_animation_frame(16.0);
assert!(seen.lock().unwrap().is_empty());
timer.start_on_this_thread();
assert_eq!(1, frame_loops());
timer_helper::js_export_on_animation_frame(32.0);
assert_eq!(vec![Duration::from_millis(32)], *seen.lock().unwrap());
}
#[test]
fn a_tick_out_of_turn_is_queued_once_for_the_thread_that_ticks() {
static QUEUED: Mutex<Vec<(usize, ThreadWork)>> = Mutex::new(Vec::new());
fn queue(thread: usize, work: ThreadWork) -> bool {
QUEUED.lock().unwrap().push((thread, work));
true
}
fn take() -> Vec<(usize, ThreadWork)> {
QUEUED.lock().unwrap().drain(..).collect()
}
fn request_twice_from_another_thread(timer: &Arc<BrowserRenderTimer>) {
let timer = timer.clone();
std::thread::spawn(move || {
THREAD.with(|thread| thread.set(12));
timer.request_tick_out_of_turn();
timer.request_tick_out_of_turn();
})
.join()
.unwrap();
}
THREAD.with(|thread| thread.set(11));
let timer = BrowserRenderTimer::with_platform(true, platform(queue));
let seen = Arc::new(Mutex::new(Vec::new()));
let log = seen.clone();
timer.set_tick(Some(Arc::new(move |time| log.lock().unwrap().push(time))));
timer.start_on_this_thread();
assert!(seen.lock().unwrap().is_empty());
timer.request_tick_out_of_turn();
assert!(take().is_empty());
request_twice_from_another_thread(&timer);
let queued = take();
assert_eq!(vec![11], queued.iter().map(|(thread, _)| *thread).collect::<Vec<usize>>());
assert!(seen.lock().unwrap().is_empty());
for (_, work) in queued {
work();
}
assert_eq!(vec![Duration::from_millis(250)], *seen.lock().unwrap());
request_twice_from_another_thread(&timer);
assert_eq!(1, take().len());
timer.out_of_turn_requested.store(false, Ordering::SeqCst);
timer.set_tick(None);
request_twice_from_another_thread(&timer);
assert!(take().is_empty());
}
#[test]
fn a_tick_asked_for_before_the_start_is_made_by_the_thread_that_starts_the_timer() {
THREAD.with(|thread| thread.set(31));
let timer = BrowserRenderTimer::with_platform(true, platform(refuse));
let seen = Arc::new(Mutex::new(Vec::new()));
let log = seen.clone();
timer.set_tick(Some(Arc::new(move |time| log.lock().unwrap().push(time))));
std::thread::spawn({
let timer = timer.clone();
move || {
THREAD.with(|thread| thread.set(32));
timer.request_tick_out_of_turn();
timer.request_tick_out_of_turn();
}
})
.join()
.unwrap();
assert!(seen.lock().unwrap().is_empty());
timer.start_on_this_thread();
assert_eq!(vec![Duration::from_millis(250)], *seen.lock().unwrap());
timer.start_on_this_thread();
assert_eq!(1, seen.lock().unwrap().len());
}
#[test]
fn a_timer_of_the_page_keeps_no_request_made_before_its_start() {
THREAD.with(|thread| thread.set(41));
let platform = TimerPlatform { render_worker_exists: a_render_worker, ..platform(refuse) };
let timer = BrowserRenderTimer::with_platform(false, platform);
let seen = Arc::new(Mutex::new(Vec::new()));
let log = seen.clone();
timer.set_tick(Some(Arc::new(move |time| log.lock().unwrap().push(time))));
timer.request_tick_out_of_turn();
timer.start_on_this_thread();
assert!(seen.lock().unwrap().is_empty());
}
#[test]
fn a_refused_tick_out_of_turn_can_be_asked_for_again() {
THREAD.with(|thread| thread.set(21));
let timer = BrowserRenderTimer::with_platform(true, platform(refuse));
timer.set_tick(Some(Arc::new(|_: Duration| {})));
timer.start_on_this_thread();
std::thread::spawn({
let timer = timer.clone();
move || timer.request_tick_out_of_turn()
})
.join()
.unwrap();
assert!(!timer.out_of_turn_requested.load(Ordering::SeqCst));
}
}