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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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.
33
from bzrlib.trace import mutter
34
from bzrlib.symbol_versioning import (
40
# XXX: deprecated; can be removed when the ProgressBar factory is removed
41
def _supports_progress(f):
42
"""Detect if we can use pretty progress bars on the output stream f.
44
If this returns true we expect that a human may be looking at that
45
output, and that we can repaint a line to update it.
47
isatty = getattr(f, 'isatty', None)
52
if os.environ.get('TERM') == 'dumb':
53
# e.g. emacs compile window
58
class ProgressTask(object):
59
"""Model component of a progress indicator.
61
Most code that needs to indicate progress should update one of these,
62
and it will in turn update the display, if one is present.
64
Code updating the task may also set fields as hints about how to display
65
it: show_pct, show_spinner, show_eta, show_count, show_bar. UIs
66
will not necessarily respect all these fields.
69
def __init__(self, parent_task=None, ui_factory=None):
70
"""Construct a new progress task.
72
Normally you should not call this directly but rather through
73
`ui_factory.nested_progress_bar`.
75
self._parent_task = parent_task
78
self.current_cnt = None
80
self.ui_factory = ui_factory
82
self.show_spinner = True
83
self.show_eta = False,
84
self.show_count = True
88
return '%s(%r/%r, msg=%r)' % (
89
self.__class__.__name__,
94
def update(self, msg, current_cnt=None, total_cnt=None):
96
self.current_cnt = current_cnt
98
self.total_cnt = total_cnt
99
self.ui_factory._progress_updated(self)
102
self.update(self.msg)
105
self.ui_factory._progress_finished(self)
107
def make_sub_task(self):
108
return ProgressTask(self, self.ui_factory)
110
def _overall_completion_fraction(self, child_fraction=0.0):
111
"""Return fractional completion of this task and its parents
113
Returns None if no completion can be computed."""
114
if self.current_cnt is not None and self.total_cnt:
115
own_fraction = (float(self.current_cnt) + child_fraction) / self.total_cnt
117
# if this task has no estimation, it just passes on directly
118
# whatever the child has measured...
119
own_fraction = child_fraction
120
if self._parent_task is None:
123
if own_fraction is None:
125
return self._parent_task._overall_completion_fraction(own_fraction)
127
def note(self, fmt_string, *args):
128
"""Record a note without disrupting the progress bar."""
129
# XXX: shouldn't be here; put it in mutter or the ui instead
131
self.ui_factory.note(fmt_string % args)
133
self.ui_factory.note(fmt_string)
136
# XXX: shouldn't be here; put it in mutter or the ui instead
137
self.ui_factory.clear_term()
140
@deprecated_function(deprecated_in((1, 16, 0)))
141
def ProgressBar(to_file=None, **kwargs):
142
"""Abstract factory"""
145
requested_bar_type = os.environ.get('BZR_PROGRESS_BAR')
146
# An value of '' or not set reverts to standard processing
147
if requested_bar_type in (None, ''):
148
if _supports_progress(to_file):
149
return TTYProgressBar(to_file=to_file, **kwargs)
151
return DummyProgress(to_file=to_file, **kwargs)
153
# Minor sanitation to prevent spurious errors
154
requested_bar_type = requested_bar_type.lower().strip()
155
# TODO: jam 20060710 Arguably we shouldn't raise an exception
156
# but should instead just disable progress bars if we
157
# don't recognize the type
158
if requested_bar_type not in _progress_bar_types:
159
raise errors.InvalidProgressBarType(requested_bar_type,
160
_progress_bar_types.keys())
161
return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
164
class _BaseProgressBar(object):
173
to_messages_file=None,
175
object.__init__(self)
178
if to_messages_file is None:
179
to_messages_file = sys.stdout
180
self.to_file = to_file
181
self.to_messages_file = to_messages_file
184
self.last_total = None
185
self.show_pct = show_pct
186
self.show_spinner = show_spinner
187
self.show_eta = show_eta
188
self.show_bar = show_bar
189
self.show_count = show_count
192
self.MIN_PAUSE = 0.1 # seconds
195
self.start_time = now
196
# next update should not throttle
197
self.last_update = now - self.MIN_PAUSE - 1
200
"""Return this bar to its progress stack."""
202
self._stack.return_pb(self)
204
def note(self, fmt_string, *args, **kwargs):
205
"""Record a note without disrupting the progress bar."""
207
self.to_messages_file.write(fmt_string % args)
208
self.to_messages_file.write('\n')
210
@deprecated_function(deprecated_in((1, 16, 0)))
211
def child_progress(self, **kwargs):
212
return ChildProgress(**kwargs)
215
class DummyProgress(_BaseProgressBar):
216
"""Progress-bar standin that does nothing.
218
This can be used as the default argument for methods that
219
take an optional progress indicator."""
224
def update(self, msg=None, current=None, total=None):
227
def child_update(self, message, current, total):
233
def note(self, fmt_string, *args, **kwargs):
234
"""See _BaseProgressBar.note()."""
236
def child_progress(self, **kwargs):
237
return DummyProgress(**kwargs)
240
class DotsProgressBar(_BaseProgressBar):
242
@deprecated_function(deprecated_in((1, 16, 0)))
243
def __init__(self, **kwargs):
244
_BaseProgressBar.__init__(self, **kwargs)
251
def update(self, msg=None, current_cnt=None, total_cnt=None):
252
if msg and msg != self.last_msg:
254
self.to_file.write('\n')
255
self.to_file.write(msg + ': ')
258
self.to_file.write('.')
262
self.to_file.write('\n')
265
def child_update(self, message, current, total):
269
class TTYProgressBar(_BaseProgressBar):
270
"""Progress bar display object.
272
Several options are available to control the display. These can
273
be passed as parameters to the constructor or assigned at any time:
276
Show percentage complete.
278
Show rotating baton. This ticks over on every update even
279
if the values don't change.
281
Show predicted time-to-completion.
285
Show numerical counts.
287
The output file should be in line-buffered or unbuffered mode.
291
@deprecated_function(deprecated_in((1, 16, 0)))
292
def __init__(self, **kwargs):
293
from bzrlib.osutils import terminal_width
294
_BaseProgressBar.__init__(self, **kwargs)
296
self.width = terminal_width()
297
self.last_updates = []
298
self._max_last_updates = 10
299
self.child_fraction = 0
300
self._have_output = False
302
def throttle(self, old_msg):
303
"""Return True if the bar was updated too recently"""
304
# time.time consistently takes 40/4000 ms = 0.01 ms.
305
# time.clock() is faster, but gives us CPU time, not wall-clock time
307
if self.start_time is not None and (now - self.start_time) < 1:
309
if old_msg != self.last_msg:
311
interval = now - self.last_update
313
if interval < self.MIN_PAUSE:
316
self.last_updates.append(now - self.last_update)
317
# Don't let the queue grow without bound
318
self.last_updates = self.last_updates[-self._max_last_updates:]
319
self.last_update = now
323
self.update(self.last_msg, self.last_cnt, self.last_total,
326
def child_update(self, message, current, total):
327
if current is not None and total != 0:
328
child_fraction = float(current) / total
329
if self.last_cnt is None:
331
elif self.last_cnt + child_fraction <= self.last_total:
332
self.child_fraction = child_fraction
333
if self.last_msg is None:
337
def update(self, msg, current_cnt=None, total_cnt=None,
339
"""Update and redraw progress bar.
344
if total_cnt is None:
345
total_cnt = self.last_total
350
if current_cnt > total_cnt:
351
total_cnt = current_cnt
353
## # optional corner case optimisation
354
## # currently does not seem to fire so costs more than saved.
355
## # trivial optimal case:
356
## # NB if callers are doing a clear and restore with
357
## # the saved values, this will prevent that:
358
## # in that case add a restore method that calls
359
## # _do_update or some such
360
## if (self.last_msg == msg and
361
## self.last_cnt == current_cnt and
362
## self.last_total == total_cnt and
363
## self.child_fraction == child_fraction):
369
old_msg = self.last_msg
370
# save these for the tick() function
372
self.last_cnt = current_cnt
373
self.last_total = total_cnt
374
self.child_fraction = child_fraction
376
# each function call takes 20ms/4000 = 0.005 ms,
377
# but multiple that by 4000 calls -> starts to cost.
378
# so anything to make this function call faster
379
# will improve base 'diff' time by up to 0.1 seconds.
380
if self.throttle(old_msg):
383
if self.show_eta and self.start_time and self.last_total:
384
eta = get_eta(self.start_time, self.last_cnt + self.child_fraction,
385
self.last_total, last_updates = self.last_updates)
386
eta_str = " " + str_tdelta(eta)
390
if self.show_spinner:
391
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
395
# always update this; it's also used for the bar
398
if self.show_pct and self.last_total and self.last_cnt:
399
pct = 100.0 * ((self.last_cnt + self.child_fraction) / self.last_total)
400
pct_str = ' (%5.1f%%)' % pct
404
if not self.show_count:
406
elif self.last_cnt is None:
408
elif self.last_total is None:
409
count_str = ' %i' % (self.last_cnt)
411
# make both fields the same size
412
t = '%i' % (self.last_total)
413
c = '%*i' % (len(t), self.last_cnt)
414
count_str = ' ' + c + '/' + t
417
# progress bar, if present, soaks up all remaining space
418
cols = self.width - 1 - len(self.last_msg) - len(spin_str) - len(pct_str) \
419
- len(eta_str) - len(count_str) - 3
422
# number of markers highlighted in bar
423
markers = int(round(float(cols) *
424
(self.last_cnt + self.child_fraction) / self.last_total))
425
bar_str = '[' + ('=' * markers).ljust(cols) + '] '
427
# don't know total, so can't show completion.
428
# so just show an expanded spinning thingy
429
m = self.spin_pos % cols
430
ms = (' ' * m + '*').ljust(cols)
432
bar_str = '[' + ms + '] '
438
m = spin_str + bar_str + self.last_msg + count_str \
440
self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
441
self._have_output = True
442
#self.to_file.flush()
445
if self._have_output:
446
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
447
self._have_output = False
448
#self.to_file.flush()
451
class ChildProgress(_BaseProgressBar):
452
"""A progress indicator that pushes its data to the parent"""
454
@deprecated_function(deprecated_in((1, 16, 0)))
455
def __init__(self, _stack, **kwargs):
456
_BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
457
self.parent = _stack.top()
460
self.child_fraction = 0
463
def update(self, msg, current_cnt=None, total_cnt=None):
464
self.current = current_cnt
465
if total_cnt is not None:
466
self.total = total_cnt
468
self.child_fraction = 0
471
def child_update(self, message, current, total):
472
if current is None or total == 0:
473
self.child_fraction = 0
475
self.child_fraction = float(current) / total
479
if self.current is None:
482
count = self.current+self.child_fraction
483
if count > self.total:
485
mutter('clamping count of %d to %d' % (count, self.total))
487
self.parent.child_update(self.message, count, self.total)
492
def note(self, *args, **kwargs):
493
self.parent.note(*args, **kwargs)
496
def str_tdelta(delt):
499
delt = int(round(delt))
500
return '%d:%02d:%02d' % (delt/3600,
505
def get_eta(start_time, current, total, enough_samples=3, last_updates=None, n_recent=10):
506
if start_time is None:
512
if current < enough_samples:
518
elapsed = time.time() - start_time
520
if elapsed < 2.0: # not enough time to estimate
523
total_duration = float(elapsed) * float(total) / float(current)
525
if last_updates and len(last_updates) >= n_recent:
526
avg = sum(last_updates) / float(len(last_updates))
527
time_left = avg * (total - current)
529
old_time_left = total_duration - elapsed
531
# We could return the average, or some other value here
532
return (time_left + old_time_left) / 2
534
return total_duration - elapsed
537
class ProgressPhase(object):
538
"""Update progress object with the current phase"""
539
def __init__(self, message, total, pb):
540
object.__init__(self)
542
self.message = message
544
self.cur_phase = None
546
def next_phase(self):
547
if self.cur_phase is None:
551
self.pb.update(self.message, self.cur_phase, self.total)
554
_progress_bar_types = {}
555
_progress_bar_types['dummy'] = DummyProgress
556
_progress_bar_types['none'] = DummyProgress
557
_progress_bar_types['tty'] = TTYProgressBar
558
_progress_bar_types['dots'] = DotsProgressBar