use std::sync::Mutex;
use std::time::{Duration, Instant};
use sceptre::ProgressSink;
const STAGE_DETECT: &str = "detect";
const STAGE_RECOGNIZE: &str = "recognize";
#[derive(Default)]
pub struct StageTimer {
events: Mutex<Vec<(String, Instant)>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Breakdown {
pub setup: Duration,
pub detect: Duration,
pub recognize: Duration,
pub total: Duration,
}
impl StageTimer {
pub fn new() -> Self {
Self::default()
}
pub fn breakdown(&self, start: Instant, end: Instant) -> Breakdown {
let events = self.events.lock().expect("stage-timer mutex is not poisoned");
let relative: Vec<(String, Duration)> = events
.iter()
.map(|(stage, at)| (stage.clone(), at.saturating_duration_since(start)))
.collect();
compute_breakdown(&relative, end.saturating_duration_since(start))
}
}
impl ProgressSink for StageTimer {
fn on_stage(&self, stage: &str) {
self.events
.lock()
.expect("stage-timer mutex is not poisoned")
.push((stage.to_string(), Instant::now()));
}
}
fn compute_breakdown(events: &[(String, Duration)], total: Duration) -> Breakdown {
let setup = events.first().map(|(_, at)| *at).unwrap_or(total);
let mut detect = Duration::ZERO;
let mut recognize = Duration::ZERO;
for (index, (stage, at)) in events.iter().enumerate() {
let next = events.get(index + 1).map(|(_, at)| *at).unwrap_or(total);
let span = next.saturating_sub(*at);
match stage.as_str() {
STAGE_DETECT => detect += span,
STAGE_RECOGNIZE => recognize += span,
_ => {}
}
}
Breakdown {
setup,
detect,
recognize,
total,
}
}
pub fn render(breakdown: &Breakdown) -> String {
let ms = |d: Duration| d.as_secs_f64() * 1000.0;
let pct = |d: Duration| {
if breakdown.total.is_zero() {
0.0
} else {
d.as_secs_f64() / breakdown.total.as_secs_f64() * 100.0
}
};
format!(
"timings (ms): total {:.1} | load+decode {:.1} ({:.0}%) | detect {:.1} ({:.0}%) | recognize {:.1} ({:.0}%)",
ms(breakdown.total),
ms(breakdown.setup),
pct(breakdown.setup),
ms(breakdown.detect),
pct(breakdown.detect),
ms(breakdown.recognize),
pct(breakdown.recognize),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn ms(value: u64) -> Duration {
Duration::from_millis(value)
}
#[test]
fn single_image_splits_setup_detect_recognize() {
let events = vec![
(STAGE_DETECT.to_string(), ms(100)),
(STAGE_RECOGNIZE.to_string(), ms(250)),
];
let breakdown = compute_breakdown(&events, ms(400));
assert_eq!(breakdown.setup, ms(100));
assert_eq!(breakdown.detect, ms(150));
assert_eq!(breakdown.recognize, ms(150));
assert_eq!(breakdown.total, ms(400));
}
#[test]
fn batch_sums_each_stage_across_images() {
let events = vec![
(STAGE_DETECT.to_string(), ms(50)),
(STAGE_RECOGNIZE.to_string(), ms(120)),
(STAGE_DETECT.to_string(), ms(200)),
(STAGE_RECOGNIZE.to_string(), ms(260)),
];
let breakdown = compute_breakdown(&events, ms(300));
assert_eq!(breakdown.setup, ms(50));
assert_eq!(breakdown.detect, ms(130));
assert_eq!(breakdown.recognize, ms(120));
}
#[test]
fn no_events_attributes_everything_to_setup() {
let breakdown = compute_breakdown(&[], ms(200));
assert_eq!(breakdown.setup, ms(200));
assert_eq!(breakdown.detect, Duration::ZERO);
assert_eq!(breakdown.recognize, Duration::ZERO);
}
#[test]
fn render_reports_total_and_percentages() {
let breakdown = Breakdown {
setup: ms(100),
detect: ms(150),
recognize: ms(150),
total: ms(400),
};
let line = render(&breakdown);
assert!(line.contains("total 400.0"), "{line}");
assert!(line.contains("load+decode 100.0"), "{line}");
assert!(line.contains("detect 150.0"), "{line}");
assert!(line.contains("recognize 150.0"), "{line}");
}
}