just playing with tangled
at diffedit3 231 lines 7.0 kB view raw
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}