2022-11-07 00:31:25 +00:00
|
|
|
use std::io;
|
2023-05-06 00:12:48 +00:00
|
|
|
use std::path::Path;
|
|
|
|
use std::sync::Mutex;
|
2022-10-23 20:20:02 +00:00
|
|
|
use std::time::{Duration, Instant};
|
2022-10-23 20:05:23 +00:00
|
|
|
|
2022-10-23 20:50:03 +00:00
|
|
|
use crossterm::terminal::{Clear, ClearType};
|
2023-06-28 14:12:40 +00:00
|
|
|
use jj_lib::git;
|
|
|
|
use jj_lib::repo_path::RepoPath;
|
2022-10-23 20:05:23 +00:00
|
|
|
|
2022-10-30 02:34:17 +00:00
|
|
|
use crate::cleanup_guard::CleanupGuard;
|
2023-06-12 12:44:43 +00:00
|
|
|
use crate::ui::{OutputGuard, ProgressOutput, Ui};
|
2022-10-23 20:05:23 +00:00
|
|
|
|
2022-11-06 18:15:44 +00:00
|
|
|
pub struct Progress {
|
2022-10-23 20:20:02 +00:00
|
|
|
next_print: Instant,
|
2022-10-23 20:05:23 +00:00
|
|
|
rate: RateEstimate,
|
|
|
|
buffer: String,
|
2022-10-30 02:34:17 +00:00
|
|
|
guard: Option<CleanupGuard>,
|
2022-10-23 20:05:23 +00:00
|
|
|
}
|
|
|
|
|
2022-11-06 18:15:44 +00:00
|
|
|
impl Progress {
|
|
|
|
pub fn new(now: Instant) -> Self {
|
2022-10-23 20:05:23 +00:00
|
|
|
Self {
|
2022-10-23 20:20:02 +00:00
|
|
|
next_print: now + INITIAL_DELAY,
|
2022-10-23 20:05:23 +00:00
|
|
|
rate: RateEstimate::new(),
|
|
|
|
buffer: String::new(),
|
2022-10-30 02:34:17 +00:00
|
|
|
guard: None,
|
2022-10-23 20:05:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-07 00:31:25 +00:00
|
|
|
pub fn update(
|
|
|
|
&mut self,
|
|
|
|
now: Instant,
|
|
|
|
progress: &git::Progress,
|
2023-06-12 12:44:43 +00:00
|
|
|
output: &mut ProgressOutput,
|
2022-11-07 00:31:25 +00:00
|
|
|
) -> io::Result<()> {
|
2022-10-23 20:05:23 +00:00
|
|
|
use std::fmt::Write as _;
|
|
|
|
|
2022-11-06 18:15:44 +00:00
|
|
|
if progress.overall == 1.0 {
|
2023-06-12 12:44:43 +00:00
|
|
|
write!(output, "\r{}", Clear(ClearType::CurrentLine))?;
|
|
|
|
output.flush()?;
|
2022-11-07 00:31:25 +00:00
|
|
|
return Ok(());
|
2022-11-06 18:15:44 +00:00
|
|
|
}
|
|
|
|
|
2022-10-23 20:20:02 +00:00
|
|
|
let rate = progress
|
|
|
|
.bytes_downloaded
|
|
|
|
.and_then(|x| self.rate.update(now, x));
|
|
|
|
if now < self.next_print {
|
2022-11-07 00:31:25 +00:00
|
|
|
return Ok(());
|
2022-10-23 20:20:02 +00:00
|
|
|
}
|
2022-10-30 02:34:17 +00:00
|
|
|
if self.guard.is_none() {
|
2023-06-12 12:44:43 +00:00
|
|
|
let guard = output.output_guard(crossterm::cursor::Show.to_string());
|
2022-10-30 02:34:17 +00:00
|
|
|
let guard = CleanupGuard::new(move || {
|
|
|
|
drop(guard);
|
|
|
|
});
|
2023-06-12 12:44:43 +00:00
|
|
|
_ = write!(output, "{}", crossterm::cursor::Hide);
|
2022-10-30 02:34:17 +00:00
|
|
|
self.guard = Some(guard);
|
|
|
|
}
|
2022-10-23 20:20:02 +00:00
|
|
|
self.next_print = now.min(self.next_print + Duration::from_secs(1) / UPDATE_HZ);
|
|
|
|
|
2022-10-23 20:05:23 +00:00
|
|
|
self.buffer.clear();
|
2022-10-23 20:50:03 +00:00
|
|
|
write!(self.buffer, "\r{}", Clear(ClearType::CurrentLine)).unwrap();
|
|
|
|
let control_chars = self.buffer.len();
|
|
|
|
write!(self.buffer, "{: >3.0}% ", 100.0 * progress.overall).unwrap();
|
2023-04-05 12:26:47 +00:00
|
|
|
if let Some(total) = progress.bytes_downloaded {
|
|
|
|
let (scaled, prefix) = binary_prefix(total as f32);
|
|
|
|
write!(self.buffer, "{scaled: >5.1} {prefix}B ").unwrap();
|
|
|
|
}
|
2022-10-23 20:20:02 +00:00
|
|
|
if let Some(estimate) = rate {
|
2022-10-23 20:05:23 +00:00
|
|
|
let (scaled, prefix) = binary_prefix(estimate);
|
2023-04-05 12:26:47 +00:00
|
|
|
write!(self.buffer, "at {scaled: >5.1} {prefix}B/s ").unwrap();
|
2022-10-23 20:05:23 +00:00
|
|
|
}
|
2022-10-23 20:50:03 +00:00
|
|
|
|
2023-06-12 12:44:43 +00:00
|
|
|
let bar_width = output
|
2023-03-10 10:00:32 +00:00
|
|
|
.term_width()
|
|
|
|
.map(usize::from)
|
2022-10-23 20:50:03 +00:00
|
|
|
.unwrap_or(0)
|
|
|
|
.saturating_sub(self.buffer.len() - control_chars + 2);
|
|
|
|
self.buffer.push('[');
|
|
|
|
draw_progress(progress.overall, &mut self.buffer, bar_width);
|
|
|
|
self.buffer.push(']');
|
|
|
|
|
2023-06-12 12:44:43 +00:00
|
|
|
write!(output, "{}", self.buffer)?;
|
|
|
|
output.flush()?;
|
2022-11-07 00:31:25 +00:00
|
|
|
Ok(())
|
2022-10-23 20:05:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-23 20:50:03 +00:00
|
|
|
fn draw_progress(progress: f32, buffer: &mut String, width: usize) {
|
|
|
|
const CHARS: [char; 9] = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
|
|
|
|
const RESOLUTION: usize = CHARS.len() - 1;
|
2022-10-27 13:49:46 +00:00
|
|
|
let ticks = (width as f32 * progress.clamp(0.0, 1.0) * RESOLUTION as f32).round() as usize;
|
2022-10-23 20:50:03 +00:00
|
|
|
let whole = ticks / RESOLUTION;
|
|
|
|
for _ in 0..whole {
|
|
|
|
buffer.push(CHARS[CHARS.len() - 1]);
|
|
|
|
}
|
|
|
|
if whole < width {
|
|
|
|
let fraction = ticks % RESOLUTION;
|
|
|
|
buffer.push(CHARS[fraction]);
|
|
|
|
}
|
|
|
|
for _ in (whole + 1)..width {
|
|
|
|
buffer.push(CHARS[0]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-23 20:20:02 +00:00
|
|
|
const UPDATE_HZ: u32 = 30;
|
|
|
|
const INITIAL_DELAY: Duration = Duration::from_millis(250);
|
|
|
|
|
2022-10-23 20:05:23 +00:00
|
|
|
/// Find the smallest binary prefix with which the whole part of `x` is at most
|
|
|
|
/// three digits, and return the scaled `x` and that prefix.
|
|
|
|
fn binary_prefix(x: f32) -> (f32, &'static str) {
|
|
|
|
const TABLE: [&str; 9] = ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"];
|
|
|
|
|
|
|
|
let mut i = 0;
|
|
|
|
let mut scaled = x;
|
|
|
|
while scaled.abs() >= 1000.0 && i < TABLE.len() - 1 {
|
|
|
|
i += 1;
|
|
|
|
scaled /= 1024.0;
|
|
|
|
}
|
|
|
|
(scaled, TABLE[i])
|
|
|
|
}
|
|
|
|
|
|
|
|
struct RateEstimate {
|
|
|
|
state: Option<RateEstimateState>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl RateEstimate {
|
|
|
|
fn new() -> Self {
|
|
|
|
RateEstimate { state: None }
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Compute smoothed rate from an update
|
|
|
|
fn update(&mut self, now: Instant, total: u64) -> Option<f32> {
|
|
|
|
if let Some(ref mut state) = self.state {
|
|
|
|
return Some(state.update(now, total));
|
|
|
|
}
|
|
|
|
|
|
|
|
self.state = Some(RateEstimateState {
|
|
|
|
total,
|
|
|
|
avg_rate: None,
|
|
|
|
last_sample: now,
|
|
|
|
});
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct RateEstimateState {
|
|
|
|
total: u64,
|
|
|
|
avg_rate: Option<f32>,
|
|
|
|
last_sample: Instant,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl RateEstimateState {
|
|
|
|
fn update(&mut self, now: Instant, total: u64) -> f32 {
|
|
|
|
let delta = total - self.total;
|
|
|
|
self.total = total;
|
|
|
|
let dt = now - self.last_sample;
|
|
|
|
self.last_sample = now;
|
|
|
|
let sample = delta as f32 / dt.as_secs_f32();
|
|
|
|
match self.avg_rate {
|
|
|
|
None => *self.avg_rate.insert(sample),
|
|
|
|
Some(ref mut avg_rate) => {
|
|
|
|
// From Algorithms for Unevenly Spaced Time Series: Moving
|
|
|
|
// Averages and Other Rolling Operators (Andreas Eckner, 2019)
|
|
|
|
const TIME_WINDOW: f32 = 2.0;
|
|
|
|
let alpha = 1.0 - (-dt.as_secs_f32() / TIME_WINDOW).exp();
|
|
|
|
*avg_rate += alpha * (sample - *avg_rate);
|
|
|
|
*avg_rate
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-10-23 20:50:03 +00:00
|
|
|
|
2023-06-12 12:44:43 +00:00
|
|
|
pub fn snapshot_progress(ui: &Ui) -> Option<impl Fn(&RepoPath) + '_> {
|
|
|
|
struct State {
|
2023-05-06 00:12:48 +00:00
|
|
|
guard: Option<OutputGuard>,
|
2023-06-12 12:44:43 +00:00
|
|
|
output: ProgressOutput,
|
2023-05-26 16:01:22 +00:00
|
|
|
next_display_time: Instant,
|
2023-05-06 00:12:48 +00:00
|
|
|
}
|
|
|
|
|
2023-06-12 12:44:43 +00:00
|
|
|
let output = ui.progress_output()?;
|
2023-05-06 00:12:48 +00:00
|
|
|
|
2023-05-26 16:01:22 +00:00
|
|
|
// Don't clutter the output during fast operations.
|
2023-06-13 10:28:02 +00:00
|
|
|
let next_display_time = Instant::now() + INITIAL_DELAY;
|
2023-05-26 16:01:22 +00:00
|
|
|
let state = Mutex::new(State {
|
|
|
|
guard: None,
|
2023-06-12 12:44:43 +00:00
|
|
|
output,
|
2023-05-26 16:01:22 +00:00
|
|
|
next_display_time,
|
|
|
|
});
|
2023-05-06 00:12:48 +00:00
|
|
|
|
|
|
|
Some(move |path: &RepoPath| {
|
2023-05-26 16:01:22 +00:00
|
|
|
let mut state = state.lock().unwrap();
|
|
|
|
let now = Instant::now();
|
|
|
|
if now < state.next_display_time {
|
|
|
|
// Future work: Display current path after exactly, say, 250ms has elapsed, to
|
|
|
|
// better handle large single files
|
2023-05-06 00:12:48 +00:00
|
|
|
return;
|
|
|
|
}
|
2023-06-13 10:28:02 +00:00
|
|
|
state.next_display_time = now + Duration::from_secs(1) / UPDATE_HZ;
|
2023-05-26 16:01:22 +00:00
|
|
|
|
2023-05-06 00:12:48 +00:00
|
|
|
if state.guard.is_none() {
|
|
|
|
state.guard = Some(
|
|
|
|
state
|
2023-06-12 12:44:43 +00:00
|
|
|
.output
|
2023-05-06 00:12:48 +00:00
|
|
|
.output_guard(format!("\r{}", Clear(ClearType::CurrentLine))),
|
|
|
|
);
|
|
|
|
}
|
2023-05-10 00:31:20 +00:00
|
|
|
|
2023-06-12 12:44:43 +00:00
|
|
|
let line_width = state.output.term_width().map(usize::from).unwrap_or(80);
|
2023-05-10 00:31:20 +00:00
|
|
|
let path_width = line_width.saturating_sub(13); // Account for "Snapshotting "
|
|
|
|
|
2023-05-06 00:12:48 +00:00
|
|
|
_ = write!(
|
2023-06-12 12:44:43 +00:00
|
|
|
state.output,
|
2023-05-10 00:31:20 +00:00
|
|
|
"\r{}Snapshotting {:.*}",
|
2023-05-06 00:12:48 +00:00
|
|
|
Clear(ClearType::CurrentLine),
|
2023-05-10 00:31:20 +00:00
|
|
|
path_width,
|
2023-05-06 00:12:48 +00:00
|
|
|
path.to_fs_path(Path::new("")).display()
|
|
|
|
);
|
2023-06-12 12:44:43 +00:00
|
|
|
_ = state.output.flush();
|
2023-05-06 00:12:48 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-10-23 20:50:03 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_bar() {
|
|
|
|
let mut buf = String::new();
|
|
|
|
draw_progress(0.0, &mut buf, 10);
|
|
|
|
assert_eq!(buf, " ");
|
|
|
|
buf.clear();
|
|
|
|
draw_progress(1.0, &mut buf, 10);
|
|
|
|
assert_eq!(buf, "██████████");
|
|
|
|
buf.clear();
|
|
|
|
draw_progress(0.5, &mut buf, 10);
|
|
|
|
assert_eq!(buf, "█████ ");
|
|
|
|
buf.clear();
|
|
|
|
draw_progress(0.54, &mut buf, 10);
|
|
|
|
assert_eq!(buf, "█████▍ ");
|
|
|
|
buf.clear();
|
|
|
|
}
|
|
|
|
}
|