use std::{
fmt,
time::{Duration, Instant},
};
pub const PREVIEW_PROGRESS_STALL: Duration = Duration::from_secs(60);
pub const PREVIEW_REMOTE_MAX_WALL: Duration = Duration::from_secs(300);
pub const PREVIEW_CANCEL_ACK_WAIT: Duration = Duration::from_secs(10);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RemoteShortfall {
Failed(String),
Unsupported(String),
Cancelled,
Stalled {
samples_done: u32,
stalled_for: Duration,
},
WallClock {
samples_done: u32,
elapsed: Duration,
},
ShortSamples {
done: u32,
wanted: u32,
},
Disconnected,
}
impl fmt::Display for RemoteShortfall {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Failed(message) => write!(f, "remote render failed: {message}"),
Self::Unsupported(message) => {
write!(f, "remote does not support this request: {message}")
}
Self::Cancelled => write!(f, "remote render was cancelled"),
Self::Stalled {
samples_done,
stalled_for,
} => write!(
f,
"remote render stalled at {samples_done} sample(s) for {stalled_for:.0?}"
),
Self::WallClock {
samples_done,
elapsed,
} => write!(
f,
"remote render exceeded its time limit ({elapsed:.0?}, {samples_done} sample(s) done)"
),
Self::ShortSamples { done, wanted } => {
write!(f, "remote render returned {done} of {wanted} sample(s)")
}
Self::Disconnected => write!(f, "remote render thread ended without a result"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ProgressWatch {
started: Instant,
last_advance: Instant,
samples_done: u32,
}
impl ProgressWatch {
#[must_use]
pub const fn new(now: Instant) -> Self {
Self {
started: now,
last_advance: now,
samples_done: 0,
}
}
pub const fn observe(&mut self, samples_done: u32, now: Instant) {
if samples_done > self.samples_done {
self.samples_done = samples_done;
self.last_advance = now;
}
}
#[must_use]
pub fn verdict(
&self,
now: Instant,
stall: Duration,
wall: Duration,
) -> Option<RemoteShortfall> {
let elapsed = now.saturating_duration_since(self.started);
if elapsed > wall {
return Some(RemoteShortfall::WallClock {
samples_done: self.samples_done,
elapsed,
});
}
let stalled_for = now.saturating_duration_since(self.last_advance);
(stalled_for > stall).then_some(RemoteShortfall::Stalled {
samples_done: self.samples_done,
stalled_for,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
const STALL: Duration = Duration::from_secs(60);
const WALL: Duration = Duration::from_secs(300);
fn secs(n: u64) -> Duration {
Duration::from_secs(n)
}
#[test]
fn a_larger_samples_done_resets_the_stall_clock() {
let t0 = Instant::now();
let mut watch = ProgressWatch::new(t0);
watch.observe(10, t0 + secs(50));
assert_eq!(watch.verdict(t0 + secs(100), STALL, WALL), None);
}
#[test]
fn an_equal_report_does_not_reset_the_stall_clock() {
let t0 = Instant::now();
let mut watch = ProgressWatch::new(t0);
watch.observe(10, t0 + secs(5));
watch.observe(10, t0 + secs(60));
watch.observe(3, t0 + secs(64));
assert_eq!(
watch.verdict(t0 + secs(66), STALL, WALL),
Some(RemoteShortfall::Stalled {
samples_done: 10,
stalled_for: secs(61),
})
);
}
#[test]
fn no_advance_past_the_stall_window_is_stalled() {
let t0 = Instant::now();
let watch = ProgressWatch::new(t0);
assert_eq!(
watch.verdict(t0 + secs(61), STALL, WALL),
Some(RemoteShortfall::Stalled {
samples_done: 0,
stalled_for: secs(61),
})
);
}
#[test]
fn total_time_past_the_wall_cap_is_wall_clock_even_with_steady_progress() {
let t0 = Instant::now();
let mut watch = ProgressWatch::new(t0);
for step in 1..=31_u32 {
watch.observe(step, t0 + secs(u64::from(step) * 10));
}
assert_eq!(
watch.verdict(t0 + secs(310), STALL, WALL),
Some(RemoteShortfall::WallClock {
samples_done: 31,
elapsed: secs(310),
})
);
}
#[test]
fn verdict_is_none_just_under_either_bound() {
let t0 = Instant::now();
let mut watch = ProgressWatch::new(t0);
assert_eq!(watch.verdict(t0 + secs(60), STALL, WALL), None);
watch.observe(1, t0 + secs(250));
assert_eq!(watch.verdict(t0 + secs(300), STALL, WALL), None);
assert!(watch.verdict(t0 + secs(301), STALL, WALL).is_some());
}
#[test]
fn display_gives_one_human_line_per_variant() {
let all = [
RemoteShortfall::Failed("boom".into()),
RemoteShortfall::Unsupported("nope".into()),
RemoteShortfall::Cancelled,
RemoteShortfall::Stalled {
samples_done: 4,
stalled_for: secs(61),
},
RemoteShortfall::WallClock {
samples_done: 4,
elapsed: secs(301),
},
RemoteShortfall::ShortSamples { done: 1, wanted: 2 },
RemoteShortfall::Disconnected,
];
for shortfall in &all {
let line = shortfall.to_string();
assert!(!line.is_empty() && !line.contains('\n'), "{line:?}");
}
}
}