Skip to main content

geop_core_program/
watch.rs

1//! What a long computation has done, for whoever waits for it, and whether
2//! they still want it.
3//!
4//! A front end that can wait for a kernel on a thread of its own (`geop
5//! mcp`) sets a watcher around what it asks ([`watching`]). The loops that
6//! take long tell it how far they are at each turn: a loop that can stop
7//! part way and say so asks [`go_on`]; one whose answer is nothing unless
8//! whole, [`checkpoint`], which fails; one that must finish, a build,
9//! [`report`]. A watcher decides how much of a question is asked, never
10//! what the answer means: a report stopped part way says how far it got.
11//! Between two turns nothing is looked at, so a boolean running stops once
12//! it is done.
13
14use std::cell::RefCell;
15
16use geop_core_math::geop_error::{GeopError, GeopResult};
17
18/// How far a computation is.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub struct Progress<'a> {
21    /// What it is doing.
22    pub what: &'a str,
23    pub done: usize,
24    pub total: usize,
25}
26
27type Watcher = Box<dyn FnMut(&Progress) -> bool>;
28
29thread_local! {
30    static WATCHER: RefCell<Option<Watcher>> = const { RefCell::new(None) };
31}
32
33/// Runs `f` with `watcher` told how far it is, and asked whether to go on.
34pub fn watching<T>(watcher: impl FnMut(&Progress) -> bool + 'static, f: impl FnOnce() -> T) -> T {
35    /// Takes the watcher away again, even from a kernel that panicked.
36    struct Unset(Option<Watcher>);
37    impl Drop for Unset {
38        fn drop(&mut self) {
39            WATCHER.with(|w| *w.borrow_mut() = self.0.take());
40        }
41    }
42    let _unset = Unset(WATCHER.with(|w| w.borrow_mut().replace(Box::new(watcher))));
43    f()
44}
45
46/// Tells the watcher how far a loop is: whether it should go on, as it
47/// should with none.
48pub fn go_on(what: &str, done: usize, total: usize) -> bool {
49    let progress = Progress { what, done, total };
50    WATCHER.with(|w| (w.borrow_mut().as_mut()).is_none_or(|watcher| watcher(&progress)))
51}
52
53/// Tells the watcher how far a computation that must finish is.
54pub fn report(what: &str, done: usize, total: usize) {
55    go_on(what, done, total);
56}
57
58/// [`go_on`] for an answer that is nothing unless whole: fails, saying how
59/// far it got, if it should not go on.
60pub fn checkpoint(what: &str, done: usize, total: usize) -> GeopResult<()> {
61    match go_on(what, done, total) {
62        true => Ok(()),
63        false => Err(GeopError::new(format!(
64            "stopped at {what}, {done} of {total} done: cancelled, or past its time limit"
65        ))),
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use std::{cell::Cell, rc::Rc};
72
73    use super::*;
74
75    #[test]
76    fn with_no_watcher_a_loop_goes_on() {
77        assert!(go_on("anything", 0, 1));
78        assert!(checkpoint("anything", 0, 1).is_ok());
79    }
80
81    #[test]
82    fn a_watcher_hears_how_far_it_is_and_stops_it() {
83        let heard = Rc::new(Cell::new(0));
84        let counted = heard.clone();
85        let done = watching(
86            move |progress: &Progress| {
87                counted.set(counted.get() + 1);
88                progress.done < 2
89            },
90            || (0..5).take_while(|&done| go_on("pairs", done, 5)).count(),
91        );
92        assert_eq!((done, heard.get()), (2, 3));
93        let stopped = watching(|_: &Progress| false, || checkpoint("solids", 3, 7));
94        let said = stopped.unwrap_err().to_string();
95        assert!(said.contains("solids, 3 of 7"), "{said}");
96        assert!(go_on("after it", 0, 1), "the watcher is gone afterwards");
97    }
98
99    #[test]
100    fn a_panic_takes_the_watcher_away() {
101        let panicked = std::panic::catch_unwind(|| {
102            watching(|_: &Progress| false, || panic!("the kernel crashed"))
103        });
104        assert!(panicked.is_err());
105        assert!(go_on("after it", 0, 1));
106    }
107}