use std::sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProgressSnapshot {
pub done: usize,
pub total: usize,
pub phase: String,
}
pub trait Sink: Send + Sync {
fn render(&self, snap: ProgressSnapshot);
}
struct ProgressInner {
done: AtomicUsize,
total: AtomicUsize,
phase: Mutex<String>,
sink: Option<Box<dyn Sink>>,
}
#[derive(Clone)]
pub struct Progress(Arc<ProgressInner>);
impl Progress {
pub fn null() -> Self {
Progress(Arc::new(ProgressInner {
done: AtomicUsize::new(0),
total: AtomicUsize::new(0),
phase: Mutex::new(String::new()),
sink: None,
}))
}
pub fn with_sink(sink: Box<dyn Sink>) -> Self {
Progress(Arc::new(ProgressInner {
done: AtomicUsize::new(0),
total: AtomicUsize::new(0),
phase: Mutex::new(String::new()),
sink: Some(sink),
}))
}
#[inline]
pub fn is_active(&self) -> bool {
self.0.sink.is_some()
}
pub fn set_total(&self, total: usize) {
self.0.total.store(total, Ordering::Relaxed);
}
#[inline]
pub fn done(&self) -> usize {
self.0.done.load(Ordering::Relaxed)
}
#[inline]
pub fn total(&self) -> usize {
self.0.total.load(Ordering::Relaxed)
}
pub fn set_phase(&self, label: impl Into<String>) {
let label = label.into();
{
let mut guard = self.lock_phase();
*guard = label;
}
if self.0.sink.is_some() {
self.render_current();
}
}
pub fn phase(&self) -> String {
self.lock_phase().clone()
}
#[inline]
pub fn inc(&self, n: usize) {
self.0.done.fetch_add(n, Ordering::Relaxed);
if self.0.sink.is_some() {
self.render_current();
}
}
fn render_current(&self) {
if let Some(sink) = &self.0.sink {
let snap = ProgressSnapshot {
done: self.0.done.load(Ordering::Relaxed),
total: self.0.total.load(Ordering::Relaxed),
phase: self.lock_phase().clone(),
};
sink.render(snap);
}
}
pub fn snapshot(&self) -> ProgressSnapshot {
ProgressSnapshot {
done: self.0.done.load(Ordering::Relaxed),
total: self.0.total.load(Ordering::Relaxed),
phase: self.lock_phase().clone(),
}
}
fn lock_phase(&self) -> std::sync::MutexGuard<'_, String> {
self.0.phase.lock().unwrap_or_else(|p| p.into_inner())
}
}
impl std::fmt::Debug for Progress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Progress")
.field("done", &self.done())
.field("total", &self.total())
.field("active", &self.is_active())
.finish_non_exhaustive()
}
}
impl Default for Progress {
fn default() -> Self {
Self::null()
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use super::*;
struct Shared<T: Sink>(Arc<T>);
impl<T: Sink> Sink for Shared<T> {
fn render(&self, snap: ProgressSnapshot) {
self.0.render(snap);
}
}
fn progress_over<T: Sink + 'static>(sink: &Arc<T>) -> Progress {
Progress::with_sink(Box::new(Shared(Arc::clone(sink))))
}
#[derive(Default)]
struct CapturingSink {
renders: Mutex<Vec<ProgressSnapshot>>,
calls: AtomicUsize,
}
impl CapturingSink {
fn snapshots(&self) -> Vec<ProgressSnapshot> {
self.renders.lock().unwrap().clone()
}
fn call_count(&self) -> usize {
self.calls.load(Ordering::Relaxed)
}
}
impl Sink for CapturingSink {
fn render(&self, snap: ProgressSnapshot) {
self.calls.fetch_add(1, Ordering::Relaxed);
self.renders.lock().unwrap().push(snap);
}
}
#[test]
fn null_path_renders_nothing_under_a_tight_loop() {
let p = Progress::null();
assert!(!p.is_active());
for _ in 0..1_000_000 {
p.inc(1);
}
assert_eq!(p.done(), 1_000_000);
assert_eq!(p.total(), 0);
}
#[test]
fn inactive_handle_never_dispatches_render() {
let p = Progress::null();
p.set_total(50);
p.set_phase("noise");
p.inc(10);
assert!(!p.is_active());
assert_eq!(p.done(), 10);
assert_eq!(p.total(), 50);
assert_eq!(p.phase(), "noise");
}
#[test]
fn inc_and_set_total_track_counters() {
let sink = Arc::new(CapturingSink::default());
let p = progress_over(&sink);
p.set_total(128);
p.inc(1);
p.inc(3);
assert_eq!(p.done(), 4);
assert_eq!(p.total(), 128);
assert_eq!(sink.call_count(), 2);
let snaps = sink.snapshots();
assert_eq!(snaps.last().unwrap().done, 4);
assert_eq!(snaps.last().unwrap().total, 128);
}
#[test]
fn set_phase_is_captured_and_renders() {
let sink = Arc::new(CapturingSink::default());
let p = progress_over(&sink);
p.set_phase("scanning refs");
p.inc(1);
p.set_phase("writing refs");
assert_eq!(p.phase(), "writing refs");
let snaps = sink.snapshots();
assert_eq!(snaps.len(), 3);
assert_eq!(snaps[0].phase, "scanning refs");
assert_eq!(snaps[1].phase, "scanning refs");
assert_eq!(snaps[2].phase, "writing refs");
}
#[test]
fn a_throttling_sink_sees_every_call_and_decides_itself() {
const INTERVAL: usize = 64;
#[derive(Default)]
struct ThrottlingSink {
seen: AtomicUsize,
painted: AtomicUsize,
}
impl Sink for ThrottlingSink {
fn render(&self, snap: ProgressSnapshot) {
self.seen.fetch_add(1, Ordering::Relaxed);
if snap.done.is_multiple_of(INTERVAL) {
self.painted.fetch_add(1, Ordering::Relaxed);
}
}
}
let sink = Arc::new(ThrottlingSink::default());
let p = progress_over(&sink);
for _ in 0..256 {
p.inc(1);
}
assert_eq!(sink.seen.load(Ordering::Relaxed), 256);
assert_eq!(sink.painted.load(Ordering::Relaxed), 4);
}
#[test]
fn clone_shares_counters() {
let sink = Arc::new(CapturingSink::default());
let p = progress_over(&sink);
let q = p.clone();
p.inc(2);
q.inc(3);
assert_eq!(p.done(), 5);
assert_eq!(q.done(), 5);
}
}