use std::{
sync::atomic::{AtomicU8, Ordering},
time::{Duration, Instant},
};
const DEFAULT_REPORT_INTERVAL: Duration = Duration::from_secs(5);
const MIN_ITEMS_FOR_PROGRESS: usize = 1000;
const STATE_UNINITIALIZED: u8 = 0;
const STATE_DISABLED: u8 = 1;
const STATE_ENABLED: u8 = 2;
static PROGRESS_STATE: AtomicU8 = AtomicU8::new(STATE_UNINITIALIZED);
pub fn is_enabled() -> bool {
let state = PROGRESS_STATE.load(Ordering::Relaxed);
if state == STATE_UNINITIALIZED {
let enabled = std::env::var("DDL_PROGRESS")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false);
let new_state = if enabled { STATE_ENABLED } else { STATE_DISABLED };
let _ = PROGRESS_STATE.compare_exchange(
STATE_UNINITIALIZED,
new_state,
Ordering::Relaxed,
Ordering::Relaxed,
);
enabled
} else {
state == STATE_ENABLED
}
}
pub fn enable() {
PROGRESS_STATE.store(STATE_ENABLED, Ordering::Relaxed);
}
pub fn disable() {
PROGRESS_STATE.store(STATE_DISABLED, Ordering::Relaxed);
}
pub struct ProgressTracker {
operation: String,
total: Option<usize>,
current: usize,
start_time: Instant,
last_report_time: Instant,
report_interval: Duration,
has_reported: bool,
}
impl ProgressTracker {
pub fn new(operation: impl Into<String>, total: Option<usize>) -> Self {
let now = Instant::now();
Self {
operation: operation.into(),
total,
current: 0,
start_time: now,
last_report_time: now,
report_interval: DEFAULT_REPORT_INTERVAL,
has_reported: false,
}
}
pub fn with_interval(mut self, interval: Duration) -> Self {
self.report_interval = interval;
self
}
pub fn update(&mut self, current: usize) {
self.current = current;
if !is_enabled() {
return;
}
if let Some(total) = self.total {
if total < MIN_ITEMS_FOR_PROGRESS {
return;
}
}
let now = Instant::now();
if now.duration_since(self.last_report_time) >= self.report_interval {
self.report_progress();
self.last_report_time = now;
}
}
pub fn report(&mut self) {
if !is_enabled() {
return;
}
self.report_progress();
self.last_report_time = Instant::now();
}
pub fn finish(&mut self) {
if !is_enabled() {
return;
}
if let Some(total) = self.total {
if total < MIN_ITEMS_FOR_PROGRESS && !self.has_reported {
return;
}
}
let elapsed = self.start_time.elapsed();
let elapsed_str = format_duration(elapsed);
if let Some(total) = self.total {
let rate = if elapsed.as_secs_f64() > 0.0 {
self.current as f64 / elapsed.as_secs_f64()
} else {
0.0
};
log::info!(
"{}: completed {} / {} items (100%) [{} elapsed, {:.0} items/sec]",
self.operation,
format_number(self.current),
format_number(total),
elapsed_str,
rate
);
} else {
log::info!(
"{}: completed {} items [{} elapsed]",
self.operation,
format_number(self.current),
elapsed_str
);
}
}
fn report_progress(&mut self) {
self.has_reported = true;
let elapsed = self.start_time.elapsed();
let elapsed_str = format_duration(elapsed);
if let Some(total) = self.total {
let percentage =
if total > 0 { (self.current as f64 / total as f64 * 100.0) as u8 } else { 0 };
let remaining_str = if self.current > 0 && elapsed.as_secs_f64() > 0.0 {
let rate = self.current as f64 / elapsed.as_secs_f64();
let remaining_items = total.saturating_sub(self.current);
let remaining_secs = remaining_items as f64 / rate;
format!(", ~{} remaining", format_duration(Duration::from_secs_f64(remaining_secs)))
} else {
String::new()
};
log::info!(
"{}: {} / {} items ({}%) [{} elapsed{}]",
self.operation,
format_number(self.current),
format_number(total),
percentage,
elapsed_str,
remaining_str
);
} else {
log::info!(
"{}: {} items [{} elapsed]",
self.operation,
format_number(self.current),
elapsed_str
);
}
}
}
fn format_number(n: usize) -> String {
let s = n.to_string();
let chars: Vec<char> = s.chars().collect();
let mut result = String::new();
for (i, c) in chars.iter().enumerate() {
if i > 0 && (chars.len() - i).is_multiple_of(3) {
result.push(',');
}
result.push(*c);
}
result
}
fn format_duration(d: Duration) -> String {
let total_secs = d.as_secs();
if total_secs < 60 {
format!("{}s", total_secs)
} else if total_secs < 3600 {
let mins = total_secs / 60;
let secs = total_secs % 60;
format!("{}m {}s", mins, secs)
} else {
let hours = total_secs / 3600;
let mins = (total_secs % 3600) / 60;
let secs = total_secs % 60;
format!("{}h {}m {}s", hours, mins, secs)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_number() {
assert_eq!(format_number(0), "0");
assert_eq!(format_number(100), "100");
assert_eq!(format_number(1000), "1,000");
assert_eq!(format_number(1234567), "1,234,567");
}
#[test]
fn test_format_duration() {
assert_eq!(format_duration(Duration::from_secs(5)), "5s");
assert_eq!(format_duration(Duration::from_secs(65)), "1m 5s");
assert_eq!(format_duration(Duration::from_secs(3665)), "1h 1m 5s");
}
#[test]
fn test_progress_tracker_disabled() {
disable();
let mut tracker = ProgressTracker::new("Test operation", Some(100));
tracker.update(50);
tracker.finish();
}
#[test]
fn test_progress_tracker_enabled() {
enable();
let mut tracker = ProgressTracker::new("Test operation", Some(10000))
.with_interval(Duration::from_millis(1)); tracker.update(5000);
tracker.report();
tracker.finish();
disable();
}
}