1
1
# Copyright (C) 2005 Aaron Bentley <aaron.bentley@utoronto.ca>
2
# Copyright (C) 2005, 2006 Canonical <canonical.com>
2
# Copyright (C) 2005 Canonical <canonical.com>
4
4
# This program is free software; you can redistribute it and/or modify
5
5
# it under the terms of the GNU General Public License as published by
42
from collections import deque
45
import bzrlib.errors as errors
46
from bzrlib.trace import mutter
45
"""Return estimated terminal width.
47
TODO: Do something smart on Windows?
49
TODO: Is there anything that gets a better update when the window
50
is resized while the program is running?
53
return int(os.environ['COLUMNS'])
54
except (IndexError, KeyError, ValueError):
49
58
def _supports_progress(f):
61
def ProgressBar(to_file=None, **kwargs):
70
def ProgressBar(to_file=sys.stderr, **kwargs):
62
71
"""Abstract factory"""
65
72
if _supports_progress(to_file):
66
73
return TTYProgressBar(to_file=to_file, **kwargs)
68
75
return DotsProgressBar(to_file=to_file, **kwargs)
71
class ProgressBarStack(object):
72
"""A stack of progress bars."""
81
to_messages_file=None,
83
"""Setup the stack with the parameters the progress bars should have."""
86
if to_messages_file is None:
87
to_messages_file = sys.stdout
88
self._to_file = to_file
89
self._show_pct = show_pct
90
self._show_spinner = show_spinner
91
self._show_eta = show_eta
92
self._show_bar = show_bar
93
self._show_count = show_count
94
self._to_messages_file = to_messages_file
96
self._klass = klass or TTYProgressBar
99
if len(self._stack) != 0:
100
return self._stack[-1]
105
if len(self._stack) != 0:
106
return self._stack[0]
110
def get_nested(self):
111
"""Return a nested progress bar."""
112
if len(self._stack) == 0:
115
func = self.top().child_progress
116
new_bar = func(to_file=self._to_file,
117
show_pct=self._show_pct,
118
show_spinner=self._show_spinner,
119
show_eta=self._show_eta,
120
show_bar=self._show_bar,
121
show_count=self._show_count,
122
to_messages_file=self._to_messages_file,
124
self._stack.append(new_bar)
127
def return_pb(self, bar):
128
"""Return bar after its been used."""
129
if bar is not self._stack[-1]:
130
raise errors.MissingProgressBarFinish()
134
78
class _BaseProgressBar(object):
136
79
def __init__(self,
139
82
show_spinner=False,
143
to_messages_file=None,
145
86
object.__init__(self)
148
if to_messages_file is None:
149
to_messages_file = sys.stdout
150
87
self.to_file = to_file
151
self.to_messages_file = to_messages_file
152
89
self.last_msg = None
153
90
self.last_cnt = None
154
91
self.last_total = None
157
94
self.show_eta = show_eta
158
95
self.show_bar = show_bar
159
96
self.show_count = show_count
162
self.MIN_PAUSE = 0.1 # seconds
165
self.start_time = now
166
# next update should not throttle
167
self.last_update = now - self.MIN_PAUSE - 1
170
"""Return this bar to its progress stack."""
172
assert self._stack is not None
173
self._stack.return_pb(self)
175
def note(self, fmt_string, *args, **kwargs):
176
"""Record a note without disrupting the progress bar."""
178
self.to_messages_file.write(fmt_string % args)
179
self.to_messages_file.write('\n')
181
def child_progress(self, **kwargs):
182
return ChildProgress(**kwargs)
185
100
class DummyProgress(_BaseProgressBar):
193
108
def update(self, msg=None, current=None, total=None):
196
def child_update(self, message, current, total):
202
def note(self, fmt_string, *args, **kwargs):
203
"""See _BaseProgressBar.note()."""
205
def child_progress(self, **kwargs):
206
return DummyProgress(**kwargs)
209
115
class DotsProgressBar(_BaseProgressBar):
211
116
def __init__(self, **kwargs):
212
117
_BaseProgressBar.__init__(self, **kwargs)
213
118
self.last_msg = None
255
157
The output file should be in line-buffered or unbuffered mode.
257
159
SPIN_CHARS = r'/-\|'
160
MIN_PAUSE = 0.1 # seconds
260
163
def __init__(self, **kwargs):
261
from bzrlib.osutils import terminal_width
262
164
_BaseProgressBar.__init__(self, **kwargs)
263
165
self.spin_pos = 0
264
self.width = terminal_width()
166
self.width = _width()
265
167
self.start_time = None
266
self.last_updates = deque()
267
self.child_fraction = 0
168
self.last_update = None
270
171
def throttle(self):
271
172
"""Return True if the bar was updated too recently"""
272
# time.time consistently takes 40/4000 ms = 0.01 ms.
273
# but every single update to the pb invokes it.
274
# so we use time.clock which takes 20/4000 ms = 0.005ms
275
# on the downside, time.clock() appears to have approximately
276
# 10ms granularity, so we treat a zero-time change as 'throttled.'
279
interval = now - self.last_update
281
if interval < self.MIN_PAUSE:
174
if self.start_time is None:
175
self.start_time = self.last_update = now
178
interval = now - self.last_update
179
if interval > 0 and interval < self.MIN_PAUSE:
284
self.last_updates.append(now - self.last_update)
285
182
self.last_update = now
290
self.update(self.last_msg, self.last_cnt, self.last_total,
293
def child_update(self, message, current, total):
294
if current is not None and total != 0:
295
child_fraction = float(current) / total
296
if self.last_cnt is None:
298
elif self.last_cnt + child_fraction <= self.last_total:
299
self.child_fraction = child_fraction
301
mutter('not updating child fraction')
302
if self.last_msg is None:
307
def update(self, msg, current_cnt=None, total_cnt=None,
187
self.update(self.last_msg, self.last_cnt, self.last_total)
191
def update(self, msg, current_cnt=None, total_cnt=None):
309
192
"""Update and redraw progress bar."""
311
194
if current_cnt < 0:
314
197
if current_cnt > total_cnt:
315
198
total_cnt = current_cnt
317
## # optional corner case optimisation
318
## # currently does not seem to fire so costs more than saved.
319
## # trivial optimal case:
320
## # NB if callers are doing a clear and restore with
321
## # the saved values, this will prevent that:
322
## # in that case add a restore method that calls
323
## # _do_update or some such
324
## if (self.last_msg == msg and
325
## self.last_cnt == current_cnt and
326
## self.last_total == total_cnt and
327
## self.child_fraction == child_fraction):
330
old_msg = self.last_msg
331
200
# save these for the tick() function
332
201
self.last_msg = msg
333
202
self.last_cnt = current_cnt
334
203
self.last_total = total_cnt
335
self.child_fraction = child_fraction
337
# each function call takes 20ms/4000 = 0.005 ms,
338
# but multiple that by 4000 calls -> starts to cost.
339
# so anything to make this function call faster
340
# will improve base 'diff' time by up to 0.1 seconds.
341
if old_msg == self.last_msg and self.throttle():
344
if self.show_eta and self.start_time and self.last_total:
345
eta = get_eta(self.start_time, self.last_cnt + self.child_fraction,
346
self.last_total, last_updates = self.last_updates)
208
if self.show_eta and self.start_time and total_cnt:
209
eta = get_eta(self.start_time, current_cnt, total_cnt)
347
210
eta_str = " " + str_tdelta(eta)
356
219
# always update this; it's also used for the bar
357
220
self.spin_pos += 1
359
if self.show_pct and self.last_total and self.last_cnt:
360
pct = 100.0 * ((self.last_cnt + self.child_fraction) / self.last_total)
222
if self.show_pct and total_cnt and current_cnt:
223
pct = 100.0 * current_cnt / total_cnt
361
224
pct_str = ' (%5.1f%%)' % pct
365
228
if not self.show_count:
367
elif self.last_cnt is None:
230
elif current_cnt is None:
369
elif self.last_total is None:
370
count_str = ' %i' % (self.last_cnt)
232
elif total_cnt is None:
233
count_str = ' %i' % (current_cnt)
372
235
# make both fields the same size
373
t = '%i' % (self.last_total)
374
c = '%*i' % (len(t), self.last_cnt)
236
t = '%i' % (total_cnt)
237
c = '%*i' % (len(t), current_cnt)
375
238
count_str = ' ' + c + '/' + t
377
240
if self.show_bar:
378
241
# progress bar, if present, soaks up all remaining space
379
cols = self.width - 1 - len(self.last_msg) - len(spin_str) - len(pct_str) \
242
cols = self.width - 1 - len(msg) - len(spin_str) - len(pct_str) \
380
243
- len(eta_str) - len(count_str) - 3
383
246
# number of markers highlighted in bar
384
markers = int(round(float(cols) *
385
(self.last_cnt + self.child_fraction) / self.last_total))
247
markers = int(round(float(cols) * current_cnt / total_cnt))
386
248
bar_str = '[' + ('=' * markers).ljust(cols) + '] '
388
250
# don't know total, so can't show completion.
399
m = spin_str + bar_str + self.last_msg + count_str + pct_str + eta_str
261
m = spin_str + bar_str + msg + count_str + pct_str + eta_str
401
263
assert len(m) < self.width
402
264
self.to_file.write('\r' + m.ljust(self.width - 1))
403
265
#self.to_file.flush()
406
269
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
407
270
#self.to_file.flush()
410
class ChildProgress(_BaseProgressBar):
411
"""A progress indicator that pushes its data to the parent"""
413
def __init__(self, _stack, **kwargs):
414
_BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
415
self.parent = _stack.top()
418
self.child_fraction = 0
421
def update(self, msg, current_cnt=None, total_cnt=None):
422
self.current = current_cnt
423
self.total = total_cnt
425
self.child_fraction = 0
428
def child_update(self, message, current, total):
429
if current is None or total == 0:
430
self.child_fraction = 0
432
self.child_fraction = float(current) / total
436
if self.current is None:
439
count = self.current+self.child_fraction
440
if count > self.total:
442
mutter('clamping count of %d to %d' % (count, self.total))
444
self.parent.child_update(self.message, count, self.total)
449
def note(self, *args, **kwargs):
450
self.parent.note(*args, **kwargs)
453
274
def str_tdelta(delt):
462
def get_eta(start_time, current, total, enough_samples=3, last_updates=None, n_recent=10):
283
def get_eta(start_time, current, total, enough_samples=3):
463
284
if start_time is None:
482
303
assert total_duration >= elapsed
484
if last_updates and len(last_updates) >= n_recent:
485
while len(last_updates) > n_recent:
486
last_updates.popleft()
487
avg = sum(last_updates) / float(len(last_updates))
488
time_left = avg * (total - current)
490
old_time_left = total_duration - elapsed
492
# We could return the average, or some other value here
493
return (time_left + old_time_left) / 2
495
305
return total_duration - elapsed
498
class ProgressPhase(object):
499
"""Update progress object with the current phase"""
500
def __init__(self, message, total, pb):
501
object.__init__(self)
503
self.message = message
505
self.cur_phase = None
507
def next_phase(self):
508
if self.cur_phase is None:
512
assert self.cur_phase < self.total
513
self.pb.update(self.message, self.cur_phase, self.total)
518
310
result = doctest.testmod()