diff options
-rw-r--r-- | progress.py | 72 | ||||
-rw-r--r-- | subcmds/sync.py | 8 |
2 files changed, 71 insertions, 9 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" |
diff --git a/subcmds/sync.py b/subcmds/sync.py index eabaa68b..324f15b5 100644 --- a/subcmds/sync.py +++ b/subcmds/sync.py | |||
@@ -675,7 +675,13 @@ later is required to fix a server side protocol bug. | |||
675 | jobs = opt.jobs_network | 675 | jobs = opt.jobs_network |
676 | fetched = set() | 676 | fetched = set() |
677 | remote_fetched = set() | 677 | remote_fetched = set() |
678 | pm = Progress("Fetching", len(projects), delay=False, quiet=opt.quiet) | 678 | pm = Progress( |
679 | "Fetching", | ||
680 | len(projects), | ||
681 | delay=False, | ||
682 | quiet=opt.quiet, | ||
683 | show_elapsed=True, | ||
684 | ) | ||
679 | 685 | ||
680 | objdir_project_map = dict() | 686 | objdir_project_map = dict() |
681 | for project in projects: | 687 | for project in projects: |