1use std::io::{self, BufRead, Write};
7
8use unicode_width::UnicodeWidthStr;
9
10use crate::diffstat::terminal_columns;
11
12const TP_IDX_MAX: usize = 8;
13
14thread_local! {
15 static PROGRESS_TEST_NS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
16}
17
18fn utf8_display_width(s: &str) -> usize {
19 UnicodeWidthStr::width(s)
20}
21
22fn humanise_bytes_value_unit(bytes: u64, rate: bool) -> (String, &'static str) {
24 if bytes > 1 << 30 {
25 let value = format!(
26 "{}.{:02}",
27 bytes >> 30,
28 (bytes & ((1 << 30) - 1)) / 10_737_419
29 );
30 let unit = if rate { "GiB/s" } else { "GiB" };
31 (value, unit)
32 } else if bytes > 1 << 20 {
33 let x = bytes + 5243;
34 let value = format!("{}.{:02}", x >> 20, ((x & ((1 << 20) - 1)) * 100) >> 20);
35 let unit = if rate { "MiB/s" } else { "MiB" };
36 (value, unit)
37 } else if bytes > 1 << 10 {
38 let x = bytes + 5;
39 let value = format!("{}.{:02}", x >> 10, ((x & ((1 << 10) - 1)) * 100) >> 10);
40 let unit = if rate { "KiB/s" } else { "KiB" };
41 (value, unit)
42 } else {
43 let value = format!("{bytes}");
44 let unit = if rate {
45 if bytes == 1 {
46 "byte/s"
47 } else {
48 "bytes/s"
49 }
50 } else if bytes == 1 {
51 "byte"
52 } else {
53 "bytes"
54 };
55 (value, unit)
56 }
57}
58
59fn throughput_display(total: u64, rate: u32) -> String {
60 let (v1, u1) = humanise_bytes_value_unit(total, false);
61 let (v2, u2) = humanise_bytes_value_unit(u64::from(rate).saturating_mul(1024), true);
62 format!(", {v1} {u1} | {v2} {u2}")
63}
64
65struct Throughput {
66 curr_total: u64,
67 prev_total: u64,
68 prev_ns: u64,
69 avg_bytes: u64,
70 avg_misecs: u64,
71 last_bytes: [u32; TP_IDX_MAX],
72 last_misecs: [u32; TP_IDX_MAX],
73 idx: usize,
74 display: String,
75}
76
77impl Throughput {
78 fn new(byte_count: u64, now_ns: u64) -> Self {
79 Self {
80 curr_total: byte_count,
81 prev_total: byte_count,
82 prev_ns: now_ns,
83 avg_bytes: 0,
84 avg_misecs: 0,
85 last_bytes: [0; TP_IDX_MAX],
86 last_misecs: [0; TP_IDX_MAX],
87 idx: 0,
88 display: String::new(),
89 }
90 }
91}
92
93struct Progress {
94 title: String,
95 last_value: Option<u64>,
96 total: u64,
97 last_percent: Option<u32>,
98 throughput: Option<Throughput>,
99 start_ns: u64,
100 counters_sb: String,
101 title_len: usize,
102 split: bool,
103 force_update: bool,
104}
105
106impl Progress {
107 fn new(title: String, total: u64) -> Self {
108 let title_len = utf8_display_width(&title);
109 let start_ns = nanotime();
110 Self {
111 title,
112 last_value: None,
113 total,
114 last_percent: None,
115 throughput: None,
116 start_ns,
117 counters_sb: String::new(),
118 title_len,
119 split: false,
120 force_update: false,
121 }
122 }
123
124 fn now_ns(&self) -> u64 {
125 self.start_ns.saturating_add(PROGRESS_TEST_NS.get())
126 }
127
128 fn render_line(&mut self, n: u64, done_suffix: Option<&str>) -> io::Result<()> {
129 let mut show_update = false;
130 let update = self.force_update;
131 self.force_update = false;
132
133 let last_count_len = self.counters_sb.len();
134 self.last_value = Some(n);
135
136 let tp = self
137 .throughput
138 .as_ref()
139 .map(|t| t.display.as_str())
140 .unwrap_or("");
141
142 if self.total > 0 {
143 let percent: u32 = ((n as u128 * 100) / u128::from(self.total)) as u32;
144 if Some(percent) != self.last_percent || update {
145 self.last_percent = Some(percent);
146 self.counters_sb = format!("{:3}% ({n}/{}){tp}", percent, self.total);
147 show_update = true;
148 }
149 } else if update {
150 self.counters_sb = format!("{n}{tp}");
151 show_update = true;
152 }
153
154 if !show_update {
155 return Ok(());
156 }
157
158 let stderr = io::stderr();
159 let show = is_foreground_stderr(&stderr) || done_suffix.is_some();
160 if !show {
161 return Ok(());
162 }
163
164 let eol = done_suffix.unwrap_or("\r");
165 let clear_len = if self.counters_sb.len() < last_count_len {
166 last_count_len - self.counters_sb.len() + 1
167 } else {
168 0
169 };
170 let progress_line_len = self.title_len + self.counters_sb.len() + 2;
171 let cols = terminal_columns();
172
173 let mut err = stderr.lock();
174 if self.split {
175 let w = clear_len.max(eol.len());
177 write!(err, " {}{:>w$}", self.counters_sb, eol, w = w)?;
178 } else if done_suffix.is_none() && cols < progress_line_len {
179 let title_pad = if self.title_len + 1 < cols {
180 cols - self.title_len - 1
181 } else {
182 0
183 };
184 write!(
185 err,
186 "{}:{}\n {}{:>w$}",
187 self.title,
188 " ".repeat(title_pad),
189 self.counters_sb,
190 eol,
191 w = clear_len.max(eol.len())
192 )?;
193 self.split = true;
194 } else {
195 write!(
196 err,
197 "{}: {}{:>w$}",
198 self.title,
199 self.counters_sb,
200 eol,
201 w = clear_len.max(eol.len())
202 )?;
203 }
204 err.flush()?;
205 Ok(())
206 }
207
208 fn display_progress(&mut self, n: u64) -> io::Result<()> {
209 self.render_line(n, None)
210 }
211
212 fn display_throughput(&mut self, total: u64, global_update: bool) -> io::Result<()> {
213 let now_ns = self.now_ns();
214
215 if self.throughput.is_none() {
216 self.throughput = Some(Throughput::new(total, now_ns));
217 return Ok(());
218 }
219 let tp = self.throughput.as_mut().unwrap();
220 tp.curr_total = total;
221
222 if now_ns.saturating_sub(tp.prev_ns) <= 500_000_000 {
223 return Ok(());
224 }
225
226 let misecs: u32 = (((now_ns - tp.prev_ns) as u128 * 4398) >> 32) as u32;
227 let count = total.saturating_sub(tp.prev_total);
228 tp.prev_total = total;
229 tp.prev_ns = now_ns;
230 tp.avg_bytes = tp.avg_bytes.saturating_add(count);
231 tp.avg_misecs = tp.avg_misecs.saturating_add(u64::from(misecs));
232 let rate = if tp.avg_misecs > 0 {
233 (tp.avg_bytes / tp.avg_misecs) as u32
234 } else {
235 0
236 };
237 tp.avg_bytes = tp
238 .avg_bytes
239 .saturating_sub(u64::from(tp.last_bytes[tp.idx]));
240 tp.avg_misecs = tp
241 .avg_misecs
242 .saturating_sub(u64::from(tp.last_misecs[tp.idx]));
243 tp.last_bytes[tp.idx] = count as u32;
244 tp.last_misecs[tp.idx] = misecs;
245 tp.idx = (tp.idx + 1) % TP_IDX_MAX;
246
247 tp.display = throughput_display(total, rate);
248
249 if self.last_value.is_some() && global_update {
250 let n = self.last_value.unwrap_or(0);
251 self.force_update = true;
252 self.display_progress(n)?;
253 }
254 Ok(())
255 }
256
257 fn force_last_update(&mut self, msg: &str) -> io::Result<()> {
258 let now_ns = self.now_ns();
259 if let Some(tp) = self.throughput.as_mut() {
260 let misecs: u32 =
261 (((now_ns.saturating_sub(self.start_ns)) as u128 * 4398) >> 32) as u32;
262 let rate = if misecs > 0 {
263 (tp.curr_total / u64::from(misecs)) as u32
264 } else {
265 0
266 };
267 tp.display = throughput_display(tp.curr_total, rate);
268 }
269 self.force_update = true;
270 let n = self.last_value.unwrap_or(0);
271 let done = format!(", {msg}.\n");
272 self.render_line(n, Some(&done))?;
273 Ok(())
274 }
275
276 fn stop(&mut self, trace_path: Option<&str>) -> io::Result<()> {
277 if self.last_value.is_some() {
278 self.force_last_update("done")?;
279 }
280 if let Some(path) = trace_path {
281 trace2_append_json_line(
282 path,
283 &format!(
284 r#"{{"event":"data","sid":"grit-0","time":"{}","category":"progress","key":"total_objects","value":"{}"}}"#,
285 trace_now(),
286 self.total
287 ),
288 )?;
289 if let Some(tp) = &self.throughput {
290 trace2_append_json_line(
291 path,
292 &format!(
293 r#"{{"event":"data","sid":"grit-0","time":"{}","category":"progress","key":"total_bytes","value":"{}"}}"#,
294 trace_now(),
295 tp.curr_total
296 ),
297 )?;
298 }
299 trace2_append_json_line(
300 path,
301 &format!(
302 r#"{{"event":"region_leave","sid":"grit-0","time":"{}","category":"progress","label":"{}","t_rel":0.0}}"#,
303 trace_now(),
304 json_escape(&self.title)
305 ),
306 )?;
307 }
308 Ok(())
309 }
310}
311
312fn nanotime() -> u64 {
313 use std::time::{SystemTime, UNIX_EPOCH};
314 SystemTime::now()
315 .duration_since(UNIX_EPOCH)
316 .map(|d| d.as_nanos() as u64)
317 .unwrap_or(0)
318}
319
320fn trace_now() -> String {
321 use std::time::{SystemTime, UNIX_EPOCH};
322 let now = SystemTime::now()
323 .duration_since(UNIX_EPOCH)
324 .unwrap_or_default();
325 let total_secs = now.as_secs();
326 let micros = now.subsec_micros();
327 let secs_in_day = total_secs % 86400;
328 let hours = secs_in_day / 3600;
329 let mins = (secs_in_day % 3600) / 60;
330 let secs = secs_in_day % 60;
331 format!("{:02}:{:02}:{:02}.{:06}", hours, mins, secs, micros)
332}
333
334fn json_escape(s: &str) -> String {
335 s.replace('\\', "\\\\").replace('"', "\\\"")
336}
337
338fn trace2_append_json_line(path: &str, line: &str) -> io::Result<()> {
339 let mut file = std::fs::OpenOptions::new()
340 .create(true)
341 .append(true)
342 .open(path)?;
343 writeln!(file, "{line}")
344}
345
346#[cfg(unix)]
347fn is_foreground_stderr(stderr: &io::Stderr) -> bool {
348 use std::os::unix::io::AsRawFd;
349 let fd = stderr.as_raw_fd();
350 unsafe {
352 let tpgrp = libc::tcgetpgrp(fd);
353 if tpgrp < 0 {
354 return true;
355 }
356 libc::getpgid(0) == tpgrp
357 }
358}
359
360#[cfg(not(unix))]
361fn is_foreground_stderr(_stderr: &io::Stderr) -> bool {
362 true
363}
364
365pub fn run() -> io::Result<()> {
370 PROGRESS_TEST_NS.set(0);
371
372 let trace_path = std::env::var("GIT_TRACE2_EVENT")
373 .ok()
374 .filter(|s| !s.is_empty());
375
376 let stdin = io::stdin();
377 let mut progress: Option<Progress> = None;
378 let mut title_storage: Vec<String> = Vec::new();
379
380 for line in stdin.lock().lines() {
381 let line = line?;
382 if let Some(rest) = line.strip_prefix("start ") {
383 let mut parts = rest.splitn(2, |c: char| c.is_ascii_whitespace());
384 let total_str = parts.next().unwrap_or("");
385 let total: u64 = total_str.parse().map_err(|e| {
386 io::Error::new(
387 io::ErrorKind::InvalidInput,
388 format!("invalid start total: {e}"),
389 )
390 })?;
391 let title: String = match parts.next() {
392 None | Some("") => "Working hard".to_string(),
393 Some(t) => {
394 title_storage.push(t.to_string());
395 title_storage.last().unwrap().clone()
396 }
397 };
398 if let Some(path) = trace_path.as_deref() {
399 trace2_append_json_line(
400 path,
401 &format!(
402 r#"{{"event":"region_enter","sid":"grit-0","time":"{}","category":"progress","label":"{}"}}"#,
403 trace_now(),
404 json_escape(&title)
405 ),
406 )?;
407 }
408 progress = Some(Progress::new(title, total));
409 } else if let Some(rest) = line.strip_prefix("progress ") {
410 let n: u64 = rest.trim().parse().map_err(|e| {
411 io::Error::new(
412 io::ErrorKind::InvalidInput,
413 format!("invalid progress value: {e}"),
414 )
415 })?;
416 if let Some(ref mut p) = progress {
417 p.display_progress(n)?;
418 }
419 } else if let Some(rest) = line.strip_prefix("throughput ") {
420 let mut it = rest.split_whitespace();
421 let byte_count: u64 = it
422 .next()
423 .ok_or_else(|| {
424 io::Error::new(io::ErrorKind::InvalidInput, "throughput: missing bytes")
425 })?
426 .parse()
427 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("{e}")))?;
428 let test_ms: u64 = it
429 .next()
430 .ok_or_else(|| {
431 io::Error::new(io::ErrorKind::InvalidInput, "throughput: missing millis")
432 })?
433 .parse()
434 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("{e}")))?;
435 PROGRESS_TEST_NS.set(test_ms.saturating_mul(1_000_000));
436 let global_update = progress.as_ref().is_some_and(|p| p.force_update);
437 if let Some(ref mut p) = progress {
438 p.display_throughput(byte_count, global_update)?;
439 }
440 } else if line == "update" {
441 if let Some(ref mut p) = progress {
442 p.force_update = true;
443 }
444 } else if line == "stop" {
445 if let Some(mut p) = progress.take() {
446 p.stop(trace_path.as_deref())?;
447 }
448 } else {
449 return Err(io::Error::new(
450 io::ErrorKind::InvalidInput,
451 format!("invalid input: '{line}'"),
452 ));
453 }
454 }
455
456 Ok(())
457}