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 Some(tp) = self.throughput.as_mut() else {
220 return Ok(());
221 };
222 tp.curr_total = total;
223
224 if now_ns.saturating_sub(tp.prev_ns) <= 500_000_000 {
225 return Ok(());
226 }
227
228 let misecs: u32 = (((now_ns - tp.prev_ns) as u128 * 4398) >> 32) as u32;
229 let count = total.saturating_sub(tp.prev_total);
230 tp.prev_total = total;
231 tp.prev_ns = now_ns;
232 tp.avg_bytes = tp.avg_bytes.saturating_add(count);
233 tp.avg_misecs = tp.avg_misecs.saturating_add(u64::from(misecs));
234 let rate = if tp.avg_misecs > 0 {
235 (tp.avg_bytes / tp.avg_misecs) as u32
236 } else {
237 0
238 };
239 tp.avg_bytes = tp
240 .avg_bytes
241 .saturating_sub(u64::from(tp.last_bytes[tp.idx]));
242 tp.avg_misecs = tp
243 .avg_misecs
244 .saturating_sub(u64::from(tp.last_misecs[tp.idx]));
245 tp.last_bytes[tp.idx] = count as u32;
246 tp.last_misecs[tp.idx] = misecs;
247 tp.idx = (tp.idx + 1) % TP_IDX_MAX;
248
249 tp.display = throughput_display(total, rate);
250
251 if self.last_value.is_some() && global_update {
252 let n = self.last_value.unwrap_or(0);
253 self.force_update = true;
254 self.display_progress(n)?;
255 }
256 Ok(())
257 }
258
259 fn force_last_update(&mut self, msg: &str) -> io::Result<()> {
260 let now_ns = self.now_ns();
261 if let Some(tp) = self.throughput.as_mut() {
262 let misecs: u32 =
263 (((now_ns.saturating_sub(self.start_ns)) as u128 * 4398) >> 32) as u32;
264 let rate = if misecs > 0 {
265 (tp.curr_total / u64::from(misecs)) as u32
266 } else {
267 0
268 };
269 tp.display = throughput_display(tp.curr_total, rate);
270 }
271 self.force_update = true;
272 let n = self.last_value.unwrap_or(0);
273 let done = format!(", {msg}.\n");
274 self.render_line(n, Some(&done))?;
275 Ok(())
276 }
277
278 fn stop(&mut self, trace_path: Option<&str>) -> io::Result<()> {
279 if self.last_value.is_some() {
280 self.force_last_update("done")?;
281 }
282 if let Some(path) = trace_path {
283 trace2_append_json_line(
284 path,
285 &format!(
286 r#"{{"event":"data","sid":"grit-0","time":"{}","category":"progress","key":"total_objects","value":"{}"}}"#,
287 trace_now(),
288 self.total
289 ),
290 )?;
291 if let Some(tp) = &self.throughput {
292 trace2_append_json_line(
293 path,
294 &format!(
295 r#"{{"event":"data","sid":"grit-0","time":"{}","category":"progress","key":"total_bytes","value":"{}"}}"#,
296 trace_now(),
297 tp.curr_total
298 ),
299 )?;
300 }
301 trace2_append_json_line(
302 path,
303 &format!(
304 r#"{{"event":"region_leave","sid":"grit-0","time":"{}","category":"progress","label":"{}","t_rel":0.0}}"#,
305 trace_now(),
306 json_escape(&self.title)
307 ),
308 )?;
309 }
310 Ok(())
311 }
312}
313
314fn nanotime() -> u64 {
315 use std::time::{SystemTime, UNIX_EPOCH};
316 SystemTime::now()
317 .duration_since(UNIX_EPOCH)
318 .map(|d| d.as_nanos() as u64)
319 .unwrap_or(0)
320}
321
322fn trace_now() -> String {
323 use std::time::{SystemTime, UNIX_EPOCH};
324 let now = SystemTime::now()
325 .duration_since(UNIX_EPOCH)
326 .unwrap_or_default();
327 let total_secs = now.as_secs();
328 let micros = now.subsec_micros();
329 let secs_in_day = total_secs % 86400;
330 let hours = secs_in_day / 3600;
331 let mins = (secs_in_day % 3600) / 60;
332 let secs = secs_in_day % 60;
333 format!("{:02}:{:02}:{:02}.{:06}", hours, mins, secs, micros)
334}
335
336fn json_escape(s: &str) -> String {
337 s.replace('\\', "\\\\").replace('"', "\\\"")
338}
339
340fn trace2_append_json_line(path: &str, line: &str) -> io::Result<()> {
341 let mut file = std::fs::OpenOptions::new()
342 .create(true)
343 .append(true)
344 .open(path)?;
345 writeln!(file, "{line}")
346}
347
348#[cfg(unix)]
349fn is_foreground_stderr(stderr: &io::Stderr) -> bool {
350 use std::os::unix::io::AsRawFd;
351 let fd = stderr.as_raw_fd();
352 unsafe {
354 let tpgrp = libc::tcgetpgrp(fd);
355 if tpgrp < 0 {
356 return true;
357 }
358 libc::getpgid(0) == tpgrp
359 }
360}
361
362#[cfg(not(unix))]
363fn is_foreground_stderr(_stderr: &io::Stderr) -> bool {
364 true
365}
366
367pub fn run() -> io::Result<()> {
372 PROGRESS_TEST_NS.set(0);
373
374 let trace_path = std::env::var("GIT_TRACE2_EVENT")
375 .ok()
376 .filter(|s| !s.is_empty());
377
378 let stdin = io::stdin();
379 let mut progress: Option<Progress> = None;
380 let mut title_storage: Vec<String> = Vec::new();
381
382 for line in stdin.lock().lines() {
383 let line = line?;
384 if let Some(rest) = line.strip_prefix("start ") {
385 let mut parts = rest.splitn(2, |c: char| c.is_ascii_whitespace());
386 let total_str = parts.next().unwrap_or("");
387 let total: u64 = total_str.parse().map_err(|e| {
388 io::Error::new(
389 io::ErrorKind::InvalidInput,
390 format!("invalid start total: {e}"),
391 )
392 })?;
393 let title: String = match parts.next() {
394 None | Some("") => "Working hard".to_string(),
395 Some(t) => {
396 title_storage.push(t.to_string());
397 t.to_string()
398 }
399 };
400 if let Some(path) = trace_path.as_deref() {
401 trace2_append_json_line(
402 path,
403 &format!(
404 r#"{{"event":"region_enter","sid":"grit-0","time":"{}","category":"progress","label":"{}"}}"#,
405 trace_now(),
406 json_escape(&title)
407 ),
408 )?;
409 }
410 progress = Some(Progress::new(title, total));
411 } else if let Some(rest) = line.strip_prefix("progress ") {
412 let n: u64 = rest.trim().parse().map_err(|e| {
413 io::Error::new(
414 io::ErrorKind::InvalidInput,
415 format!("invalid progress value: {e}"),
416 )
417 })?;
418 if let Some(ref mut p) = progress {
419 p.display_progress(n)?;
420 }
421 } else if let Some(rest) = line.strip_prefix("throughput ") {
422 let mut it = rest.split_whitespace();
423 let byte_count: u64 = it
424 .next()
425 .ok_or_else(|| {
426 io::Error::new(io::ErrorKind::InvalidInput, "throughput: missing bytes")
427 })?
428 .parse()
429 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("{e}")))?;
430 let test_ms: u64 = it
431 .next()
432 .ok_or_else(|| {
433 io::Error::new(io::ErrorKind::InvalidInput, "throughput: missing millis")
434 })?
435 .parse()
436 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("{e}")))?;
437 PROGRESS_TEST_NS.set(test_ms.saturating_mul(1_000_000));
438 let global_update = progress.as_ref().is_some_and(|p| p.force_update);
439 if let Some(ref mut p) = progress {
440 p.display_throughput(byte_count, global_update)?;
441 }
442 } else if line == "update" {
443 if let Some(ref mut p) = progress {
444 p.force_update = true;
445 }
446 } else if line == "stop" {
447 if let Some(mut p) = progress.take() {
448 p.stop(trace_path.as_deref())?;
449 }
450 } else {
451 return Err(io::Error::new(
452 io::ErrorKind::InvalidInput,
453 format!("invalid input: '{line}'"),
454 ));
455 }
456 }
457
458 Ok(())
459}