diff options
| author | Gavin Mak <gavinmak@google.com> | 2023-04-27 05:58:57 +0000 |
|---|---|---|
| committer | LUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2023-05-02 20:51:46 +0000 |
| commit | edcaa94ca86d29c1ea106eddac837f4a699379ba (patch) | |
| tree | 7b73163f09f2e1b6bd4100f0545e4e128e4b0cda /progress.py | |
| parent | 7ef5b465cd5a3156f0dc2fcc914c2a92b2bf226a (diff) | |
| download | git-repo-edcaa94ca86d29c1ea106eddac837f4a699379ba.tar.gz | |
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>
Diffstat (limited to 'progress.py')
| -rw-r--r-- | progress.py | 72 |
1 files changed, 64 insertions, 8 deletions
diff --git a/progress.py b/progress.py index d1a7c543..4844eb88 100644 --- a/progress.py +++ b/progress.py | |||
| @@ -14,7 +14,13 @@ | |||
| 14 | 14 | ||
| 15 | import os | 15 | import os |
| 16 | import sys | 16 | import sys |
| 17 | from time import time | 17 | import time |
| 18 | |||
| 19 | try: | ||
| 20 | import threading as _threading | ||
| 21 | except ImportError: | ||
| 22 | import dummy_threading as _threading | ||
| 23 | |||
| 18 | from repo_trace import IsTraceToStderr | 24 | from repo_trace import IsTraceToStderr |
| 19 | 25 | ||
| 20 | _NOT_TTY = not os.isatty(2) | 26 | _NOT_TTY = not os.isatty(2) |
| @@ -30,14 +36,20 @@ CSI_ERASE_LINE = "\x1b[2K" | |||
| 30 | CSI_ERASE_LINE_AFTER = "\x1b[K" | 36 | CSI_ERASE_LINE_AFTER = "\x1b[K" |
| 31 | 37 | ||
| 32 | 38 | ||
| 39 | def convert_to_hms(total): | ||
| 40 | """Converts a period of seconds to hours, minutes, and seconds.""" | ||
| 41 | hours, rem = divmod(total, 3600) | ||
| 42 | mins, secs = divmod(rem, 60) | ||
| 43 | return int(hours), int(mins), secs | ||
| 44 | |||
| 45 | |||
| 33 | def duration_str(total): | 46 | def duration_str(total): |
| 34 | """A less noisy timedelta.__str__. | 47 | """A less noisy timedelta.__str__. |
| 35 | 48 | ||
| 36 | The default timedelta stringification contains a lot of leading zeros and | 49 | The default timedelta stringification contains a lot of leading zeros and |
| 37 | uses microsecond resolution. This makes for noisy output. | 50 | uses microsecond resolution. This makes for noisy output. |
| 38 | """ | 51 | """ |
| 39 | hours, rem = divmod(total, 3600) | 52 | hours, mins, secs = convert_to_hms(total) |
| 40 | mins, secs = divmod(rem, 60) | ||
| 41 | ret = "%.3fs" % (secs,) | 53 | ret = "%.3fs" % (secs,) |
| 42 | if mins: | 54 | if mins: |
| 43 | ret = "%im%s" % (mins, ret) | 55 | ret = "%im%s" % (mins, ret) |
| @@ -46,6 +58,24 @@ def duration_str(total): | |||
| 46 | return ret | 58 | return ret |
| 47 | 59 | ||
| 48 | 60 | ||
| 61 | def elapsed_str(total): | ||
| 62 | """Returns seconds in the format [H:]MM:SS. | ||
| 63 | |||
| 64 | Does not display a leading zero for minutes if under 10 minutes. This should | ||
| 65 | be used when displaying elapsed time in a progress indicator. | ||
| 66 | """ | ||
| 67 | hours, mins, secs = convert_to_hms(total) | ||
| 68 | ret = f"{int(secs):>02d}" | ||
| 69 | if total >= 3600: | ||
| 70 | # Show leading zeroes if over an hour. | ||
| 71 | ret = f"{mins:>02d}:{ret}" | ||
| 72 | else: | ||
| 73 | ret = f"{mins}:{ret}" | ||
| 74 | if hours: | ||
| 75 | ret = f"{hours}:{ret}" | ||
| 76 | return ret | ||
| 77 | |||
| 78 | |||
| 49 | class Progress(object): | 79 | class Progress(object): |
| 50 | def __init__( | 80 | def __init__( |
| 51 | self, | 81 | self, |
| @@ -55,11 +85,12 @@ class Progress(object): | |||
| 55 | print_newline=False, | 85 | print_newline=False, |
| 56 | delay=True, | 86 | delay=True, |
| 57 | quiet=False, | 87 | quiet=False, |
| 88 | show_elapsed=False, | ||
| 58 | ): | 89 | ): |
| 59 | self._title = title | 90 | self._title = title |
| 60 | self._total = total | 91 | self._total = total |
| 61 | self._done = 0 | 92 | self._done = 0 |
| 62 | self._start = time() | 93 | self._start = time.time() |
| 63 | self._show = not delay | 94 | self._show = not delay |
| 64 | self._units = units | 95 | self._units = units |
| 65 | self._print_newline = print_newline | 96 | self._print_newline = print_newline |
| @@ -67,12 +98,30 @@ class Progress(object): | |||
| 67 | self._show_jobs = False | 98 | self._show_jobs = False |
| 68 | self._active = 0 | 99 | self._active = 0 |
| 69 | 100 | ||
| 101 | # Save the last message for displaying on refresh. | ||
| 102 | self._last_msg = None | ||
| 103 | self._show_elapsed = show_elapsed | ||
| 104 | self._update_event = _threading.Event() | ||
| 105 | self._update_thread = _threading.Thread( | ||
| 106 | target=self._update_loop, | ||
| 107 | ) | ||
| 108 | self._update_thread.daemon = True | ||
| 109 | |||
| 70 | # When quiet, never show any output. It's a bit hacky, but reusing the | 110 | # When quiet, never show any output. It's a bit hacky, but reusing the |
| 71 | # existing logic that delays initial output keeps the rest of the class | 111 | # existing logic that delays initial output keeps the rest of the class |
| 72 | # clean. Basically we set the start time to years in the future. | 112 | # clean. Basically we set the start time to years in the future. |
| 73 | if quiet: | 113 | if quiet: |
| 74 | self._show = False | 114 | self._show = False |
| 75 | self._start += 2**32 | 115 | self._start += 2**32 |
| 116 | elif show_elapsed: | ||
| 117 | self._update_thread.start() | ||
| 118 | |||
| 119 | def _update_loop(self): | ||
| 120 | while True: | ||
| 121 | if self._update_event.is_set(): | ||
| 122 | return | ||
| 123 | self.update(inc=0, msg=self._last_msg) | ||
| 124 | time.sleep(1) | ||
| 76 | 125 | ||
| 77 | def start(self, name): | 126 | def start(self, name): |
| 78 | self._active += 1 | 127 | self._active += 1 |
| @@ -86,12 +135,14 @@ class Progress(object): | |||
| 86 | 135 | ||
| 87 | def update(self, inc=1, msg=""): | 136 | def update(self, inc=1, msg=""): |
| 88 | self._done += inc | 137 | self._done += inc |
| 138 | self._last_msg = msg | ||
| 89 | 139 | ||
| 90 | if _NOT_TTY or IsTraceToStderr(): | 140 | if _NOT_TTY or IsTraceToStderr(): |
| 91 | return | 141 | return |
| 92 | 142 | ||
| 143 | elapsed_sec = time.time() - self._start | ||
| 93 | if not self._show: | 144 | if not self._show: |
| 94 | if 0.5 <= time() - self._start: | 145 | if 0.5 <= elapsed_sec: |
| 95 | self._show = True | 146 | self._show = True |
| 96 | else: | 147 | else: |
| 97 | return | 148 | return |
| @@ -110,8 +161,12 @@ class Progress(object): | |||
| 110 | ) | 161 | ) |
| 111 | else: | 162 | else: |
| 112 | jobs = "" | 163 | jobs = "" |
| 164 | if self._show_elapsed: | ||
| 165 | elapsed = f" {elapsed_str(elapsed_sec)} |" | ||
| 166 | else: | ||
| 167 | elapsed = "" | ||
| 113 | sys.stderr.write( | 168 | sys.stderr.write( |
| 114 | "\r%s: %2d%% %s(%d%s/%d%s)%s%s%s%s" | 169 | "\r%s: %2d%% %s(%d%s/%d%s)%s %s%s%s" |
| 115 | % ( | 170 | % ( |
| 116 | self._title, | 171 | self._title, |
| 117 | p, | 172 | p, |
| @@ -120,7 +175,7 @@ class Progress(object): | |||
| 120 | self._units, | 175 | self._units, |
| 121 | self._total, | 176 | self._total, |
| 122 | self._units, | 177 | self._units, |
| 123 | " " if msg else "", | 178 | elapsed, |
| 124 | msg, | 179 | msg, |
| 125 | CSI_ERASE_LINE_AFTER, | 180 | CSI_ERASE_LINE_AFTER, |
| 126 | "\n" if self._print_newline else "", | 181 | "\n" if self._print_newline else "", |
| @@ -129,10 +184,11 @@ class Progress(object): | |||
| 129 | sys.stderr.flush() | 184 | sys.stderr.flush() |
| 130 | 185 | ||
| 131 | def end(self): | 186 | def end(self): |
| 187 | self._update_event.set() | ||
| 132 | if _NOT_TTY or IsTraceToStderr() or not self._show: | 188 | if _NOT_TTY or IsTraceToStderr() or not self._show: |
| 133 | return | 189 | return |
| 134 | 190 | ||
| 135 | duration = duration_str(time() - self._start) | 191 | duration = duration_str(time.time() - self._start) |
| 136 | if self._total <= 0: | 192 | if self._total <= 0: |
| 137 | sys.stderr.write( | 193 | sys.stderr.write( |
| 138 | "\r%s: %d, done in %s%s\n" | 194 | "\r%s: %d, done in %s%s\n" |
