ferroui_browser/rendering/
browser_render_timer.rs1use super::{RenderStatistics, RenderWorker};
2use crate::interop::thread_proxy::{self, RunOnThread};
3use crate::interop::timer_helper;
4use ferroui_base::rendering::{IRenderTimer, RenderTimerTick};
5use std::rc::Rc;
6use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
7use std::sync::{Arc, Mutex, PoisonError, Weak};
8use std::time::Duration;
9
10pub struct BrowserRenderTimer {
13 this: Weak<BrowserRenderTimer>,
14 tick: Mutex<Option<RenderTimerTick>>,
15 started: AtomicBool,
16 runs_in_background: bool,
17 thread: AtomicUsize,
21 out_of_turn_requested: AtomicBool,
23 out_of_turn_before_start: AtomicBool,
26 platform: TimerPlatform,
27}
28
29#[derive(Clone, Copy)]
32struct TimerPlatform {
33 render_worker_exists: fn() -> bool,
35 run_animation_frames: fn(),
36 current_thread: fn() -> usize,
37 run_on_thread: RunOnThread,
38 now: fn() -> f64,
39 proxied_calls: fn() -> Option<u64>,
43 last_proxied_function: fn() -> Option<i64>,
44 tick_ended: fn(Option<u64>, Option<i64>),
47}
48
49impl BrowserRenderTimer {
50 pub fn new(is_background: bool) -> Arc<Self> {
55 Self::with_platform(
56 is_background,
57 TimerPlatform {
58 render_worker_exists: RenderWorker::exists,
59 run_animation_frames: timer_helper::run_animation_frames,
60 current_thread: thread_proxy::current_thread,
61 run_on_thread: thread_proxy::run_on_thread,
62 now: timer_helper::now,
63 proxied_calls: thread_proxy::proxied_calls,
64 last_proxied_function: thread_proxy::last_proxied_function,
65 tick_ended: RenderStatistics::tick_ended,
66 },
67 )
68 }
69
70 fn with_platform(is_background: bool, platform: TimerPlatform) -> Arc<Self> {
71 Arc::new_cyclic(|this| Self {
72 this: this.clone(),
73 tick: Mutex::new(None),
74 started: AtomicBool::new(false),
75 runs_in_background: is_background,
76 thread: AtomicUsize::new(0),
77 out_of_turn_requested: AtomicBool::new(false),
78 out_of_turn_before_start: AtomicBool::new(false),
79 platform,
80 })
81 }
82
83 pub fn start_on_this_thread(&self) {
90 if !self.started.swap(true, Ordering::SeqCst) {
91 self.thread.store((self.platform.current_thread)(), Ordering::SeqCst);
92 let this = self.this.clone();
93 timer_helper::add_animation_frame(Rc::new(move |timestamp| {
94 if let Some(this) = this.upgrade() {
95 this.render_frame_callback(timestamp);
96 }
97 }));
98 (self.platform.run_animation_frames)();
99 if self.out_of_turn_before_start.swap(false, Ordering::SeqCst) {
104 self.render_frame_callback((self.platform.now)());
105 }
106 }
107 }
108
109 fn render_frame_callback(&self, timestamp: f64) {
110 let tick = self.tick.lock().unwrap_or_else(PoisonError::into_inner).clone();
111 let Some(tick) = tick else { return };
112 let time = Duration::from_secs_f64(timestamp.max(0.0) / 1000.0);
113 if !self.runs_in_background {
114 tick(time);
115 return;
116 }
117 let before = (self.platform.proxied_calls)();
121 tick(time);
122 let proxied = match (before, (self.platform.proxied_calls)()) {
123 (Some(before), Some(after)) => Some(after.saturating_sub(before)),
124 _ => None,
125 };
126 let last = if proxied.is_some_and(|proxied| proxied > 0) {
127 (self.platform.last_proxied_function)()
128 } else {
129 None
130 };
131 (self.platform.tick_ended)(proxied, last);
132 }
133}
134
135impl IRenderTimer for BrowserRenderTimer {
136 fn tick(&self) -> Option<RenderTimerTick> {
137 self.tick.lock().unwrap_or_else(PoisonError::into_inner).clone()
138 }
139
140 fn set_tick(&self, value: Option<RenderTimerTick>) {
141 if !self.runs_in_background && !(self.platform.render_worker_exists)() {
151 self.start_on_this_thread();
152 }
153
154 *self.tick.lock().unwrap_or_else(PoisonError::into_inner) = value;
155 }
156
157 fn runs_in_background(&self) -> bool {
158 self.runs_in_background
159 }
160
161 fn request_tick_out_of_turn(&self) {
176 let mut thread = self.thread.load(Ordering::SeqCst);
177 if thread == 0 {
178 if !self.runs_in_background {
179 return;
180 }
181 self.out_of_turn_before_start.store(true, Ordering::SeqCst);
182 thread = self.thread.load(Ordering::SeqCst);
186 if thread == 0 || !self.out_of_turn_before_start.swap(false, Ordering::SeqCst) {
187 return;
188 }
189 }
190 if thread == (self.platform.current_thread)() {
191 return;
192 }
193 if self.tick.lock().unwrap_or_else(PoisonError::into_inner).is_none() {
194 return;
195 }
196 if self.out_of_turn_requested.swap(true, Ordering::SeqCst) {
197 return;
198 }
199 let this = self.this.clone();
200 let queued = (self.platform.run_on_thread)(
201 thread,
202 Box::new(move || {
203 if let Some(this) = this.upgrade() {
204 this.out_of_turn_requested.store(false, Ordering::SeqCst);
207 this.render_frame_callback((this.platform.now)());
208 }
209 }),
210 );
211 if !queued {
212 self.out_of_turn_requested.store(false, Ordering::SeqCst);
213 }
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::interop::thread_proxy::ThreadWork;
221 use std::cell::{Cell, RefCell};
222
223 ferroui_base::ferro_thread_local! {
224 static FRAME_LOOPS: Cell<u32> = const { Cell::new(0) };
226 static THREAD: Cell<usize> = const { Cell::new(0) };
229 }
230
231 fn count_frame_loop() {
232 FRAME_LOOPS.with(|loops| loops.set(loops.get() + 1));
233 }
234
235 fn frame_loops() -> u32 {
236 FRAME_LOOPS.with(|loops| loops.get())
237 }
238
239 fn test_thread() -> usize {
240 THREAD.with(|thread| thread.get())
241 }
242
243 fn refuse(_thread: usize, _work: ThreadWork) -> bool {
244 false
245 }
246
247 fn quarter_of_a_second() -> f64 {
248 250.0
249 }
250
251 fn no_render_worker() -> bool {
252 false
253 }
254
255 fn a_render_worker() -> bool {
256 true
257 }
258
259 ferroui_base::ferro_thread_local! {
260 static PROXIED: Cell<Option<u64>> = const { Cell::new(None) };
262 static TICKS: RefCell<Vec<(Option<u64>, Option<i64>)>> = const { RefCell::new(Vec::new()) };
264 }
265
266 fn proxied() -> Option<u64> {
267 PROXIED.with(|proxied| proxied.get())
268 }
269
270 fn function_seven() -> Option<i64> {
271 Some(7)
272 }
273
274 fn record_tick(proxied: Option<u64>, last: Option<i64>) {
275 TICKS.with(|ticks| ticks.borrow_mut().push((proxied, last)));
276 }
277
278 fn ticks() -> Vec<(Option<u64>, Option<i64>)> {
279 TICKS.with(|ticks| ticks.borrow().clone())
280 }
281
282 fn platform(run_on_thread: RunOnThread) -> TimerPlatform {
283 TimerPlatform {
284 render_worker_exists: no_render_worker,
285 run_animation_frames: count_frame_loop,
286 current_thread: test_thread,
287 run_on_thread,
288 now: quarter_of_a_second,
289 proxied_calls: proxied,
290 last_proxied_function: function_seven,
291 tick_ended: record_tick,
292 }
293 }
294
295 #[test]
296 fn frames_reach_the_tick_as_durations_and_the_loop_is_started_once() {
297 let timer = BrowserRenderTimer::with_platform(false, platform(refuse));
298 let seen = Arc::new(Mutex::new(Vec::new()));
299 assert!(!timer.runs_in_background());
300 assert!(timer.tick().is_none());
301
302 timer_helper::js_export_on_animation_frame(8.0);
304
305 let log = seen.clone();
306 timer.set_tick(Some(Arc::new(move |time| log.lock().unwrap().push(time))));
307 assert_eq!(1, frame_loops());
308 timer_helper::js_export_on_animation_frame(16.0);
309 timer_helper::js_export_on_animation_frame(1500.5);
310
311 timer.set_tick(None);
312 timer_helper::js_export_on_animation_frame(2000.0);
313
314 assert_eq!(1, frame_loops());
315 assert_eq!(vec![Duration::from_millis(16), Duration::from_micros(1_500_500)], *seen.lock().unwrap());
316 assert!(ticks().is_empty());
318 }
319
320 #[test]
321 fn the_ticks_of_a_background_timer_report_what_they_asked_of_the_main_thread() {
322 let timer = BrowserRenderTimer::with_platform(true, platform(refuse));
323 let step = Arc::new(Mutex::new(0));
326 let at = step.clone();
327 timer.set_tick(Some(Arc::new(move |_: Duration| {
328 let mut step = at.lock().unwrap();
329 *step += 1;
330 match *step {
331 1 => PROXIED.with(|proxied| proxied.set(Some(12))),
332 3 => PROXIED.with(|proxied| proxied.set(None)),
333 _ => {}
334 }
335 })));
336 timer.start_on_this_thread();
337
338 assert!(ticks().is_empty());
340 PROXIED.with(|proxied| proxied.set(Some(10)));
341 timer_helper::js_export_on_animation_frame(16.0);
342 timer_helper::js_export_on_animation_frame(32.0);
343 timer_helper::js_export_on_animation_frame(48.0);
344
345 assert_eq!(vec![(Some(2), Some(7)), (Some(0), None), (None, None)], ticks());
346 }
347
348 #[test]
349 fn a_background_timer_reports_itself_and_is_not_started_by_its_tick() {
350 let timer = BrowserRenderTimer::with_platform(true, platform(refuse));
351 let seen = Arc::new(Mutex::new(Vec::new()));
352 assert!(timer.runs_in_background());
353
354 let log = seen.clone();
355 timer.set_tick(Some(Arc::new(move |time| log.lock().unwrap().push(time))));
356 assert_eq!(0, frame_loops());
358 timer_helper::js_export_on_animation_frame(16.0);
359 assert!(seen.lock().unwrap().is_empty());
360
361 timer.start_on_this_thread();
362 timer.start_on_this_thread();
363 assert_eq!(1, frame_loops());
364 timer_helper::js_export_on_animation_frame(32.0);
365 assert_eq!(vec![Duration::from_millis(32)], *seen.lock().unwrap());
366 }
367
368 #[test]
369 fn with_a_render_worker_no_timer_is_started_by_its_tick() {
370 let platform = TimerPlatform { render_worker_exists: a_render_worker, ..platform(refuse) };
371 let timer = BrowserRenderTimer::with_platform(false, platform);
372 let seen = Arc::new(Mutex::new(Vec::new()));
373 assert!(!timer.runs_in_background());
374
375 let log = seen.clone();
376 timer.set_tick(Some(Arc::new(move |time| log.lock().unwrap().push(time))));
377 assert_eq!(0, frame_loops());
380 timer_helper::js_export_on_animation_frame(16.0);
381 assert!(seen.lock().unwrap().is_empty());
382
383 timer.start_on_this_thread();
385 assert_eq!(1, frame_loops());
386 timer_helper::js_export_on_animation_frame(32.0);
387 assert_eq!(vec![Duration::from_millis(32)], *seen.lock().unwrap());
388 }
389
390 #[test]
391 fn a_tick_out_of_turn_is_queued_once_for_the_thread_that_ticks() {
392 static QUEUED: Mutex<Vec<(usize, ThreadWork)>> = Mutex::new(Vec::new());
393 fn queue(thread: usize, work: ThreadWork) -> bool {
394 QUEUED.lock().unwrap().push((thread, work));
395 true
396 }
397 fn take() -> Vec<(usize, ThreadWork)> {
398 QUEUED.lock().unwrap().drain(..).collect()
399 }
400 fn request_twice_from_another_thread(timer: &Arc<BrowserRenderTimer>) {
402 let timer = timer.clone();
403 std::thread::spawn(move || {
404 THREAD.with(|thread| thread.set(12));
405 timer.request_tick_out_of_turn();
406 timer.request_tick_out_of_turn();
407 })
408 .join()
409 .unwrap();
410 }
411
412 THREAD.with(|thread| thread.set(11));
413 let timer = BrowserRenderTimer::with_platform(true, platform(queue));
414 let seen = Arc::new(Mutex::new(Vec::new()));
415
416 let log = seen.clone();
417 timer.set_tick(Some(Arc::new(move |time| log.lock().unwrap().push(time))));
418 timer.start_on_this_thread();
419 assert!(seen.lock().unwrap().is_empty());
420 timer.request_tick_out_of_turn();
422 assert!(take().is_empty());
423
424 request_twice_from_another_thread(&timer);
427 let queued = take();
428 assert_eq!(vec![11], queued.iter().map(|(thread, _)| *thread).collect::<Vec<usize>>());
429 assert!(seen.lock().unwrap().is_empty());
430 for (_, work) in queued {
431 work();
432 }
433 assert_eq!(vec![Duration::from_millis(250)], *seen.lock().unwrap());
434
435 request_twice_from_another_thread(&timer);
437 assert_eq!(1, take().len());
438
439 timer.out_of_turn_requested.store(false, Ordering::SeqCst);
442 timer.set_tick(None);
443 request_twice_from_another_thread(&timer);
444 assert!(take().is_empty());
445 }
446
447 #[test]
448 fn a_tick_asked_for_before_the_start_is_made_by_the_thread_that_starts_the_timer() {
449 THREAD.with(|thread| thread.set(31));
450 let timer = BrowserRenderTimer::with_platform(true, platform(refuse));
451 let seen = Arc::new(Mutex::new(Vec::new()));
452 let log = seen.clone();
453 timer.set_tick(Some(Arc::new(move |time| log.lock().unwrap().push(time))));
454
455 std::thread::spawn({
458 let timer = timer.clone();
459 move || {
460 THREAD.with(|thread| thread.set(32));
461 timer.request_tick_out_of_turn();
462 timer.request_tick_out_of_turn();
463 }
464 })
465 .join()
466 .unwrap();
467 assert!(seen.lock().unwrap().is_empty());
468
469 timer.start_on_this_thread();
470 assert_eq!(vec![Duration::from_millis(250)], *seen.lock().unwrap());
471 timer.start_on_this_thread();
473 assert_eq!(1, seen.lock().unwrap().len());
474 }
475
476 #[test]
477 fn a_timer_of_the_page_keeps_no_request_made_before_its_start() {
478 THREAD.with(|thread| thread.set(41));
479 let platform = TimerPlatform { render_worker_exists: a_render_worker, ..platform(refuse) };
480 let timer = BrowserRenderTimer::with_platform(false, platform);
481 let seen = Arc::new(Mutex::new(Vec::new()));
482 let log = seen.clone();
483 timer.set_tick(Some(Arc::new(move |time| log.lock().unwrap().push(time))));
484
485 timer.request_tick_out_of_turn();
486 timer.start_on_this_thread();
487
488 assert!(seen.lock().unwrap().is_empty());
489 }
490
491 #[test]
492 fn a_refused_tick_out_of_turn_can_be_asked_for_again() {
493 THREAD.with(|thread| thread.set(21));
494 let timer = BrowserRenderTimer::with_platform(true, platform(refuse));
495 timer.set_tick(Some(Arc::new(|_: Duration| {})));
496 timer.start_on_this_thread();
497
498 std::thread::spawn({
499 let timer = timer.clone();
500 move || timer.request_tick_out_of_turn()
501 })
502 .join()
503 .unwrap();
504
505 assert!(!timer.out_of_turn_requested.load(Ordering::SeqCst));
506 }
507}