diff --git a/Cargo.lock b/Cargo.lock index 8ae927a..cc58708 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,12 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi-escapes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3c0daaaae24df5995734b689627f8fa02101bc5bbc768be3055b66a010d7af" + [[package]] name = "anstream" version = "0.6.4" @@ -93,10 +99,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" [[package]] -name = "atomic-counter" -version = "1.0.1" +name = "atty" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f447d68cfa5a9ab0c1c862a703da2a65b5ed1b7ce1153c9eb0169506d56019" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] [[package]] name = "autocfg" @@ -474,7 +485,6 @@ name = "fclones" version = "0.33.0" dependencies = [ "assert_matches", - "atomic-counter", "bincode", "blake3", "byte-unit", @@ -496,7 +506,6 @@ dependencies = [ "hex", "ignore", "indexmap 2.0.2", - "indicatif", "itertools", "lazy-init", "lazy_static", @@ -519,6 +528,7 @@ dependencies = [ "sha3", "sled", "smallvec", + "status-line", "stfu8", "sysinfo", "tempfile", @@ -646,6 +656,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.3" @@ -718,19 +737,6 @@ dependencies = [ "hashbrown 0.14.2", ] -[[package]] -name = "indicatif" -version = "0.17.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25" -dependencies = [ - "console", - "instant", - "number_prefix", - "portable-atomic", - "unicode-width", -] - [[package]] name = "instant" version = "0.1.12" @@ -901,16 +907,10 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.3", "libc", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "once_cell" version = "1.18.0" @@ -981,12 +981,6 @@ dependencies = [ "syn", ] -[[package]] -name = "portable-atomic" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31114a898e107c51bb1609ffaf55a0e011cf6a4d7f1170d0015a165082c0338b" - [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1275,6 +1269,16 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +[[package]] +name = "status-line" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20cc99bbe608305546a850ec4352907279a8b8044f9c13ae58bd0a8ab46ebc1" +dependencies = [ + "ansi-escapes", + "atty", +] + [[package]] name = "stfu8" version = "0.2.6" diff --git a/fclones/Cargo.toml b/fclones/Cargo.toml index 25fc311..9407fee 100644 --- a/fclones/Cargo.toml +++ b/fclones/Cargo.toml @@ -17,7 +17,6 @@ exclude = [ rust-version = "1.70" [dependencies] -atomic-counter = "1.0" bincode = "1.3" blake3 = { version = "1.3", optional = true } byteorder = "1.4" @@ -36,7 +35,6 @@ fallible-iterator = "0.3" filetime = "0.2" hex = "0.4" ignore = "0.4.18" -indicatif = "0.17" indexmap = "2" itertools = "0.11" lazy-init = "0.5" @@ -56,6 +54,7 @@ sha2 = { version = "0.10", optional = true } sha3 = { version = "0.10", optional = true } sled = "0.34" smallvec = "1.8" +status-line = "0.2.0" stfu8 = "0.2" sysinfo = "0.29" thread_local = "1.1" diff --git a/fclones/src/log.rs b/fclones/src/log.rs index 765c74a..e68ab9f 100644 --- a/fclones/src/log.rs +++ b/fclones/src/log.rs @@ -5,7 +5,7 @@ use std::sync::{Arc, Mutex, Weak}; use console::style; use nom::lib::std::fmt::Display; -use crate::progress::{FastProgressBar, ProgressTracker}; +use crate::progress::{ProgressBar, ProgressTracker}; use chrono::Local; /// Determines the size of the task tracked by ProgressTracker. @@ -63,7 +63,7 @@ impl LogExt for L { /// A logger that uses standard error stream to communicate with the user. pub struct StdLog { program_name: String, - progress_bar: Mutex>, + progress_bar: Mutex>, pub log_stderr_to_stdout: bool, pub no_progress: bool, } @@ -84,9 +84,9 @@ impl StdLog { } /// Clears any previous progress bar or spinner and installs a new spinner. - pub fn spinner(&self, msg: &str) -> Arc { + pub fn spinner(&self, msg: &str) -> Arc { if self.no_progress { - return Arc::new(FastProgressBar::new_hidden()); + return Arc::new(ProgressBar::new_hidden()); } self.progress_bar .lock() @@ -94,30 +94,30 @@ impl StdLog { .upgrade() .iter() .for_each(|pb| pb.finish_and_clear()); - let result = Arc::new(FastProgressBar::new_spinner(msg)); + let result = Arc::new(ProgressBar::new_spinner(msg)); *self.progress_bar.lock().unwrap() = Arc::downgrade(&result); result } /// Clears any previous progress bar or spinner and installs a new progress bar. - pub fn progress_bar(&self, msg: &str, len: u64) -> Arc { + pub fn progress_bar(&self, msg: &str, len: u64) -> Arc { if self.no_progress { - return Arc::new(FastProgressBar::new_hidden()); + return Arc::new(ProgressBar::new_hidden()); } - let result = Arc::new(FastProgressBar::new_progress_bar(msg, len)); + let result = Arc::new(ProgressBar::new_progress_bar(msg, len)); *self.progress_bar.lock().unwrap() = Arc::downgrade(&result); result } /// Creates a no-op progressbar that doesn't display itself. - pub fn hidden(&self) -> Arc { - Arc::new(FastProgressBar::new_hidden()) + pub fn hidden(&self) -> Arc { + Arc::new(ProgressBar::new_hidden()) } /// Clears any previous progress bar or spinner and installs a new progress bar. - pub fn bytes_progress_bar(&self, msg: &str, len: u64) -> Arc { + pub fn bytes_progress_bar(&self, msg: &str, len: u64) -> Arc { if self.no_progress { - return Arc::new(FastProgressBar::new_hidden()); + return Arc::new(ProgressBar::new_hidden()); } self.progress_bar .lock() @@ -125,7 +125,7 @@ impl StdLog { .upgrade() .iter() .for_each(|pb| pb.finish_and_clear()); - let result = Arc::new(FastProgressBar::new_bytes_progress_bar(msg, len)); + let result = Arc::new(ProgressBar::new_bytes_progress_bar(msg, len)); *self.progress_bar.lock().unwrap() = Arc::downgrade(&result); result } diff --git a/fclones/src/progress.rs b/fclones/src/progress.rs index 716dcdc..6877d43 100644 --- a/fclones/src/progress.rs +++ b/fclones/src/progress.rs @@ -1,11 +1,11 @@ //! Fast, concurrent, lockless progress bars. -use atomic_counter::{AtomicCounter, RelaxedCounter}; +use crate::FileLen; use console::style; -use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle}; -use std::sync::Arc; -use std::thread; -use std::time::Duration; +use status_line::{Options, StatusLine}; +use std::fmt::{Display, Formatter}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::Instant; /// Common interface for components that can show progress of a task. E.g. progress bars. pub trait ProgressTracker: Sync + Send { @@ -21,210 +21,248 @@ impl ProgressTracker for NoProgressBar { fn inc(&self, _delta: u64) {} } -/// Console-based progress bar that renders to standard error. -/// -/// Implemented as a wrapper over `indicatif::ProgressBar` that makes updating its progress -/// lockless. -/// Unfortunately `indicatif::ProgressBar` wraps state in a `Mutex`, so updates are slow -/// and can become a bottleneck in multithreaded context. -/// This wrapper uses `atomic_counter::RelaxedCounter` to keep shared state without ever blocking -/// writers. That state is copied repeatedly by a background thread to an underlying -/// `ProgressBar` at a low rate. -pub struct FastProgressBar { - counter: Arc, - progress_bar: Arc, +#[derive(Debug, Default)] +enum ProgressUnit { + #[default] + Item, + Bytes, } -impl FastProgressBar { - /// Width of the progress bar in characters - const WIDTH: usize = 50; - /// Spinner animation looks like this (moves right and left): - const SPACESHIP: &'static str = "<===>"; - /// Progress bar looks like this: - const PROGRESS_CHARS: &'static str = "=> "; - /// How much time to wait between refreshes, in milliseconds - const REFRESH_PERIOD_MS: u64 = 50; - - /// Wrap an existing `ProgressBar` and start the background updater-thread. - /// The thread periodically copies the `FastProgressBar` position into the wrapped - /// `ProgressBar` instance. - pub fn wrap(progress_bar: ProgressBar) -> FastProgressBar { - let pb = Arc::new(progress_bar); - let pb2 = pb.clone(); - let counter = Arc::new(RelaxedCounter::new(0)); - let counter2 = counter.clone(); - thread::spawn(move || { - while Arc::strong_count(&counter2) > 1 && !pb2.is_finished() { - pb2.set_position(counter2.get() as u64); - thread::sleep(Duration::from_millis(Self::REFRESH_PERIOD_MS)); - } - }); - FastProgressBar { - counter, - progress_bar: pb, +/// Keeps state of the progress bar and controls how it is rendered to a string +#[derive(Debug)] +struct Progress { + msg: String, // message shown before the progress bar + value: AtomicU64, // controls the length of the progress bar + max: Option, // maximum expected value, if not set an animated spinner is shown + unit: ProgressUnit, // how to format the numbers + start_time: Instant, // needed for the animation + color: bool, +} + +impl Progress { + fn fmt_value(&self, value: u64) -> String { + match self.unit { + ProgressUnit::Item => value.to_string(), + ProgressUnit::Bytes => FileLen(value).to_string(), } } - /// Generate spinner animation strings. - /// The spinner moves to the next string from the returned vector with every tick. - /// The spinner is rendered as a SPACESHIP that bounces right and left from the - /// ends of the spinner bar. - fn gen_tick_strings() -> Vec { - let mut tick_strings = vec![]; - for i in 0..(Self::WIDTH - Self::SPACESHIP.len()) { - let prefix_len = i; - let suffix_len = Self::WIDTH - i - Self::SPACESHIP.len(); - let tick_str = " ".repeat(prefix_len) + Self::SPACESHIP + &" ".repeat(suffix_len); - tick_strings.push(tick_str); + /// Draws the progress bar alone (without message and numbers) + fn bar(&self, length: usize) -> String { + let mut bar = "=".repeat(length); + if !bar.is_empty() { + bar.pop(); + bar.push('>'); } - let mut tick_strings_2 = tick_strings.clone(); - tick_strings_2.reverse(); - tick_strings.extend(tick_strings_2); - tick_strings + bar.truncate(MAX_BAR_LEN); + bar } + fn animate_spinner(&self, frame: u64) -> String { + let spaceship = "<===>"; + let max_pos = (MAX_BAR_LEN - spaceship.len()) as u64; + let pos = ((frame + max_pos) % (max_pos * 2)).abs_diff(max_pos); + assert!(pos < MAX_BAR_LEN as u64); + " ".repeat(pos as usize) + spaceship + } +} + +impl Default for Progress { + fn default() -> Self { + Progress { + msg: "".to_owned(), + value: AtomicU64::default(), + max: None, + unit: ProgressUnit::default(), + start_time: Instant::now(), + color: true, + } + } +} + +const MAX_BAR_LEN: usize = 50; + +impl Display for Progress { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let value = self.value.load(Ordering::Relaxed); + let value_str = self.fmt_value(value); + let msg = if self.color { + style(self.msg.clone()).for_stderr().cyan().bold() + } else { + style(self.msg.clone()) + }; + + match self.max { + Some(max) => { + let max_str = self.fmt_value(max); + let bar_len = (MAX_BAR_LEN as u64 * value / max.max(1)) as usize; + let bar = self.bar(bar_len); + write!(f, "{msg:32}[{bar:MAX_BAR_LEN$}]{value_str:>14} / {max_str}") + } + None => { + let frame = (self.start_time.elapsed().as_millis() / 50) as u64; + let bar = self.animate_spinner(frame); + write!(f, "{msg:32}[{bar:MAX_BAR_LEN$}]{value_str:>14}") + } + } + } +} + +/// Console-based progress bar that renders to standard error. +pub struct ProgressBar { + status_line: StatusLine, +} + +impl ProgressBar { /// Create a new preconfigured animated spinner with given message. - pub fn new_spinner(msg: &str) -> FastProgressBar { - let inner = ProgressBar::new_spinner(); - let template = - style("{msg:32}").cyan().bold().for_stderr().to_string() + "[{spinner}] {pos:>10}"; - let tick_strings = Self::gen_tick_strings(); - let tick_strings: Vec<&str> = tick_strings.iter().map(|s| s as &str).collect(); - inner.set_style( - ProgressStyle::default_spinner() - .template(template.as_str()) - .unwrap() - .tick_strings(tick_strings.as_slice()), - ); - inner.set_message(msg.to_string()); - Self::wrap(inner) + pub fn new_spinner(msg: &str) -> ProgressBar { + let progress = Progress { + msg: msg.to_string(), + ..Default::default() + }; + ProgressBar { + status_line: StatusLine::new(progress), + } } /// Create a new preconfigured progress bar with given message. - pub fn new_progress_bar(msg: &str, len: u64) -> FastProgressBar { - let inner = ProgressBar::new(len); - let template = style("{msg:32}").cyan().bold().for_stderr().to_string() - + &"[{bar:WIDTH}] {pos:>10}/{len}".replace("WIDTH", Self::WIDTH.to_string().as_str()); - - inner.set_style( - ProgressStyle::default_bar() - .template(template.as_str()) - .unwrap() - .progress_chars(Self::PROGRESS_CHARS), - ); - inner.set_message(msg.to_string()); - FastProgressBar::wrap(inner) + pub fn new_progress_bar(msg: &str, len: u64) -> ProgressBar { + let progress = Progress { + msg: msg.to_string(), + max: Some(len), + ..Default::default() + }; + ProgressBar { + status_line: StatusLine::new(progress), + } } /// Create a new preconfigured progress bar with given message. /// Displays progress in bytes. - pub fn new_bytes_progress_bar(msg: &str, len: u64) -> FastProgressBar { - let inner = ProgressBar::new(len); - let template = style("{msg:32}").cyan().bold().for_stderr().to_string() - + &"[{bar:WIDTH}] {bytes:>10}/{total_bytes}" - .replace("WIDTH", Self::WIDTH.to_string().as_str()); - - inner.set_style( - ProgressStyle::default_bar() - .template(template.as_str()) - .unwrap() - .progress_chars(Self::PROGRESS_CHARS), - ); - inner.set_message(msg.to_string()); - - FastProgressBar::wrap(inner) + pub fn new_bytes_progress_bar(msg: &str, len: u64) -> ProgressBar { + let progress = Progress { + msg: msg.to_string(), + max: Some(len), + unit: ProgressUnit::Bytes, + ..Default::default() + }; + ProgressBar { + status_line: StatusLine::new(progress), + } } /// Creates a new invisible progress bar. /// This is useful when you need to disable progress bar, but you need to pass an instance /// of a `ProgressBar` to something that expects it. - pub fn new_hidden() -> FastProgressBar { - let inner = ProgressBar::new(u64::MAX); - inner.set_draw_target(ProgressDrawTarget::hidden()); - FastProgressBar::wrap(inner) - } - - fn update_progress(&self) { - let value = self.counter.get() as u64; - self.progress_bar.set_position(value); - } - - pub fn set_draw_target(&self, target: ProgressDrawTarget) { - self.progress_bar.set_draw_target(target) + pub fn new_hidden() -> ProgressBar { + ProgressBar { + status_line: StatusLine::with_options( + Progress::default(), + Options { + refresh_period: Default::default(), + initially_visible: false, + enable_ansi_escapes: false, + }, + ), + } } pub fn is_visible(&self) -> bool { - !self.progress_bar.is_hidden() + self.status_line.is_visible() } pub fn println>(&self, msg: I) { - self.progress_bar.println(msg); + let was_visible = self.status_line.is_visible(); + self.status_line.set_visible(false); + println!("{}", msg.as_ref()); + self.status_line.set_visible(was_visible); } pub fn tick(&self) { - self.counter.inc(); + self.status_line.value.fetch_add(1, Ordering::Relaxed); } - pub fn position(&self) -> usize { - self.counter.get() + pub fn finish_and_clear(&self) { + self.status_line.set_visible(false); } +} - pub fn last_displayed_position(&self) -> u64 { - self.progress_bar.position() +impl ProgressTracker for ProgressBar { + fn inc(&self, delta: u64) { + self.status_line.value.fetch_add(delta, Ordering::Relaxed); } +} - pub fn finish(&self) { - self.update_progress(); - self.progress_bar.finish(); - } +#[cfg(test)] +mod test { + use crate::progress::{Progress, ProgressUnit}; + use crate::regex::Regex; + use std::sync::atomic::{AtomicU64, Ordering}; - pub fn finish_and_clear(&self) { - self.update_progress(); - self.progress_bar.finish_and_clear(); - } + #[test] + fn draw_progress_bar() { + let p = Progress { + msg: "Message".to_string(), + max: Some(100), + color: false, + ..Default::default() + }; - pub fn abandon(&self) { - self.update_progress(); - self.progress_bar.abandon(); + assert_eq!(p.to_string(), "Message [ ] 0 / 100"); + p.value.fetch_add(2, Ordering::Relaxed); + assert_eq!(p.to_string(), "Message [> ] 2 / 100"); + p.value.fetch_add(50, Ordering::Relaxed); + assert_eq!(p.to_string(), "Message [=========================> ] 52 / 100"); + p.value.fetch_add(48, Ordering::Relaxed); + assert_eq!(p.to_string(), "Message [=================================================>] 100 / 100"); } - pub fn is_finished(&self) -> bool { - self.progress_bar.is_finished() - } -} + #[test] + fn draw_progress_bar_bytes() { + let p = Progress { + msg: "Message".to_string(), + max: Some(1000000000), + value: AtomicU64::new(12000), + unit: ProgressUnit::Bytes, + color: false, + ..Default::default() + }; -impl ProgressTracker for FastProgressBar { - fn inc(&self, delta: u64) { - self.counter.add(delta as usize); + assert_eq!(p.to_string(), "Message [ ] 12.0 KB / 1000.0 MB"); } -} -impl Drop for FastProgressBar { - fn drop(&mut self) { - if !self.is_finished() { - self.finish_and_clear(); - } - } -} + #[test] + fn animate_spinner() { + let p = Progress { + msg: "Message".to_string(), + color: false, + ..Default::default() + }; -#[cfg(test)] -mod test { + let pattern = Regex::new( + "^Message \\[ *<===> *\\] 0$", + false, + ) + .unwrap(); + let s = p.to_string(); + assert!( + pattern.is_match(s.as_str()), + "Spinner doesn't match pattern: {}", + s + ); - use super::*; - use rayon::prelude::*; + assert_eq!(p.animate_spinner(0), "<===>"); + assert_eq!(p.animate_spinner(1), " <===>"); + assert_eq!(p.animate_spinner(2), " <===>"); + assert_eq!(p.animate_spinner(3), " <===>"); - #[test] - fn all_ticks_should_be_counted() { - let collection = vec![0; 100000]; - let pb = ProgressBar::new(collection.len() as u64); - pb.set_draw_target(ProgressDrawTarget::hidden()); - let pb = FastProgressBar::wrap(pb); - collection - .par_iter() - .inspect(|_| pb.tick()) - .for_each(|_| ()); - pb.abandon(); - assert_eq!(pb.position(), 100000); - assert_eq!(pb.last_displayed_position(), 100000); + assert_eq!(p.animate_spinner(85), " <===>"); + assert_eq!(p.animate_spinner(86), " <===>"); + assert_eq!(p.animate_spinner(87), " <===>"); + assert_eq!(p.animate_spinner(88), " <===>"); + assert_eq!(p.animate_spinner(89), " <===>"); + assert_eq!(p.animate_spinner(90), "<===>"); + assert_eq!(p.animate_spinner(91), " <===>"); + assert_eq!(p.animate_spinner(92), " <===>"); } }