1
# Copyright (C) 2005, 2006, 2008, 2009 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
"""Progress indicators.
20
The usual way to use this is via bzrlib.ui.ui_factory.nested_progress_bar which
21
will manage a conceptual stack of nested activities.
37
from bzrlib.trace import mutter
40
def _supports_progress(f):
41
"""Detect if we can use pretty progress bars on the output stream f.
43
If this returns true we expect that a human may be looking at that
44
output, and that we can repaint a line to update it.
46
isatty = getattr(f, 'isatty', None)
51
if os.environ.get('TERM') == 'dumb':
52
# e.g. emacs compile window
57
class ProgressTask(object):
58
"""Model component of a progress indicator.
60
Most code that needs to indicate progress should update one of these,
61
and it will in turn update the display, if one is present.
63
Code updating the task may also set fields as hints about how to display
64
it: show_pct, show_spinner, show_eta, show_count, show_bar. UIs
65
will not necessarily respect all these fields.
68
def __init__(self, parent_task=None, ui_factory=None):
69
self._parent_task = parent_task
72
self.current_cnt = None
74
self.ui_factory = ui_factory
76
self.show_spinner = True
77
self.show_eta = False,
78
self.show_count = True
82
return '%s(%r/%r, msg=%r)' % (
83
self.__class__.__name__,
88
def update(self, msg, current_cnt=None, total_cnt=None):
90
self.current_cnt = current_cnt
92
self.total_cnt = total_cnt
93
self.ui_factory._progress_updated(self)
99
self.ui_factory._progress_finished(self)
101
def make_sub_task(self):
102
return ProgressTask(self, self.ui_factory)
104
def _overall_completion_fraction(self, child_fraction=0.0):
105
"""Return fractional completion of this task and its parents
107
Returns None if no completion can be computed."""
109
own_fraction = (float(self.current_cnt) + child_fraction) / self.total_cnt
112
if self._parent_task is None:
115
if own_fraction is None:
117
return self._parent_task._overall_completion_fraction(own_fraction)
119
def note(self, fmt_string, *args):
120
"""Record a note without disrupting the progress bar."""
121
# XXX: shouldn't be here; put it in mutter or the ui instead
123
self.ui_factory.note(fmt_string % args)
125
self.ui_factory.note(fmt_string)
128
# XXX: shouldn't be here; put it in mutter or the ui instead
129
self.ui_factory.clear_term()
132
def ProgressBar(to_file=None, **kwargs):
133
"""Abstract factory"""
136
requested_bar_type = os.environ.get('BZR_PROGRESS_BAR')
137
# An value of '' or not set reverts to standard processing
138
if requested_bar_type in (None, ''):
139
if _supports_progress(to_file):
140
return TTYProgressBar(to_file=to_file, **kwargs)
142
return DummyProgress(to_file=to_file, **kwargs)
144
# Minor sanitation to prevent spurious errors
145
requested_bar_type = requested_bar_type.lower().strip()
146
# TODO: jam 20060710 Arguably we shouldn't raise an exception
147
# but should instead just disable progress bars if we
148
# don't recognize the type
149
if requested_bar_type not in _progress_bar_types:
150
raise errors.InvalidProgressBarType(requested_bar_type,
151
_progress_bar_types.keys())
152
return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
155
class ProgressBarStack(object):
156
"""A stack of progress bars."""
165
to_messages_file=None,
167
"""Setup the stack with the parameters the progress bars should have."""
170
if to_messages_file is None:
171
to_messages_file = sys.stdout
172
self._to_file = to_file
173
self._show_pct = show_pct
174
self._show_spinner = show_spinner
175
self._show_eta = show_eta
176
self._show_bar = show_bar
177
self._show_count = show_count
178
self._to_messages_file = to_messages_file
180
self._klass = klass or ProgressBar
183
if len(self._stack) != 0:
184
return self._stack[-1]
189
if len(self._stack) != 0:
190
return self._stack[0]
194
def get_nested(self):
195
"""Return a nested progress bar."""
196
if len(self._stack) == 0:
199
func = self.top().child_progress
200
new_bar = func(to_file=self._to_file,
201
show_pct=self._show_pct,
202
show_spinner=self._show_spinner,
203
show_eta=self._show_eta,
204
show_bar=self._show_bar,
205
show_count=self._show_count,
206
to_messages_file=self._to_messages_file,
208
self._stack.append(new_bar)
211
def return_pb(self, bar):
212
"""Return bar after its been used."""
213
if bar is not self._stack[-1]:
214
warnings.warn("%r is not currently active" % (bar,))
219
class _BaseProgressBar(object):
228
to_messages_file=None,
230
object.__init__(self)
233
if to_messages_file is None:
234
to_messages_file = sys.stdout
235
self.to_file = to_file
236
self.to_messages_file = to_messages_file
239
self.last_total = None
240
self.show_pct = show_pct
241
self.show_spinner = show_spinner
242
self.show_eta = show_eta
243
self.show_bar = show_bar
244
self.show_count = show_count
247
self.MIN_PAUSE = 0.1 # seconds
250
self.start_time = now
251
# next update should not throttle
252
self.last_update = now - self.MIN_PAUSE - 1
255
"""Return this bar to its progress stack."""
257
self._stack.return_pb(self)
259
def note(self, fmt_string, *args, **kwargs):
260
"""Record a note without disrupting the progress bar."""
262
self.to_messages_file.write(fmt_string % args)
263
self.to_messages_file.write('\n')
265
def child_progress(self, **kwargs):
266
return ChildProgress(**kwargs)
269
class DummyProgress(_BaseProgressBar):
270
"""Progress-bar standin that does nothing.
272
This can be used as the default argument for methods that
273
take an optional progress indicator."""
278
def update(self, msg=None, current=None, total=None):
281
def child_update(self, message, current, total):
287
def note(self, fmt_string, *args, **kwargs):
288
"""See _BaseProgressBar.note()."""
290
def child_progress(self, **kwargs):
291
return DummyProgress(**kwargs)
294
class DotsProgressBar(_BaseProgressBar):
296
def __init__(self, **kwargs):
297
_BaseProgressBar.__init__(self, **kwargs)
304
def update(self, msg=None, current_cnt=None, total_cnt=None):
305
if msg and msg != self.last_msg:
307
self.to_file.write('\n')
308
self.to_file.write(msg + ': ')
311
self.to_file.write('.')
315
self.to_file.write('\n')
318
def child_update(self, message, current, total):
324
class TTYProgressBar(_BaseProgressBar):
325
"""Progress bar display object.
327
Several options are available to control the display. These can
328
be passed as parameters to the constructor or assigned at any time:
331
Show percentage complete.
333
Show rotating baton. This ticks over on every update even
334
if the values don't change.
336
Show predicted time-to-completion.
340
Show numerical counts.
342
The output file should be in line-buffered or unbuffered mode.
347
def __init__(self, **kwargs):
348
from bzrlib.osutils import terminal_width
349
_BaseProgressBar.__init__(self, **kwargs)
351
self.width = terminal_width()
352
self.last_updates = []
353
self._max_last_updates = 10
354
self.child_fraction = 0
355
self._have_output = False
357
def throttle(self, old_msg):
358
"""Return True if the bar was updated too recently"""
359
# time.time consistently takes 40/4000 ms = 0.01 ms.
360
# time.clock() is faster, but gives us CPU time, not wall-clock time
362
if self.start_time is not None and (now - self.start_time) < 1:
364
if old_msg != self.last_msg:
366
interval = now - self.last_update
368
if interval < self.MIN_PAUSE:
371
self.last_updates.append(now - self.last_update)
372
# Don't let the queue grow without bound
373
self.last_updates = self.last_updates[-self._max_last_updates:]
374
self.last_update = now
378
self.update(self.last_msg, self.last_cnt, self.last_total,
381
def child_update(self, message, current, total):
382
if current is not None and total != 0:
383
child_fraction = float(current) / total
384
if self.last_cnt is None:
386
elif self.last_cnt + child_fraction <= self.last_total:
387
self.child_fraction = child_fraction
388
if self.last_msg is None:
392
def update(self, msg, current_cnt=None, total_cnt=None,
394
"""Update and redraw progress bar.
399
if total_cnt is None:
400
total_cnt = self.last_total
405
if current_cnt > total_cnt:
406
total_cnt = current_cnt
408
## # optional corner case optimisation
409
## # currently does not seem to fire so costs more than saved.
410
## # trivial optimal case:
411
## # NB if callers are doing a clear and restore with
412
## # the saved values, this will prevent that:
413
## # in that case add a restore method that calls
414
## # _do_update or some such
415
## if (self.last_msg == msg and
416
## self.last_cnt == current_cnt and
417
## self.last_total == total_cnt and
418
## self.child_fraction == child_fraction):
424
old_msg = self.last_msg
425
# save these for the tick() function
427
self.last_cnt = current_cnt
428
self.last_total = total_cnt
429
self.child_fraction = child_fraction
431
# each function call takes 20ms/4000 = 0.005 ms,
432
# but multiple that by 4000 calls -> starts to cost.
433
# so anything to make this function call faster
434
# will improve base 'diff' time by up to 0.1 seconds.
435
if self.throttle(old_msg):
438
if self.show_eta and self.start_time and self.last_total:
439
eta = get_eta(self.start_time, self.last_cnt + self.child_fraction,
440
self.last_total, last_updates = self.last_updates)
441
eta_str = " " + str_tdelta(eta)
445
if self.show_spinner:
446
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
450
# always update this; it's also used for the bar
453
if self.show_pct and self.last_total and self.last_cnt:
454
pct = 100.0 * ((self.last_cnt + self.child_fraction) / self.last_total)
455
pct_str = ' (%5.1f%%)' % pct
459
if not self.show_count:
461
elif self.last_cnt is None:
463
elif self.last_total is None:
464
count_str = ' %i' % (self.last_cnt)
466
# make both fields the same size
467
t = '%i' % (self.last_total)
468
c = '%*i' % (len(t), self.last_cnt)
469
count_str = ' ' + c + '/' + t
472
# progress bar, if present, soaks up all remaining space
473
cols = self.width - 1 - len(self.last_msg) - len(spin_str) - len(pct_str) \
474
- len(eta_str) - len(count_str) - 3
477
# number of markers highlighted in bar
478
markers = int(round(float(cols) *
479
(self.last_cnt + self.child_fraction) / self.last_total))
480
bar_str = '[' + ('=' * markers).ljust(cols) + '] '
482
# don't know total, so can't show completion.
483
# so just show an expanded spinning thingy
484
m = self.spin_pos % cols
485
ms = (' ' * m + '*').ljust(cols)
487
bar_str = '[' + ms + '] '
493
m = spin_str + bar_str + self.last_msg + count_str \
495
self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
496
self._have_output = True
497
#self.to_file.flush()
500
if self._have_output:
501
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
502
self._have_output = False
503
#self.to_file.flush()
508
class ChildProgress(_BaseProgressBar):
509
"""A progress indicator that pushes its data to the parent"""
511
def __init__(self, _stack, **kwargs):
512
_BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
513
self.parent = _stack.top()
516
self.child_fraction = 0
519
def update(self, msg, current_cnt=None, total_cnt=None):
520
self.current = current_cnt
521
if total_cnt is not None:
522
self.total = total_cnt
524
self.child_fraction = 0
527
def child_update(self, message, current, total):
528
if current is None or total == 0:
529
self.child_fraction = 0
531
self.child_fraction = float(current) / total
535
if self.current is None:
538
count = self.current+self.child_fraction
539
if count > self.total:
541
mutter('clamping count of %d to %d' % (count, self.total))
543
self.parent.child_update(self.message, count, self.total)
548
def note(self, *args, **kwargs):
549
self.parent.note(*args, **kwargs)
552
class InstrumentedProgress(TTYProgressBar):
553
"""TTYProgress variant that tracks outcomes"""
555
def __init__(self, *args, **kwargs):
556
self.always_throttled = True
557
self.never_throttle = False
558
TTYProgressBar.__init__(self, *args, **kwargs)
560
def throttle(self, old_message):
561
if self.never_throttle:
564
result = TTYProgressBar.throttle(self, old_message)
566
self.always_throttled = False
569
def str_tdelta(delt):
572
delt = int(round(delt))
573
return '%d:%02d:%02d' % (delt/3600,
578
def get_eta(start_time, current, total, enough_samples=3, last_updates=None, n_recent=10):
579
if start_time is None:
585
if current < enough_samples:
591
elapsed = time.time() - start_time
593
if elapsed < 2.0: # not enough time to estimate
596
total_duration = float(elapsed) * float(total) / float(current)
598
if last_updates and len(last_updates) >= n_recent:
599
avg = sum(last_updates) / float(len(last_updates))
600
time_left = avg * (total - current)
602
old_time_left = total_duration - elapsed
604
# We could return the average, or some other value here
605
return (time_left + old_time_left) / 2
607
return total_duration - elapsed
610
class ProgressPhase(object):
611
"""Update progress object with the current phase"""
612
def __init__(self, message, total, pb):
613
object.__init__(self)
615
self.message = message
617
self.cur_phase = None
619
def next_phase(self):
620
if self.cur_phase is None:
624
self.pb.update(self.message, self.cur_phase, self.total)
627
_progress_bar_types = {}
628
_progress_bar_types['dummy'] = DummyProgress
629
_progress_bar_types['none'] = DummyProgress
630
_progress_bar_types['tty'] = TTYProgressBar
631
_progress_bar_types['dots'] = DotsProgressBar