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}