just playing with tangled
1use std::io;
2use std::path::Path;
3use std::sync::Mutex;
4use std::time::{Duration, Instant};
5
6use crossterm::terminal::{Clear, ClearType};
7use jj_lib::fmt_util::binary_prefix;
8use jj_lib::git;
9use jj_lib::repo_path::RepoPath;
10
11use crate::cleanup_guard::CleanupGuard;
12use crate::text_util;
13use crate::ui::{OutputGuard, ProgressOutput, Ui};
14
15pub struct Progress {
16 next_print: Instant,
17 rate: RateEstimate,
18 buffer: String,
19 guard: Option<CleanupGuard>,
20}
21
22impl Progress {
23 pub fn new(now: Instant) -> Self {
24 Self {
25 next_print: now + INITIAL_DELAY,
26 rate: RateEstimate::new(),
27 buffer: String::new(),
28 guard: None,
29 }
30 }
31
32 pub fn update(
33 &mut self,
34 now: Instant,
35 progress: &git::Progress,
36 output: &mut ProgressOutput,
37 ) -> io::Result<()> {
38 use std::fmt::Write as _;
39
40 if progress.overall == 1.0 {
41 write!(output, "\r{}", Clear(ClearType::CurrentLine))?;
42 output.flush()?;
43 return Ok(());
44 }
45
46 let rate = progress
47 .bytes_downloaded
48 .and_then(|x| self.rate.update(now, x));
49 if now < self.next_print {
50 return Ok(());
51 }
52 if self.guard.is_none() {
53 let guard = output.output_guard(crossterm::cursor::Show.to_string());
54 let guard = CleanupGuard::new(move || {
55 drop(guard);
56 });
57 _ = write!(output, "{}", crossterm::cursor::Hide);
58 self.guard = Some(guard);
59 }
60 self.next_print = now.min(self.next_print + Duration::from_secs(1) / UPDATE_HZ);
61
62 self.buffer.clear();
63 write!(self.buffer, "\r{}", Clear(ClearType::CurrentLine)).unwrap();
64 let control_chars = self.buffer.len();
65 write!(self.buffer, "{: >3.0}% ", 100.0 * progress.overall).unwrap();
66 if let Some(total) = progress.bytes_downloaded {
67 let (scaled, prefix) = binary_prefix(total as f32);
68 write!(self.buffer, "{scaled: >5.1} {prefix}B ").unwrap();
69 }
70 if let Some(estimate) = rate {
71 let (scaled, prefix) = binary_prefix(estimate);
72 write!(self.buffer, "at {scaled: >5.1} {prefix}B/s ").unwrap();
73 }
74
75 let bar_width = output
76 .term_width()
77 .map(usize::from)
78 .unwrap_or(0)
79 .saturating_sub(self.buffer.len() - control_chars + 2);
80 self.buffer.push('[');
81 draw_progress(progress.overall, &mut self.buffer, bar_width);
82 self.buffer.push(']');
83
84 write!(output, "{}", self.buffer)?;
85 output.flush()?;
86 Ok(())
87 }
88}
89
90fn draw_progress(progress: f32, buffer: &mut String, width: usize) {
91 const CHARS: [char; 9] = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
92 const RESOLUTION: usize = CHARS.len() - 1;
93 let ticks = (width as f32 * progress.clamp(0.0, 1.0) * RESOLUTION as f32).round() as usize;
94 let whole = ticks / RESOLUTION;
95 for _ in 0..whole {
96 buffer.push(CHARS[CHARS.len() - 1]);
97 }
98 if whole < width {
99 let fraction = ticks % RESOLUTION;
100 buffer.push(CHARS[fraction]);
101 }
102 for _ in (whole + 1)..width {
103 buffer.push(CHARS[0]);
104 }
105}
106
107const UPDATE_HZ: u32 = 30;
108const INITIAL_DELAY: Duration = Duration::from_millis(250);
109
110struct RateEstimate {
111 state: Option<RateEstimateState>,
112}
113
114impl RateEstimate {
115 fn new() -> Self {
116 RateEstimate { state: None }
117 }
118
119 /// Compute smoothed rate from an update
120 fn update(&mut self, now: Instant, total: u64) -> Option<f32> {
121 if let Some(ref mut state) = self.state {
122 return Some(state.update(now, total));
123 }
124
125 self.state = Some(RateEstimateState {
126 total,
127 avg_rate: None,
128 last_sample: now,
129 });
130 None
131 }
132}
133
134struct RateEstimateState {
135 total: u64,
136 avg_rate: Option<f32>,
137 last_sample: Instant,
138}
139
140impl RateEstimateState {
141 fn update(&mut self, now: Instant, total: u64) -> f32 {
142 let delta = total - self.total;
143 self.total = total;
144 let dt = now - self.last_sample;
145 self.last_sample = now;
146 let sample = delta as f32 / dt.as_secs_f32();
147 match self.avg_rate {
148 None => *self.avg_rate.insert(sample),
149 Some(ref mut avg_rate) => {
150 // From Algorithms for Unevenly Spaced Time Series: Moving
151 // Averages and Other Rolling Operators (Andreas Eckner, 2019)
152 const TIME_WINDOW: f32 = 2.0;
153 let alpha = 1.0 - (-dt.as_secs_f32() / TIME_WINDOW).exp();
154 *avg_rate += alpha * (sample - *avg_rate);
155 *avg_rate
156 }
157 }
158 }
159}
160
161pub fn snapshot_progress(ui: &Ui) -> Option<impl Fn(&RepoPath) + '_> {
162 struct State {
163 guard: Option<OutputGuard>,
164 output: ProgressOutput,
165 next_display_time: Instant,
166 }
167
168 let output = ui.progress_output()?;
169
170 // Don't clutter the output during fast operations.
171 let next_display_time = Instant::now() + INITIAL_DELAY;
172 let state = Mutex::new(State {
173 guard: None,
174 output,
175 next_display_time,
176 });
177
178 Some(move |path: &RepoPath| {
179 let mut state = state.lock().unwrap();
180 let now = Instant::now();
181 if now < state.next_display_time {
182 // Future work: Display current path after exactly, say, 250ms has elapsed, to
183 // better handle large single files
184 return;
185 }
186 state.next_display_time = now + Duration::from_secs(1) / UPDATE_HZ;
187
188 if state.guard.is_none() {
189 state.guard = Some(
190 state
191 .output
192 .output_guard(format!("\r{}", Clear(ClearType::CurrentLine))),
193 );
194 }
195
196 let line_width = state.output.term_width().map(usize::from).unwrap_or(80);
197 let max_path_width = line_width.saturating_sub(13); // Account for "Snapshotting "
198 let fs_path = path.to_fs_path(Path::new(""));
199 let (display_path, _) =
200 text_util::elide_start(fs_path.to_str().unwrap(), "...", max_path_width);
201
202 _ = write!(
203 state.output,
204 "\r{}Snapshotting {display_path}",
205 Clear(ClearType::CurrentLine),
206 );
207 _ = state.output.flush();
208 })
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn test_bar() {
217 let mut buf = String::new();
218 draw_progress(0.0, &mut buf, 10);
219 assert_eq!(buf, " ");
220 buf.clear();
221 draw_progress(1.0, &mut buf, 10);
222 assert_eq!(buf, "██████████");
223 buf.clear();
224 draw_progress(0.5, &mut buf, 10);
225 assert_eq!(buf, "█████ ");
226 buf.clear();
227 draw_progress(0.54, &mut buf, 10);
228 assert_eq!(buf, "█████▍ ");
229 buf.clear();
230 }
231}