diff --git a/src/obisys/src/progress.rs b/src/obisys/src/progress.rs index 3985c345..f15547d1 100644 --- a/src/obisys/src/progress.rs +++ b/src/obisys/src/progress.rs @@ -5,18 +5,20 @@ use indicatif::{ProgressBar, ProgressStyle}; use tracing::{debug, info}; const BRAILLE: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; +const ETA_REFRESH_MS: u64 = 500; +const ETA_MIN_ELAPSED_MS: u64 = 1000; +const ETA_CUSTOM_HOLD_MS: u64 = 2000; -/// Wrapper around `ProgressBar` that emits `tracing` events when stderr is not -/// a TTY (e.g. HPC job logs): every 10% for bounded bars, every ~10 s for -/// spinners (throttled on `set_message`). pub struct TracedBar { pb: ProgressBar, label: String, unit: String, - total: u64, // 0 for spinners - start: Instant, // creation time, for spinner throttling - last_pct: AtomicU64, // last emitted 10%-bucket (1..=10), 0 = none yet - last_log_ms: AtomicU64, // ms since `start` at last spinner log + total: u64, + start: Instant, + last_pct: AtomicU64, + last_log_ms: AtomicU64, + last_eta_refresh: AtomicU64, + last_custom_msg_ms: AtomicU64, } impl TracedBar { @@ -40,15 +42,60 @@ impl TracedBar { ); } } + + if !self.pb.is_hidden() && self.total > 0 { + self.maybe_update_eta(); + } + } + + fn maybe_update_eta(&self) { + let now_ms = self.start.elapsed().as_millis() as u64; + + if now_ms < ETA_MIN_ELAPSED_MS { + return; + } + + let last = self.last_eta_refresh.load(Ordering::Relaxed); + if now_ms < last + ETA_REFRESH_MS { + return; + } + + let last_custom = self.last_custom_msg_ms.load(Ordering::Relaxed); + if now_ms < last_custom + ETA_CUSTOM_HOLD_MS { + return; + } + + let pos = self.pb.position(); + let remaining = self.total - pos; + + if pos == 0 || remaining == 0 { + return; + } + + let elapsed_secs = self.start.elapsed().as_secs_f64(); + let avg_rate = pos as f64 / elapsed_secs; + let eta_secs = remaining as f64 / avg_rate; + + self.pb.set_message(format!("(eta: {})", fmt_secs(eta_secs))); + + let _ = self.last_eta_refresh.compare_exchange( + last, + now_ms, + Ordering::Relaxed, + Ordering::Relaxed, + ); } pub fn set_message(&self, msg: impl Into) { let msg = msg.into(); + self.last_custom_msg_ms.store( + self.start.elapsed().as_millis() as u64, + Ordering::Relaxed, + ); if self.pb.is_hidden() { if self.total > 0 { debug!(stage = %self.label, "{msg}"); } else { - // spinner: throttle to ~10 s let now_ms = self.start.elapsed().as_millis() as u64; let last = self.last_log_ms.load(Ordering::Relaxed); if now_ms >= last + 10_000 @@ -69,6 +116,18 @@ impl TracedBar { } } +fn fmt_secs(s: f64) -> String { + if s >= 3600.0 { + format!("{:.0}h", s / 3600.0) + } else if s >= 60.0 { + format!("{:.0}min", s / 60.0) + } else if s >= 1.0 { + format!("{:.1}s", s) + } else { + format!("{:.0}ms", s * 1000.0) + } +} + /// Spinner with the standard project look: `⠋ label — msg 0s`. /// Caller updates the message with `pb.set_message(...)`. pub fn spinner(label: &str) -> TracedBar { @@ -87,6 +146,8 @@ pub fn spinner(label: &str) -> TracedBar { start: Instant::now(), last_pct: AtomicU64::new(0), last_log_ms: AtomicU64::new(0), + last_eta_refresh: AtomicU64::new(0), + last_custom_msg_ms: AtomicU64::new(0), } } @@ -96,7 +157,7 @@ pub fn progress_bar(label: &str, n: u64, unit: &str) -> TracedBar { let pb = ProgressBar::new(n); pb.set_style( ProgressStyle::with_template(&format!( - "{{spinner}} {label} — {{bar:40.cyan/blue}} {{pos}}/{{len}} {unit} {{elapsed}} (eta: {{eta}})" + "{{spinner}} {label} — {{bar:40.cyan/blue}} {{pos}}/{{len}} {unit} {{elapsed}} {{msg}}" )) .unwrap() .tick_strings(BRAILLE), @@ -110,5 +171,7 @@ pub fn progress_bar(label: &str, n: u64, unit: &str) -> TracedBar { start: Instant::now(), last_pct: AtomicU64::new(0), last_log_ms: AtomicU64::new(0), + last_eta_refresh: AtomicU64::new(0), + last_custom_msg_ms: AtomicU64::new(0), } }