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
38
from bzrlib.symbol_versioning import (
44
def _supports_progress(f):
45
"""Detect if we can use pretty progress bars on the output stream f.
47
If this returns true we expect that a human may be looking at that
48
output, and that we can repaint a line to update it.
50
isatty = getattr(f, 'isatty', None)
55
if os.environ.get('TERM') == 'dumb':
56
# e.g. emacs compile window
61
class ProgressTask(object):
62
"""Model component of a progress indicator.
64
Most code that needs to indicate progress should update one of these,
65
and it will in turn update the display, if one is present.
67
Code updating the task may also set fields as hints about how to display
68
it: show_pct, show_spinner, show_eta, show_count, show_bar. UIs
69
will not necessarily respect all these fields.
72
def __init__(self, parent_task=None, ui_factory=None):
73
"""Construct a new progress task.
75
Normally you should not call this directly but rather through
76
`ui_factory.nested_progress_bar`.
78
self._parent_task = parent_task
81
self.current_cnt = None
83
self.ui_factory = ui_factory
85
self.show_spinner = True
86
self.show_eta = False,
87
self.show_count = True
91
return '%s(%r/%r, msg=%r)' % (
92
self.__class__.__name__,
97
def update(self, msg, current_cnt=None, total_cnt=None):
99
self.current_cnt = current_cnt
101
self.total_cnt = total_cnt
102
self.ui_factory._progress_updated(self)
105
self.update(self.msg)
108
self.ui_factory._progress_finished(self)
110
def make_sub_task(self):
111
return ProgressTask(self, self.ui_factory)
113
def _overall_completion_fraction(self, child_fraction=0.0):
114
"""Return fractional completion of this task and its parents
116
Returns None if no completion can be computed."""
117
if self.current_cnt is not None and self.total_cnt:
118
own_fraction = (float(self.current_cnt) + child_fraction) / self.total_cnt
121
if self._parent_task is None:
124
if own_fraction is None:
126
return self._parent_task._overall_completion_fraction(own_fraction)
128
def note(self, fmt_string, *args):
129
"""Record a note without disrupting the progress bar."""
130
# XXX: shouldn't be here; put it in mutter or the ui instead
132
self.ui_factory.note(fmt_string % args)
134
self.ui_factory.note(fmt_string)
137
# XXX: shouldn't be here; put it in mutter or the ui instead
138
self.ui_factory.clear_term()
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 ProgressBarStack(object):
165
"""A stack of progress bars.
167
This class is deprecated: instead, ask the ui factory for a new progress
168
task and finish it when it's done.
171
@deprecated_method(deprecated_in((1, 12, 0)))
179
to_messages_file=None,
181
"""Setup the stack with the parameters the progress bars should have."""
184
if to_messages_file is None:
185
to_messages_file = sys.stdout
186
self._to_file = to_file
187
self._show_pct = show_pct
188
self._show_spinner = show_spinner
189
self._show_eta = show_eta
190
self._show_bar = show_bar
191
self._show_count = show_count
192
self._to_messages_file = to_messages_file
194
self._klass = klass or ProgressBar
197
if len(self._stack) != 0:
198
return self._stack[-1]
203
if len(self._stack) != 0:
204
return self._stack[0]
208
def get_nested(self):
209
"""Return a nested progress bar."""
210
if len(self._stack) == 0:
213
func = self.top().child_progress
214
new_bar = func(to_file=self._to_file,
215
show_pct=self._show_pct,
216
show_spinner=self._show_spinner,
217
show_eta=self._show_eta,
218
show_bar=self._show_bar,
219
show_count=self._show_count,
220
to_messages_file=self._to_messages_file,
222
self._stack.append(new_bar)
225
def return_pb(self, bar):
226
"""Return bar after its been used."""
227
if bar is not self._stack[-1]:
228
warnings.warn("%r is not currently active" % (bar,))
233
class _BaseProgressBar(object):
242
to_messages_file=None,
244
object.__init__(self)
247
if to_messages_file is None:
248
to_messages_file = sys.stdout
249
self.to_file = to_file
250
self.to_messages_file = to_messages_file
253
self.last_total = None
254
self.show_pct = show_pct
255
self.show_spinner = show_spinner
256
self.show_eta = show_eta
257
self.show_bar = show_bar
258
self.show_count = show_count
261
self.MIN_PAUSE = 0.1 # seconds
264
self.start_time = now
265
# next update should not throttle
266
self.last_update = now - self.MIN_PAUSE - 1
269
"""Return this bar to its progress stack."""
271
self._stack.return_pb(self)
273
def note(self, fmt_string, *args, **kwargs):
274
"""Record a note without disrupting the progress bar."""
276
self.to_messages_file.write(fmt_string % args)
277
self.to_messages_file.write('\n')
279
def child_progress(self, **kwargs):
280
return ChildProgress(**kwargs)
283
class DummyProgress(_BaseProgressBar):
284
"""Progress-bar standin that does nothing.
286
This can be used as the default argument for methods that
287
take an optional progress indicator."""
292
def update(self, msg=None, current=None, total=None):
295
def child_update(self, message, current, total):
301
def note(self, fmt_string, *args, **kwargs):
302
"""See _BaseProgressBar.note()."""
304
def child_progress(self, **kwargs):
305
return DummyProgress(**kwargs)
308
class DotsProgressBar(_BaseProgressBar):
310
def __init__(self, **kwargs):
311
_BaseProgressBar.__init__(self, **kwargs)
318
def update(self, msg=None, current_cnt=None, total_cnt=None):
319
if msg and msg != self.last_msg:
321
self.to_file.write('\n')
322
self.to_file.write(msg + ': ')
325
self.to_file.write('.')
329
self.to_file.write('\n')
332
def child_update(self, message, current, total):
338
class TTYProgressBar(_BaseProgressBar):
339
"""Progress bar display object.
341
Several options are available to control the display. These can
342
be passed as parameters to the constructor or assigned at any time:
345
Show percentage complete.
347
Show rotating baton. This ticks over on every update even
348
if the values don't change.
350
Show predicted time-to-completion.
354
Show numerical counts.
356
The output file should be in line-buffered or unbuffered mode.
361
def __init__(self, **kwargs):
362
from bzrlib.osutils import terminal_width
363
_BaseProgressBar.__init__(self, **kwargs)
365
self.width = terminal_width()
366
self.last_updates = []
367
self._max_last_updates = 10
368
self.child_fraction = 0
369
self._have_output = False
371
def throttle(self, old_msg):
372
"""Return True if the bar was updated too recently"""
373
# time.time consistently takes 40/4000 ms = 0.01 ms.
374
# time.clock() is faster, but gives us CPU time, not wall-clock time
376
if self.start_time is not None and (now - self.start_time) < 1:
378
if old_msg != self.last_msg:
380
interval = now - self.last_update
382
if interval < self.MIN_PAUSE:
385
self.last_updates.append(now - self.last_update)
386
# Don't let the queue grow without bound
387
self.last_updates = self.last_updates[-self._max_last_updates:]
388
self.last_update = now
392
self.update(self.last_msg, self.last_cnt, self.last_total,
395
def child_update(self, message, current, total):
396
if current is not None and total != 0:
397
child_fraction = float(current) / total
398
if self.last_cnt is None:
400
elif self.last_cnt + child_fraction <= self.last_total:
401
self.child_fraction = child_fraction
402
if self.last_msg is None:
406
def update(self, msg, current_cnt=None, total_cnt=None,
408
"""Update and redraw progress bar.
413
if total_cnt is None:
414
total_cnt = self.last_total
419
if current_cnt > total_cnt:
420
total_cnt = current_cnt
422
## # optional corner case optimisation
423
## # currently does not seem to fire so costs more than saved.
424
## # trivial optimal case:
425
## # NB if callers are doing a clear and restore with
426
## # the saved values, this will prevent that:
427
## # in that case add a restore method that calls
428
## # _do_update or some such
429
## if (self.last_msg == msg and
430
## self.last_cnt == current_cnt and
431
## self.last_total == total_cnt and
432
## self.child_fraction == child_fraction):
438
old_msg = self.last_msg
439
# save these for the tick() function
441
self.last_cnt = current_cnt
442
self.last_total = total_cnt
443
self.child_fraction = child_fraction
445
# each function call takes 20ms/4000 = 0.005 ms,
446
# but multiple that by 4000 calls -> starts to cost.
447
# so anything to make this function call faster
448
# will improve base 'diff' time by up to 0.1 seconds.
449
if self.throttle(old_msg):
452
if self.show_eta and self.start_time and self.last_total:
453
eta = get_eta(self.start_time, self.last_cnt + self.child_fraction,
454
self.last_total, last_updates = self.last_updates)
455
eta_str = " " + str_tdelta(eta)
459
if self.show_spinner:
460
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
464
# always update this; it's also used for the bar
467
if self.show_pct and self.last_total and self.last_cnt:
468
pct = 100.0 * ((self.last_cnt + self.child_fraction) / self.last_total)
469
pct_str = ' (%5.1f%%)' % pct
473
if not self.show_count:
475
elif self.last_cnt is None:
477
elif self.last_total is None:
478
count_str = ' %i' % (self.last_cnt)
480
# make both fields the same size
481
t = '%i' % (self.last_total)
482
c = '%*i' % (len(t), self.last_cnt)
483
count_str = ' ' + c + '/' + t
486
# progress bar, if present, soaks up all remaining space
487
cols = self.width - 1 - len(self.last_msg) - len(spin_str) - len(pct_str) \
488
- len(eta_str) - len(count_str) - 3
491
# number of markers highlighted in bar
492
markers = int(round(float(cols) *
493
(self.last_cnt + self.child_fraction) / self.last_total))
494
bar_str = '[' + ('=' * markers).ljust(cols) + '] '
496
# don't know total, so can't show completion.
497
# so just show an expanded spinning thingy
498
m = self.spin_pos % cols
499
ms = (' ' * m + '*').ljust(cols)
501
bar_str = '[' + ms + '] '
507
m = spin_str + bar_str + self.last_msg + count_str \
509
self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
510
self._have_output = True
511
#self.to_file.flush()
514
if self._have_output:
515
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
516
self._have_output = False
517
#self.to_file.flush()
522
class ChildProgress(_BaseProgressBar):
523
"""A progress indicator that pushes its data to the parent"""
525
def __init__(self, _stack, **kwargs):
526
_BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
527
self.parent = _stack.top()
530
self.child_fraction = 0
533
def update(self, msg, current_cnt=None, total_cnt=None):
534
self.current = current_cnt
535
if total_cnt is not None:
536
self.total = total_cnt
538
self.child_fraction = 0
541
def child_update(self, message, current, total):
542
if current is None or total == 0:
543
self.child_fraction = 0
545
self.child_fraction = float(current) / total
549
if self.current is None:
552
count = self.current+self.child_fraction
553
if count > self.total:
555
mutter('clamping count of %d to %d' % (count, self.total))
557
self.parent.child_update(self.message, count, self.total)
562
def note(self, *args, **kwargs):
563
self.parent.note(*args, **kwargs)
566
class InstrumentedProgress(TTYProgressBar):
567
"""TTYProgress variant that tracks outcomes"""
569
def __init__(self, *args, **kwargs):
570
self.always_throttled = True
571
self.never_throttle = False
572
TTYProgressBar.__init__(self, *args, **kwargs)
574
def throttle(self, old_message):
575
if self.never_throttle:
578
result = TTYProgressBar.throttle(self, old_message)
580
self.always_throttled = False
583
def str_tdelta(delt):
586
delt = int(round(delt))
587
return '%d:%02d:%02d' % (delt/3600,
592
def get_eta(start_time, current, total, enough_samples=3, last_updates=None, n_recent=10):
593
if start_time is None:
599
if current < enough_samples:
605
elapsed = time.time() - start_time
607
if elapsed < 2.0: # not enough time to estimate
610
total_duration = float(elapsed) * float(total) / float(current)
612
if last_updates and len(last_updates) >= n_recent:
613
avg = sum(last_updates) / float(len(last_updates))
614
time_left = avg * (total - current)
616
old_time_left = total_duration - elapsed
618
# We could return the average, or some other value here
619
return (time_left + old_time_left) / 2
621
return total_duration - elapsed
624
class ProgressPhase(object):
625
"""Update progress object with the current phase"""
626
def __init__(self, message, total, pb):
627
object.__init__(self)
629
self.message = message
631
self.cur_phase = None
633
def next_phase(self):
634
if self.cur_phase is None:
638
self.pb.update(self.message, self.cur_phase, self.total)
641
_progress_bar_types = {}
642
_progress_bar_types['dummy'] = DummyProgress
643
_progress_bar_types['none'] = DummyProgress
644
_progress_bar_types['tty'] = TTYProgressBar
645
_progress_bar_types['dots'] = DotsProgressBar