use super::{LocalizationKey, StageNumber, StatusReporter};
use crate::localization::{self, keys};
use crate::output_prefs::OutputPrefs;
use std::io::{self, Write};
use std::sync::Mutex;
use std::time::{Duration, Instant};
type MonotonicClock = dyn Fn() -> Duration + Send + Sync;
#[derive(Debug, Copy, Clone)]
struct StageMarker {
current: StageNumber,
total: StageNumber,
}
#[derive(Debug, Clone)]
struct RunningStage {
marker: StageMarker,
description: String,
started_at: Duration,
}
#[derive(Debug, Clone)]
struct CompletedStage {
marker: StageMarker,
description: String,
elapsed: Duration,
}
#[derive(Debug, Default)]
struct TimingState {
completed: bool,
completed_stages: Vec<CompletedStage>,
running: Option<RunningStage>,
}
impl TimingState {
fn start_stage(&mut self, now: Duration, marker: StageMarker, description: &str) {
self.finish_running(now);
self.running = Some(RunningStage {
marker,
description: description.to_owned(),
started_at: now,
});
}
fn finish(&mut self, now: Duration) {
self.finish_running(now);
}
fn completed_stages(&self) -> &[CompletedStage] {
&self.completed_stages
}
fn finish_running(&mut self, now: Duration) {
let Some(running) = self.running.take() else {
return;
};
let elapsed = now.saturating_sub(running.started_at);
self.completed_stages.push(CompletedStage {
marker: running.marker,
description: running.description,
elapsed,
});
}
}
pub struct VerboseTimingReporter {
inner: Box<dyn StatusReporter>,
prefs: OutputPrefs,
clock: Box<MonotonicClock>,
state: Mutex<TimingState>,
}
impl VerboseTimingReporter {
#[must_use]
pub fn new(inner: Box<dyn StatusReporter>, prefs: OutputPrefs) -> Self {
let start = Instant::now();
Self::with_clock(inner, prefs, Box::new(move || start.elapsed()))
}
fn with_clock(
inner: Box<dyn StatusReporter>,
prefs: OutputPrefs,
clock: Box<MonotonicClock>,
) -> Self {
Self {
inner,
prefs,
clock,
state: Mutex::new(TimingState::default()),
}
}
}
impl StatusReporter for VerboseTimingReporter {
fn report_stage(&self, current: StageNumber, total: StageNumber, description: &str) {
let should_forward = {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if state.completed {
false
} else {
state.start_stage((self.clock)(), StageMarker { current, total }, description);
true
}
};
if should_forward {
self.inner.report_stage(current, total, description);
}
}
fn report_task_progress(&self, current: u32, total: u32, description: &str) {
let should_forward = {
let state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
!state.completed
};
if should_forward {
self.inner.report_task_progress(current, total, description);
}
}
fn report_complete(&self, tool_key: LocalizationKey) {
let lines = {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if state.completed {
Vec::new()
} else {
state.completed = true;
state.finish((self.clock)());
render_summary_lines(self.prefs, state.completed_stages())
}
};
self.inner.report_complete(tool_key);
for line in lines {
drop(writeln!(io::stderr(), "{line}"));
}
}
}
fn render_summary_lines(prefs: OutputPrefs, entries: &[CompletedStage]) -> Vec<String> {
if entries.is_empty() {
return Vec::new();
}
let mut lines = Vec::with_capacity(entries.len() + 2);
let prefix = prefs.timing_prefix();
let header = localization::message(keys::STATUS_TIMING_SUMMARY_HEADER);
lines.push(format!("{prefix} {header}"));
for entry in entries {
let label = localization::message(keys::STATUS_STAGE_LABEL)
.with_arg("current", entry.marker.current.get().to_string())
.with_arg("total", entry.marker.total.get().to_string())
.with_arg("description", &entry.description)
.to_string();
let line = localization::message(keys::STATUS_TIMING_STAGE_LINE)
.with_arg("label", &label)
.with_arg("duration", format_duration(entry.elapsed))
.to_string();
lines.push(format!("{}{line}", prefs.timing_indent()));
}
let total = entries.iter().fold(Duration::ZERO, |acc, entry| {
acc.saturating_add(entry.elapsed)
});
let total_line = localization::message(keys::STATUS_TIMING_TOTAL_LINE)
.with_arg("duration", format_duration(total))
.to_string();
lines.push(format!("{}{total_line}", prefs.timing_indent()));
lines
}
fn format_duration(duration: Duration) -> String {
let seconds = duration.as_secs();
if seconds > 0 {
let milliseconds = duration.subsec_millis();
if milliseconds == 0 {
return format!("{seconds}s");
}
return format!("{seconds}.{milliseconds:03}s");
}
if duration.as_millis() > 0 {
return format!("{}ms", duration.as_millis());
}
if duration.as_micros() > 0 {
return format!("{}us", duration.as_micros());
}
format!("{}ns", duration.as_nanos())
}
#[path = "status_timing_tests.rs"]
#[cfg(test)]
mod tests;