1use crate::interop::thread_proxy::{self, RunOnThread};
2use ferroui_base::threading::{
3 DispatcherImplEvent, IDispatcherImpl, IDispatcherImplWithExplicitBackgroundProcessing, IDispatcherSignal,
4};
5use std::cell::{Cell, RefCell};
6use std::rc::Rc;
7use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
8use std::sync::Arc;
9use std::thread::{self, ThreadId};
10use std::time::Instant;
11use wasm_bindgen::prelude::*;
12
13#[wasm_bindgen(raw_module = "./ferroui.js")]
14extern "C" {
15 #[wasm_bindgen(js_namespace = SingleThreadedDispatcherHelper, js_name = signal)]
16 fn js_signal();
17
18 #[wasm_bindgen(js_namespace = SingleThreadedDispatcherHelper, js_name = requestBackgroundProcessing)]
19 fn js_request_background_processing();
20
21 #[wasm_bindgen(js_namespace = SingleThreadedDispatcherHelper, js_name = setTimer)]
22 fn js_set_timer(delay_ms: i32);
23
24 #[wasm_bindgen(js_namespace = SingleThreadedDispatcherHelper, js_name = clearTimer)]
25 fn js_clear_timer();
26}
27
28trait IScheduling {
31 fn signal(&self);
32 fn request_background_processing(&self);
33 fn set_timer(&self, delay_ms: i32);
34 fn clear_timer(&self);
35}
36
37struct PageScheduling;
38
39impl IScheduling for PageScheduling {
40 fn signal(&self) {
41 js_signal();
42 }
43
44 fn request_background_processing(&self) {
45 js_request_background_processing();
46 }
47
48 fn set_timer(&self, delay_ms: i32) {
49 js_set_timer(delay_ms);
50 }
51
52 fn clear_timer(&self) {
53 js_clear_timer();
54 }
55}
56
57ferroui_base::ferro_thread_local! {
58 static INSTANCE: RefCell<Option<Rc<BrowserSingleThreadedDispatcherImpl>>> = const { RefCell::new(None) };
59}
60
61fn instance() -> Option<Rc<BrowserSingleThreadedDispatcherImpl>> {
62 INSTANCE.with(|instance| instance.borrow().clone())
63}
64
65static LOOP_THREAD: AtomicUsize = AtomicUsize::new(0);
69
70static CROSS_THREAD_SIGNAL_PENDING: AtomicBool = AtomicBool::new(false);
73
74pub struct BrowserSingleThreadedDispatcherImpl {
83 thread: ThreadId,
84 clock: Instant,
85 signaled_flag: Cell<bool>,
86 background_requested: Cell<bool>,
87 timer_set: Cell<bool>,
88 signaled: DispatcherImplEvent,
89 timer: DispatcherImplEvent,
90 ready_for_background_processing: DispatcherImplEvent,
91 scheduling: Box<dyn IScheduling>,
92}
93
94struct Signal;
102
103impl IDispatcherSignal for Signal {
104 fn signal(&self) {
105 if let Some(instance) = instance() {
106 IDispatcherImpl::signal(&*instance);
107 } else {
108 signal_from_another_thread(
109 &CROSS_THREAD_SIGNAL_PENDING,
110 LOOP_THREAD.load(Ordering::SeqCst),
111 thread_proxy::run_on_thread,
112 );
113 }
114 }
115}
116
117fn signal_from_another_thread(pending: &'static AtomicBool, loop_thread: usize, run_on_thread: RunOnThread) {
128 if loop_thread == 0 {
129 return;
130 }
131 if pending.swap(true, Ordering::SeqCst) {
132 return;
133 }
134 let queued = run_on_thread(
135 loop_thread,
136 Box::new(move || {
137 pending.store(false, Ordering::SeqCst);
140 if let Some(instance) = instance() {
141 IDispatcherImpl::signal(&*instance);
142 }
143 }),
144 );
145 if !queued {
146 pending.store(false, Ordering::SeqCst);
147 }
148}
149
150impl BrowserSingleThreadedDispatcherImpl {
151 pub fn new() -> Rc<Self> {
154 Self::with_scheduling(Box::new(PageScheduling))
155 }
156
157 fn with_scheduling(scheduling: Box<dyn IScheduling>) -> Rc<Self> {
158 let this = Rc::new(Self {
159 thread: thread::current().id(),
160 clock: Instant::now(),
161 signaled_flag: Cell::new(false),
162 background_requested: Cell::new(false),
163 timer_set: Cell::new(false),
164 signaled: DispatcherImplEvent::new(),
165 timer: DispatcherImplEvent::new(),
166 ready_for_background_processing: DispatcherImplEvent::new(),
167 scheduling,
168 });
169 INSTANCE.with(|instance| *instance.borrow_mut() = Some(this.clone()));
170 LOOP_THREAD.store(thread_proxy::current_thread(), Ordering::SeqCst);
171 this
172 }
173}
174
175impl IDispatcherImpl for BrowserSingleThreadedDispatcherImpl {
176 fn current_thread_is_loop_thread(&self) -> bool {
177 thread::current().id() == self.thread
178 }
179
180 fn signal(&self) {
181 if self.signaled_flag.get() {
182 return;
183 }
184 self.signaled_flag.set(true);
185 self.scheduling.signal();
186 }
187
188 fn signal_handle(&self) -> Arc<dyn IDispatcherSignal> {
189 Arc::new(Signal)
190 }
191
192 fn signaled(&self) -> &DispatcherImplEvent {
193 &self.signaled
194 }
195
196 fn timer(&self) -> &DispatcherImplEvent {
197 &self.timer
198 }
199
200 fn now(&self) -> i64 {
201 self.clock.elapsed().as_millis() as i64
202 }
203
204 fn update_timer(&self, due_time_in_ms: Option<i64>) {
205 if self.timer_set.get() {
206 self.scheduling.clear_timer();
207 self.timer_set.set(false);
208 }
209
210 if let Some(due_time) = due_time_in_ms {
211 self.scheduling.set_timer((due_time - self.now()).clamp(0, i32::MAX as i64) as i32);
212 self.timer_set.set(true);
213 }
214 }
215
216 fn as_explicit_background_processing(&self) -> Option<&dyn IDispatcherImplWithExplicitBackgroundProcessing> {
217 Some(self)
218 }
219}
220
221impl IDispatcherImplWithExplicitBackgroundProcessing for BrowserSingleThreadedDispatcherImpl {
222 fn ready_for_background_processing(&self) -> &DispatcherImplEvent {
223 &self.ready_for_background_processing
224 }
225
226 fn request_background_processing(&self) {
227 if self.background_requested.get() {
228 return;
229 }
230 self.background_requested.set(true);
231 self.scheduling.request_background_processing();
232 }
233}
234
235#[wasm_bindgen(js_name = BrowserSingleThreadedDispatcherImpl_OnSignaled)]
237pub fn on_signaled() {
238 let Some(impl_) = instance() else { return };
239 impl_.signaled_flag.set(false);
240 impl_.signaled.raise();
241}
242
243#[wasm_bindgen(js_name = BrowserSingleThreadedDispatcherImpl_OnReadyForBackgroundProcessing)]
245pub fn on_ready_for_background_processing() {
246 let Some(impl_) = instance() else { return };
247 impl_.background_requested.set(false);
248 impl_.ready_for_background_processing.raise();
249}
250
251#[wasm_bindgen(js_name = BrowserSingleThreadedDispatcherImpl_OnTimer)]
253pub fn on_timer() {
254 let Some(impl_) = instance() else { return };
255 impl_.timer_set.set(false);
256 impl_.timer.raise();
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[derive(Default)]
264 struct Recorder {
265 calls: Rc<RefCell<Vec<String>>>,
266 }
267
268 impl IScheduling for Recorder {
269 fn signal(&self) {
270 self.calls.borrow_mut().push("signal".to_string());
271 }
272
273 fn request_background_processing(&self) {
274 self.calls.borrow_mut().push("background".to_string());
275 }
276
277 fn set_timer(&self, delay_ms: i32) {
278 self.calls.borrow_mut().push(format!("setTimer({})", delay_ms.min(1_000_000) / 1000));
279 }
280
281 fn clear_timer(&self) {
282 self.calls.borrow_mut().push("clearTimer".to_string());
283 }
284 }
285
286 fn create() -> (Rc<BrowserSingleThreadedDispatcherImpl>, Rc<RefCell<Vec<String>>>) {
287 let calls = Rc::new(RefCell::new(Vec::new()));
288 let impl_ = BrowserSingleThreadedDispatcherImpl::with_scheduling(Box::new(Recorder { calls: calls.clone() }));
289 (impl_, calls)
290 }
291
292 fn counter(event: &DispatcherImplEvent) -> Rc<Cell<u32>> {
293 let count = Rc::new(Cell::new(0));
294 let seen = count.clone();
295 event.add(Rc::new(move |_| seen.set(seen.get() + 1)));
296 count
297 }
298
299 #[test]
300 fn the_creating_thread_is_the_loop_thread() {
301 let (impl_, _) = create();
302
303 assert!(impl_.current_thread_is_loop_thread());
304 }
305
306 #[test]
307 fn signals_are_coalesced_until_the_posted_task_runs() {
308 let (impl_, calls) = create();
309 let signaled = counter(impl_.signaled());
310
311 IDispatcherImpl::signal(&*impl_);
312 IDispatcherImpl::signal(&*impl_);
313 assert_eq!(vec!["signal"], *calls.borrow());
314 assert_eq!(0, signaled.get());
315
316 on_signaled();
317 assert_eq!(1, signaled.get());
318
319 IDispatcherImpl::signal(&*impl_);
320 assert_eq!(vec!["signal", "signal"], *calls.borrow());
321 }
322
323 #[test]
324 fn the_signal_handle_wakes_the_instance_of_the_thread() {
325 let (impl_, calls) = create();
326
327 impl_.signal_handle().signal();
328 impl_.signal_handle().signal();
329
330 assert_eq!(vec!["signal"], *calls.borrow());
331 }
332
333 #[test]
334 fn a_signal_from_another_thread_is_carried_to_the_loop_thread_once() {
335 use crate::interop::thread_proxy::ThreadWork;
336 use std::sync::Mutex;
337
338 static PENDING: AtomicBool = AtomicBool::new(false);
339 static QUEUED: Mutex<Vec<(usize, ThreadWork)>> = Mutex::new(Vec::new());
340 fn queue(thread: usize, work: ThreadWork) -> bool {
341 QUEUED.lock().unwrap().push((thread, work));
342 true
343 }
344 fn refuse(_thread: usize, _work: ThreadWork) -> bool {
345 false
346 }
347 fn take() -> Vec<(usize, ThreadWork)> {
348 QUEUED.lock().unwrap().drain(..).collect()
349 }
350
351 let (_impl, calls) = create();
352
353 std::thread::spawn(|| {
355 assert!(instance().is_none());
356 signal_from_another_thread(&PENDING, 7, queue);
357 signal_from_another_thread(&PENDING, 7, queue);
358 })
359 .join()
360 .unwrap();
361 let queued = take();
362 assert_eq!(vec![7], queued.iter().map(|(thread, _)| *thread).collect::<Vec<usize>>());
363 assert!(calls.borrow().is_empty());
364
365 for (_, work) in queued {
367 work();
368 }
369 assert_eq!(vec!["signal"], *calls.borrow());
370 assert!(!PENDING.load(Ordering::SeqCst));
371
372 signal_from_another_thread(&PENDING, 7, refuse);
374 assert!(!PENDING.load(Ordering::SeqCst));
375
376 signal_from_another_thread(&PENDING, 0, queue);
378 assert!(take().is_empty());
379 }
380
381 #[test]
382 fn the_signal_handle_of_a_thread_without_a_dispatcher_is_lost_in_a_build_without_threads() {
383 let (impl_, calls) = create();
384 let handle = impl_.signal_handle();
385
386 std::thread::spawn(move || handle.signal()).join().unwrap();
387
388 assert!(calls.borrow().is_empty());
389 assert!(!CROSS_THREAD_SIGNAL_PENDING.load(Ordering::SeqCst));
390 }
391
392 #[test]
393 fn background_requests_are_coalesced_until_the_posted_task_runs() {
394 let (impl_, calls) = create();
395 let ready = counter(impl_.ready_for_background_processing());
396
397 impl_.request_background_processing();
398 impl_.request_background_processing();
399 assert_eq!(vec!["background"], *calls.borrow());
400
401 on_ready_for_background_processing();
402 assert_eq!(1, ready.get());
403
404 impl_.request_background_processing();
405 assert_eq!(vec!["background", "background"], *calls.borrow());
406 }
407
408 #[test]
409 fn the_timer_is_set_relative_to_now_and_replaced_by_the_next_update() {
410 let (impl_, calls) = create();
411
412 impl_.update_timer(Some(impl_.now() + 5_500));
413 impl_.update_timer(Some(impl_.now() + 2_500));
414 impl_.update_timer(None);
415 impl_.update_timer(None);
416
417 assert_eq!(vec!["setTimer(5)", "clearTimer", "setTimer(2)", "clearTimer"], *calls.borrow());
418 }
419
420 #[test]
421 fn a_due_time_in_the_past_fires_immediately() {
422 let (impl_, calls) = create();
423
424 impl_.update_timer(Some(-1_000));
425
426 assert_eq!(vec!["setTimer(0)"], *calls.borrow());
427 }
428
429 #[test]
430 fn a_fired_timer_is_not_cleared_by_the_next_update() {
431 let (impl_, calls) = create();
432 let timer = counter(impl_.timer());
433
434 impl_.update_timer(Some(impl_.now() + 1_500));
435 on_timer();
436 assert_eq!(1, timer.get());
437 impl_.update_timer(Some(impl_.now() + 3_500));
438
439 assert_eq!(vec!["setTimer(1)", "setTimer(3)"], *calls.borrow());
440 }
441
442 #[test]
443 fn explicit_background_processing_is_supported_and_a_loop_is_not() {
444 let (impl_, _) = create();
445
446 assert!(impl_.as_explicit_background_processing().is_some());
447 assert!(impl_.as_pending_input().is_none());
448 assert!(impl_.as_controlled().is_none());
449 }
450}