sync: Display total elapsed fetch time
Give users an indication that `repo sync` isn't stuck if taking a long
time to fetch.
Bug: https://crbug.com/gerrit/11293
Change-Id: Iccdaec918f86c9cc2db5dc12f9e3eef7ad0bcbda
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/371414
Tested-by: Gavin Mak <gavinmak@google.com>
Reviewed-by: Josip Sokcevic <sokcevic@google.com>
Reviewed-by: Joanna Wang <jojwang@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
diff --git a/progress.py b/progress.py
index d1a7c54..4844eb8 100644
--- a/progress.py
+++ b/progress.py
@@ -14,7 +14,13 @@
import os
import sys
-from time import time
+import time
+
+try:
+ import threading as _threading
+except ImportError:
+ import dummy_threading as _threading
+
from repo_trace import IsTraceToStderr
_NOT_TTY = not os.isatty(2)
@@ -30,14 +36,20 @@
CSI_ERASE_LINE_AFTER = "\x1b[K"
+def convert_to_hms(total):
+ """Converts a period of seconds to hours, minutes, and seconds."""
+ hours, rem = divmod(total, 3600)
+ mins, secs = divmod(rem, 60)
+ return int(hours), int(mins), secs
+
+
def duration_str(total):
"""A less noisy timedelta.__str__.
The default timedelta stringification contains a lot of leading zeros and
uses microsecond resolution. This makes for noisy output.
"""
- hours, rem = divmod(total, 3600)
- mins, secs = divmod(rem, 60)
+ hours, mins, secs = convert_to_hms(total)
ret = "%.3fs" % (secs,)
if mins:
ret = "%im%s" % (mins, ret)
@@ -46,6 +58,24 @@
return ret
+def elapsed_str(total):
+ """Returns seconds in the format [H:]MM:SS.
+
+ Does not display a leading zero for minutes if under 10 minutes. This should
+ be used when displaying elapsed time in a progress indicator.
+ """
+ hours, mins, secs = convert_to_hms(total)
+ ret = f"{int(secs):>02d}"
+ if total >= 3600:
+ # Show leading zeroes if over an hour.
+ ret = f"{mins:>02d}:{ret}"
+ else:
+ ret = f"{mins}:{ret}"
+ if hours:
+ ret = f"{hours}:{ret}"
+ return ret
+
+
class Progress(object):
def __init__(
self,
@@ -55,11 +85,12 @@
print_newline=False,
delay=True,
quiet=False,
+ show_elapsed=False,
):
self._title = title
self._total = total
self._done = 0
- self._start = time()
+ self._start = time.time()
self._show = not delay
self._units = units
self._print_newline = print_newline
@@ -67,12 +98,30 @@
self._show_jobs = False
self._active = 0
+ # Save the last message for displaying on refresh.
+ self._last_msg = None
+ self._show_elapsed = show_elapsed
+ self._update_event = _threading.Event()
+ self._update_thread = _threading.Thread(
+ target=self._update_loop,
+ )
+ self._update_thread.daemon = True
+
# When quiet, never show any output. It's a bit hacky, but reusing the
# existing logic that delays initial output keeps the rest of the class
# clean. Basically we set the start time to years in the future.
if quiet:
self._show = False
self._start += 2**32
+ elif show_elapsed:
+ self._update_thread.start()
+
+ def _update_loop(self):
+ while True:
+ if self._update_event.is_set():
+ return
+ self.update(inc=0, msg=self._last_msg)
+ time.sleep(1)
def start(self, name):
self._active += 1
@@ -86,12 +135,14 @@
def update(self, inc=1, msg=""):
self._done += inc
+ self._last_msg = msg
if _NOT_TTY or IsTraceToStderr():
return
+ elapsed_sec = time.time() - self._start
if not self._show:
- if 0.5 <= time() - self._start:
+ if 0.5 <= elapsed_sec:
self._show = True
else:
return
@@ -110,8 +161,12 @@
)
else:
jobs = ""
+ if self._show_elapsed:
+ elapsed = f" {elapsed_str(elapsed_sec)} |"
+ else:
+ elapsed = ""
sys.stderr.write(
- "\r%s: %2d%% %s(%d%s/%d%s)%s%s%s%s"
+ "\r%s: %2d%% %s(%d%s/%d%s)%s %s%s%s"
% (
self._title,
p,
@@ -120,7 +175,7 @@
self._units,
self._total,
self._units,
- " " if msg else "",
+ elapsed,
msg,
CSI_ERASE_LINE_AFTER,
"\n" if self._print_newline else "",
@@ -129,10 +184,11 @@
sys.stderr.flush()
def end(self):
+ self._update_event.set()
if _NOT_TTY or IsTraceToStderr() or not self._show:
return
- duration = duration_str(time() - self._start)
+ duration = duration_str(time.time() - self._start)
if self._total <= 0:
sys.stderr.write(
"\r%s: %d, done in %s%s\n"