Skip to main content

qcode/ui/
stalling.rs

1//! What a screen says when a profile's image build has said nothing for long enough to look hung.
2//!
3//! A build has no time limit of its own: a big image takes twenty minutes, and that is normal.
4//! What the person cannot read off a log that has stopped moving is whether to keep waiting, so
5//! both screens that show a build — the workspace tab that is building a missing image and the
6//! wizard's image page — judge the same silence the same way and say the same one line beside it.
7//!
8//! Nothing here stops a build, and nothing asks the engine whether it is stuck: the engine does not
9//! say, and a build QCode gave up on is a half-made image and a person who wanted their profile.
10//! The person's own Stop is the only way a build ends, and this rule only tells them it is time to
11//! consider it.
12//!
13//! The silence is judged by [`Lengths`], which a screen holds and a test shortens: the product
14//! uses the measured [`QUIET`] and [`LOOK`], and the number in the sentence is the first of them
15//! in whole minutes rather than a measured silence, which a person cannot be shown.
16
17use std::time::{Duration, Instant};
18
19use qframe::prelude::*;
20use qframe::runtime::Task;
21
22/// How long a build may say nothing before it is said to be quiet.
23///
24/// Five minutes is well inside the longest silence of a build that is going well — a big layer
25/// being pulled, a package being compiled — and well inside how long a person waits before
26/// wondering whether to stop it.
27pub const QUIET: Duration = Duration::from_secs(5 * 60);
28
29/// How often a build that is running is looked at.
30///
31/// Short enough that the warning is up within a look of the silence being long enough, and long
32/// enough that a build of twenty minutes is asked about some eighty times rather than once per
33/// line it prints.
34pub const LOOK: Duration = Duration::from_secs(15);
35
36/// The two lengths a build's silence is judged by. The product's are [`QUIET`] and [`LOOK`]; a
37/// test shortens both, since a test cannot wait five minutes for a warning and cannot ask the
38/// engine for a build that really takes that long.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct Lengths {
41    /// How long a build may say nothing before it is said to be stuck.
42    pub quiet: Duration,
43    /// How often a build that is running is looked at.
44    pub look: Duration,
45}
46
47impl Default for Lengths {
48    fn default() -> Self {
49        Self { quiet: QUIET, look: LOOK }
50    }
51}
52
53impl Lengths {
54    /// The whole minutes of [`Self::quiet`], which is what the sentence counts: the rule is one
55    /// length for every person, and what is said is that length rather than a silence nobody
56    /// measured to the second.
57    #[must_use]
58    pub fn minutes(&self) -> i64 {
59        i64::try_from(self.quiet.as_secs() / 60).unwrap_or(i64::MAX)
60    }
61}
62
63/// Whether a build that last said something at `said` has been quiet for longer than `quiet` as
64/// of `now`, which is what the warning beside it is raised and taken down by.
65#[must_use]
66pub fn quiet_for(said: Instant, now: Instant, quiet: Duration) -> bool {
67    now.saturating_duration_since(said) >= quiet
68}
69
70/// The next look at the builds running on a screen, timed once: a task that sleeps
71/// [`Lengths::look`] and hands `woke` the moment it woke at, so what is judged is the screen as it
72/// is then and not as it was when the look was asked for.
73///
74/// The caller decides whether a look is wanted and builds the message out of the moment, since
75/// what a look says is the message each screen speaks.
76pub fn follow<M: Send + 'static>(lengths: Lengths, woke: impl Fn(Instant) -> M + Send + 'static) -> Command<M> {
77    let look = lengths.look;
78    Command::task(Task::new(t!("build.looking"), move |cx| {
79        if cx.sleep(look) { Ok(woke(Instant::now())) } else { Err(String::new()) }
80    }))
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    /// The lengths a test judges by: a silence a moment of real time reaches and a look the
88    /// harness's clock reaches at once.
89    const BRIEF: Lengths = Lengths { quiet: Duration::from_millis(50), look: Duration::from_millis(10) };
90
91    /// The two moments one look is taken between: a build whose last line was `ago` before the
92    /// look, and the look itself.
93    fn quiet_since(ago: Duration) -> (Instant, Instant) {
94        let last = Instant::now();
95        (last, last + ago)
96    }
97
98    #[test]
99    fn a_build_is_quiet_when_nothing_has_been_written_for_as_long_as_the_rule_asks() {
100        let (last, now) = quiet_since(BRIEF.quiet - Duration::from_millis(1));
101        assert!(!quiet_for(last, now, BRIEF.quiet), "the silence has not reached the rule yet");
102        let (last, now) = quiet_since(BRIEF.quiet);
103        assert!(quiet_for(last, now, BRIEF.quiet), "and reaching it is what raises the warning");
104        // A line arriving starts the silence again, so the same rule says nothing afterwards.
105        let (last, _) = quiet_since(BRIEF.quiet);
106        assert!(!quiet_for(last, last + BRIEF.quiet / 2, BRIEF.quiet));
107    }
108
109    #[test]
110    fn the_number_in_the_sentence_is_the_rules_own_length_in_whole_minutes() {
111        assert_eq!(Lengths::default().minutes(), 5, "the product's five minutes, said as five");
112        assert_eq!(BRIEF.minutes(), 0, "a test's rule of no minutes at all says zero rather than a fraction");
113    }
114}