Skip to main content

ferroui_browser/rendering/
browser_render_timer.rs

1use 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
10/// A render timer that ticks with the animation frames of the thread it is
11/// started on: the page, or the worker of a render thread.
12pub struct BrowserRenderTimer {
13    this: Weak<BrowserRenderTimer>,
14    tick: Mutex<Option<RenderTimerTick>>,
15    started: AtomicBool,
16    runs_in_background: bool,
17    /// The thread the frames are followed on, as
18    /// [`thread_proxy::current_thread`] names it there; 0 until the timer is
19    /// started, and always in a build without threads.
20    thread: AtomicUsize,
21    /// Whether a tick out of turn is on its way to that thread.
22    out_of_turn_requested: AtomicBool,
23    /// Whether a tick out of turn was asked for before the timer was
24    /// started: the thread that starts it ticks once at once.
25    out_of_turn_before_start: AtomicBool,
26    platform: TimerPlatform,
27}
28
29/// The calls the timer makes outside itself; replaced by recorders in the
30/// tests.
31#[derive(Clone, Copy)]
32struct TimerPlatform {
33    /// Whether the page has a render thread.
34    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    /// The calls the runtime has carried from the calling thread to the
40    /// main thread so far, and the function of the last one
41    /// ([`thread_proxy::proxied_calls`]).
42    proxied_calls: fn() -> Option<u64>,
43    last_proxied_function: fn() -> Option<i64>,
44    /// Receives what a tick of a background timer cost
45    /// ([`RenderStatistics::tick_ended`]).
46    tick_ended: fn(Option<u64>, Option<i64>),
47}
48
49impl BrowserRenderTimer {
50    /// Creates the timer. `is_background` tells whether its ticks arrive on
51    /// a thread other than the UI thread: such a timer is started by the
52    /// thread that renders, with
53    /// [`start_on_this_thread`](Self::start_on_this_thread).
54    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    /// Starts following the animation frames on the current thread. Does
84    /// nothing when the timer is started already.
85    ///
86    /// The thread has to live for as long as the timer is used: its frames
87    /// arrive at the event loop of its worker, and so does a tick out of
88    /// turn.
89    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            // A tick that was asked for before the thread got here: whoever
100            // asked may be waiting for it, and the first animation frame of
101            // this thread may be a long way off (a page that is hidden has
102            // none).
103            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        // A tick of a render thread: what it asked of the main thread of
118        // the page is counted, for whoever wants to know that a frame does
119        // not depend on that thread.
120        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        // Upstream starts the loop here unless the platform runs with
142        // threads, where only its render worker starts it. Two conditions
143        // here. The timer's own: one that ticks in the background is
144        // started by the thread that renders, and the thread that sets the
145        // callback (the render loop does, on any thread) must not become
146        // the one that ticks. And upstream's, as a fact of the page instead
147        // of the build: with a render thread no timer is started by its
148        // callback, so the frames of the page never drive a loop whose
149        // surfaces belong to the worker.
150        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    /// Queues one tick for the thread the timer was started on, which runs
162    /// it from its event loop without waiting for its next animation frame.
163    /// Requests made before that tick has started are one request. The
164    /// timestamp of the tick is read from the clock of the animation frames
165    /// of that thread.
166    ///
167    /// A request made to a background timer that is not started yet is
168    /// kept: the thread that starts the timer ticks once when it does. (The
169    /// thread of the page asks before it waits for a frame, and the render
170    /// thread it started a moment ago may not have got that far.)
171    ///
172    /// Does nothing when the caller is the thread that ticks (it cannot be
173    /// waiting for itself), while no callback is set, and in a build
174    /// without threads.
175    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            // The timer may have been started in between. Whichever of the
183            // two threads takes the flag makes the tick: the one that starts
184            // the timer on the spot, this one through the queue below.
185            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                    // Cleared first: a request made from here on is for a
205                    // frame after this one.
206                    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        /// The frame loops started on the thread of a test.
225        static FRAME_LOOPS: Cell<u32> = const { Cell::new(0) };
226        /// The id a test gives its thread; 0 is what a build without threads
227        /// reports.
228        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        /// What the script of the thread of a test says it has proxied.
261        static PROXIED: Cell<Option<u64>> = const { Cell::new(None) };
262        /// What the ticks of the thread of a test reported.
263        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        // A frame before a tick is set goes nowhere.
303        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        // The ticks of the page are not measured.
317        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        // A tick that proxies two calls, one that proxies none, and one on
324        // a thread whose script stopped counting half way.
325        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        // No frame without a tick: nothing is reported.
339        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        // The thread that set the tick is not the one that ticks.
357        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        // The thread that set the tick is the one of the page; its frames
378        // do not reach the tick.
379        assert_eq!(0, frame_loops());
380        timer_helper::js_export_on_animation_frame(16.0);
381        assert!(seen.lock().unwrap().is_empty());
382
383        // The render worker starts it on its thread.
384        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        // Two requests, made by a thread other than the one of the test.
401        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        // The thread that ticks does not ask itself.
421        timer.request_tick_out_of_turn();
422        assert!(take().is_empty());
423
424        // Two requests before the tick are one, for the thread that ticks;
425        // the caller is not called back.
426        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        // After the tick a request is queued again.
436        request_twice_from_another_thread(&timer);
437        assert_eq!(1, take().len());
438
439        // Without a callback nothing is queued. The request above was
440        // dropped without running, so its flag is cleared by hand.
441        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        // Not started: there is no thread to queue the tick for. Two
456        // requests are one.
457        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        // Once, and not again by a second start.
472        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}