ifc_lite_geometry/progress.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Cooperative progress heartbeat from inside long geometry work.
6//!
7//! A browser geometry worker runs one synchronous WASM call per batch, and the
8//! host can only see that the call is still alive through messages the worker
9//! posts. Before this hook a single slow element (a wall with hundreds of
10//! curved openings: tens of seconds) was indistinguishable from a genuinely
11//! wedged call, so the host's wall-clock recovery replaced the worker after
12//! 45 s and skipped the element after another 90 s. Whether an element
13//! survived therefore depended on the user's CPU speed and load, which made the
14//! same file produce different geometry on different machines.
15//!
16//! [`tick`] is called at coarse points that every long-running geometry path
17//! passes through repeatedly (each boolean, each analytic prism cut, each
18//! coplanar consolidation bucket, a strided count of exact-predicate
19//! escalations, each element). With no hook installed, the default on every
20//! native target, it is a single atomic load. The wasm bindings install a hook
21//! that rate-limits by the JS clock and lets the worker post a heartbeat, so a
22//! call that is still working is never mistaken for a hung one. The hook has no
23//! influence on WHAT is computed: geometry output is identical with or without
24//! it.
25
26use std::sync::OnceLock;
27
28static HOOK: OnceLock<fn()> = OnceLock::new();
29
30/// Install the process-wide progress hook. First installation wins; a later
31/// call is ignored and returns `false`. The hook must be cheap and must not
32/// re-enter geometry code.
33pub fn set_hook(hook: fn()) -> bool {
34 HOOK.set(hook).is_ok()
35}
36
37/// Report that geometry work is progressing. A no-op until a hook is installed.
38#[inline]
39pub fn tick() {
40 if let Some(hook) = HOOK.get() {
41 hook();
42 }
43}
44
45/// [`tick`] on every `(stride_mask + 1)`-th call, for a hot loop whose single
46/// iteration is too cheap to report on its own. `stride_mask` is a power of two
47/// minus one; `counter` is the loop's own call count.
48#[inline]
49pub fn tick_strided(counter: &mut u32, stride_mask: u32) {
50 *counter = counter.wrapping_add(1);
51 if *counter & stride_mask == 0 {
52 tick();
53 }
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59 use std::cell::Cell;
60
61 thread_local! {
62 // Per thread: the hook is process-wide, and other tests in this binary
63 // tick it concurrently from their own threads (every boolean, CDT and
64 // consolidation calls `tick`). Counting only this thread's calls keeps
65 // the exact-delta assertion exact.
66 static TICKS: Cell<u64> = const { Cell::new(0) };
67 }
68 fn count() {
69 TICKS.with(|t| t.set(t.get() + 1));
70 }
71
72 #[test]
73 fn hook_receives_ticks_and_the_first_installation_wins() {
74 // Either test in this module may install `count` first; the tick
75 // delta below proves it is the installed hook.
76 let _ = set_hook(count);
77 assert!(!set_hook(|| panic!("a second hook must not replace the first")));
78 let before = TICKS.with(Cell::get);
79 tick();
80 tick();
81 assert_eq!(TICKS.with(Cell::get) - before, 2);
82 }
83
84 #[test]
85 fn tick_strided_reports_once_per_stride() {
86 let _ = set_hook(count);
87 let before = TICKS.with(Cell::get);
88 let mut calls = 0u32;
89 for _ in 0..64 {
90 tick_strided(&mut calls, 0xF);
91 }
92 assert_eq!(TICKS.with(Cell::get) - before, 4);
93 }
94}