1
# Copyright (C) 2005 Aaron Bentley <aaron.bentley@utoronto.ca>
 
 
2
# Copyright (C) 2005, 2006 Canonical Ltd
 
 
4
# This program is free software; you can redistribute it and/or modify
 
 
5
# it under the terms of the GNU General Public License as published by
 
 
6
# the Free Software Foundation; either version 2 of the License, or
 
 
7
# (at your option) any later version.
 
 
9
# This program is distributed in the hope that it will be useful,
 
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
 
12
# GNU General Public License for more details.
 
 
14
# You should have received a copy of the GNU General Public License
 
 
15
# along with this program; if not, write to the Free Software
 
 
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 
19
"""Progress indicators.
 
 
21
The usual way to use this is via bzrlib.ui.ui_factory.nested_progress_bar which
 
 
22
will maintain a ProgressBarStack for you.
 
 
24
For direct use, the factory ProgressBar will return an auto-detected progress
 
 
25
bar that should match your terminal type. You can manually create a
 
 
26
ProgressBarStack too if you need multiple levels of cooperating progress bars.
 
 
27
Note that bzrlib's internal functions use the ui module, so if you are using
 
 
28
bzrlib it really is best to use bzrlib.ui.ui_factory.
 
 
31
# TODO: Optionally show elapsed time instead/as well as ETA; nicer
 
 
32
# when the rate is unpredictable
 
 
38
from bzrlib.lazy_import import lazy_import
 
 
39
lazy_import(globals(), """
 
 
45
from bzrlib.trace import mutter
 
 
48
def _supports_progress(f):
 
 
49
    """Detect if we can use pretty progress bars on the output stream f.
 
 
51
    If this returns true we expect that a human may be looking at that 
 
 
52
    output, and that we can repaint a line to update it.
 
 
54
    isatty = getattr(f, 'isatty', None)
 
 
59
    if os.environ.get('TERM') == 'dumb':
 
 
60
        # e.g. emacs compile window
 
 
65
_progress_bar_types = {}
 
 
68
def ProgressBar(to_file=None, **kwargs):
 
 
69
    """Abstract factory"""
 
 
72
    requested_bar_type = os.environ.get('BZR_PROGRESS_BAR')
 
 
73
    # An value of '' or not set reverts to standard processing
 
 
74
    if requested_bar_type in (None, ''):
 
 
75
        if _supports_progress(to_file):
 
 
76
            return TTYProgressBar(to_file=to_file, **kwargs)
 
 
78
            return DummyProgress(to_file=to_file, **kwargs)
 
 
80
        # Minor sanitation to prevent spurious errors
 
 
81
        requested_bar_type = requested_bar_type.lower().strip()
 
 
82
        # TODO: jam 20060710 Arguably we shouldn't raise an exception
 
 
83
        #       but should instead just disable progress bars if we
 
 
84
        #       don't recognize the type
 
 
85
        if requested_bar_type not in _progress_bar_types:
 
 
86
            raise errors.InvalidProgressBarType(requested_bar_type,
 
 
87
                                                _progress_bar_types.keys())
 
 
88
        return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
 
 
91
class ProgressBarStack(object):
 
 
92
    """A stack of progress bars."""
 
 
101
                 to_messages_file=None,
 
 
103
        """Setup the stack with the parameters the progress bars should have."""
 
 
106
        if to_messages_file is None:
 
 
107
            to_messages_file = sys.stdout
 
 
108
        self._to_file = to_file
 
 
109
        self._show_pct = show_pct
 
 
110
        self._show_spinner = show_spinner
 
 
111
        self._show_eta = show_eta
 
 
112
        self._show_bar = show_bar
 
 
113
        self._show_count = show_count
 
 
114
        self._to_messages_file = to_messages_file
 
 
116
        self._klass = klass or ProgressBar
 
 
119
        if len(self._stack) != 0:
 
 
120
            return self._stack[-1]
 
 
125
        if len(self._stack) != 0:
 
 
126
            return self._stack[0]
 
 
130
    def get_nested(self):
 
 
131
        """Return a nested progress bar."""
 
 
132
        if len(self._stack) == 0:
 
 
135
            func = self.top().child_progress
 
 
136
        new_bar = func(to_file=self._to_file,
 
 
137
                       show_pct=self._show_pct,
 
 
138
                       show_spinner=self._show_spinner,
 
 
139
                       show_eta=self._show_eta,
 
 
140
                       show_bar=self._show_bar,
 
 
141
                       show_count=self._show_count,
 
 
142
                       to_messages_file=self._to_messages_file,
 
 
144
        self._stack.append(new_bar)
 
 
147
    def return_pb(self, bar):
 
 
148
        """Return bar after its been used."""
 
 
149
        if bar is not self._stack[-1]:
 
 
150
            raise errors.MissingProgressBarFinish()
 
 
154
class _BaseProgressBar(object):
 
 
163
                 to_messages_file=None,
 
 
165
        object.__init__(self)
 
 
168
        if to_messages_file is None:
 
 
169
            to_messages_file = sys.stdout
 
 
170
        self.to_file = to_file
 
 
171
        self.to_messages_file = to_messages_file
 
 
174
        self.last_total = None
 
 
175
        self.show_pct = show_pct
 
 
176
        self.show_spinner = show_spinner
 
 
177
        self.show_eta = show_eta
 
 
178
        self.show_bar = show_bar
 
 
179
        self.show_count = show_count
 
 
182
        self.MIN_PAUSE = 0.1 # seconds
 
 
185
        self.start_time = now
 
 
186
        # next update should not throttle
 
 
187
        self.last_update = now - self.MIN_PAUSE - 1
 
 
190
        """Return this bar to its progress stack."""
 
 
192
        assert self._stack is not None
 
 
193
        self._stack.return_pb(self)
 
 
195
    def note(self, fmt_string, *args, **kwargs):
 
 
196
        """Record a note without disrupting the progress bar."""
 
 
198
        self.to_messages_file.write(fmt_string % args)
 
 
199
        self.to_messages_file.write('\n')
 
 
201
    def child_progress(self, **kwargs):
 
 
202
        return ChildProgress(**kwargs)
 
 
205
class DummyProgress(_BaseProgressBar):
 
 
206
    """Progress-bar standin that does nothing.
 
 
208
    This can be used as the default argument for methods that
 
 
209
    take an optional progress indicator."""
 
 
213
    def update(self, msg=None, current=None, total=None):
 
 
216
    def child_update(self, message, current, total):
 
 
222
    def note(self, fmt_string, *args, **kwargs):
 
 
223
        """See _BaseProgressBar.note()."""
 
 
225
    def child_progress(self, **kwargs):
 
 
226
        return DummyProgress(**kwargs)
 
 
229
_progress_bar_types['dummy'] = DummyProgress
 
 
230
_progress_bar_types['none'] = DummyProgress
 
 
233
class DotsProgressBar(_BaseProgressBar):
 
 
235
    def __init__(self, **kwargs):
 
 
236
        _BaseProgressBar.__init__(self, **kwargs)
 
 
243
    def update(self, msg=None, current_cnt=None, total_cnt=None):
 
 
244
        if msg and msg != self.last_msg:
 
 
246
                self.to_file.write('\n')
 
 
247
            self.to_file.write(msg + ': ')
 
 
250
        self.to_file.write('.')
 
 
254
            self.to_file.write('\n')
 
 
257
    def child_update(self, message, current, total):
 
 
261
_progress_bar_types['dots'] = DotsProgressBar
 
 
264
class TTYProgressBar(_BaseProgressBar):
 
 
265
    """Progress bar display object.
 
 
267
    Several options are available to control the display.  These can
 
 
268
    be passed as parameters to the constructor or assigned at any time:
 
 
271
        Show percentage complete.
 
 
273
        Show rotating baton.  This ticks over on every update even
 
 
274
        if the values don't change.
 
 
276
        Show predicted time-to-completion.
 
 
280
        Show numerical counts.
 
 
282
    The output file should be in line-buffered or unbuffered mode.
 
 
287
    def __init__(self, **kwargs):
 
 
288
        from bzrlib.osutils import terminal_width
 
 
289
        _BaseProgressBar.__init__(self, **kwargs)
 
 
291
        self.width = terminal_width()
 
 
292
        self.last_updates = []
 
 
293
        self._max_last_updates = 10
 
 
294
        self.child_fraction = 0
 
 
295
        self._have_output = False
 
 
297
    def throttle(self, old_msg):
 
 
298
        """Return True if the bar was updated too recently"""
 
 
299
        # time.time consistently takes 40/4000 ms = 0.01 ms.
 
 
300
        # time.clock() is faster, but gives us CPU time, not wall-clock time
 
 
302
        if self.start_time is not None and (now - self.start_time) < 1:
 
 
304
        if old_msg != self.last_msg:
 
 
306
        interval = now - self.last_update
 
 
308
        if interval < self.MIN_PAUSE:
 
 
311
        self.last_updates.append(now - self.last_update)
 
 
312
        # Don't let the queue grow without bound
 
 
313
        self.last_updates = self.last_updates[-self._max_last_updates:]
 
 
314
        self.last_update = now
 
 
318
        self.update(self.last_msg, self.last_cnt, self.last_total,
 
 
321
    def child_update(self, message, current, total):
 
 
322
        if current is not None and total != 0:
 
 
323
            child_fraction = float(current) / total
 
 
324
            if self.last_cnt is None:
 
 
326
            elif self.last_cnt + child_fraction <= self.last_total:
 
 
327
                self.child_fraction = child_fraction
 
 
328
        if self.last_msg is None:
 
 
332
    def update(self, msg, current_cnt=None, total_cnt=None,
 
 
334
        """Update and redraw progress bar."""
 
 
338
        if total_cnt is None:
 
 
339
            total_cnt = self.last_total
 
 
344
        if current_cnt > total_cnt:
 
 
345
            total_cnt = current_cnt
 
 
347
        ## # optional corner case optimisation 
 
 
348
        ## # currently does not seem to fire so costs more than saved.
 
 
349
        ## # trivial optimal case:
 
 
350
        ## # NB if callers are doing a clear and restore with
 
 
351
        ## # the saved values, this will prevent that:
 
 
352
        ## # in that case add a restore method that calls
 
 
353
        ## # _do_update or some such
 
 
354
        ## if (self.last_msg == msg and
 
 
355
        ##     self.last_cnt == current_cnt and
 
 
356
        ##     self.last_total == total_cnt and
 
 
357
        ##     self.child_fraction == child_fraction):
 
 
360
        old_msg = self.last_msg
 
 
361
        # save these for the tick() function
 
 
363
        self.last_cnt = current_cnt
 
 
364
        self.last_total = total_cnt
 
 
365
        self.child_fraction = child_fraction
 
 
367
        # each function call takes 20ms/4000 = 0.005 ms, 
 
 
368
        # but multiple that by 4000 calls -> starts to cost.
 
 
369
        # so anything to make this function call faster
 
 
370
        # will improve base 'diff' time by up to 0.1 seconds.
 
 
371
        if self.throttle(old_msg):
 
 
374
        if self.show_eta and self.start_time and self.last_total:
 
 
375
            eta = get_eta(self.start_time, self.last_cnt + self.child_fraction, 
 
 
376
                    self.last_total, last_updates = self.last_updates)
 
 
377
            eta_str = " " + str_tdelta(eta)
 
 
381
        if self.show_spinner:
 
 
382
            spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '            
 
 
386
        # always update this; it's also used for the bar
 
 
389
        if self.show_pct and self.last_total and self.last_cnt:
 
 
390
            pct = 100.0 * ((self.last_cnt + self.child_fraction) / self.last_total)
 
 
391
            pct_str = ' (%5.1f%%)' % pct
 
 
395
        if not self.show_count:
 
 
397
        elif self.last_cnt is None:
 
 
399
        elif self.last_total is None:
 
 
400
            count_str = ' %i' % (self.last_cnt)
 
 
402
            # make both fields the same size
 
 
403
            t = '%i' % (self.last_total)
 
 
404
            c = '%*i' % (len(t), self.last_cnt)
 
 
405
            count_str = ' ' + c + '/' + t 
 
 
408
            # progress bar, if present, soaks up all remaining space
 
 
409
            cols = self.width - 1 - len(self.last_msg) - len(spin_str) - len(pct_str) \
 
 
410
                   - len(eta_str) - len(count_str) - 3
 
 
413
                # number of markers highlighted in bar
 
 
414
                markers = int(round(float(cols) * 
 
 
415
                              (self.last_cnt + self.child_fraction) / self.last_total))
 
 
416
                bar_str = '[' + ('=' * markers).ljust(cols) + '] '
 
 
418
                # don't know total, so can't show completion.
 
 
419
                # so just show an expanded spinning thingy
 
 
420
                m = self.spin_pos % cols
 
 
421
                ms = (' ' * m + '*').ljust(cols)
 
 
423
                bar_str = '[' + ms + '] '
 
 
429
        m = spin_str + bar_str + self.last_msg + count_str + pct_str + eta_str
 
 
430
        self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
 
 
431
        self._have_output = True
 
 
432
        #self.to_file.flush()
 
 
435
        if self._have_output:
 
 
436
            self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
 
 
437
        self._have_output = False
 
 
438
        #self.to_file.flush()        
 
 
441
_progress_bar_types['tty'] = TTYProgressBar
 
 
444
class ChildProgress(_BaseProgressBar):
 
 
445
    """A progress indicator that pushes its data to the parent"""
 
 
447
    def __init__(self, _stack, **kwargs):
 
 
448
        _BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
 
 
449
        self.parent = _stack.top()
 
 
452
        self.child_fraction = 0
 
 
455
    def update(self, msg, current_cnt=None, total_cnt=None):
 
 
456
        self.current = current_cnt
 
 
457
        if total_cnt is not None:
 
 
458
            self.total = total_cnt
 
 
460
        self.child_fraction = 0
 
 
463
    def child_update(self, message, current, total):
 
 
464
        if current is None or total == 0:
 
 
465
            self.child_fraction = 0
 
 
467
            self.child_fraction = float(current) / total
 
 
471
        if self.current is None:
 
 
474
            count = self.current+self.child_fraction
 
 
475
            if count > self.total:
 
 
477
                    mutter('clamping count of %d to %d' % (count, self.total))
 
 
479
        self.parent.child_update(self.message, count, self.total)
 
 
484
    def note(self, *args, **kwargs):
 
 
485
        self.parent.note(*args, **kwargs)
 
 
488
class InstrumentedProgress(TTYProgressBar):
 
 
489
    """TTYProgress variant that tracks outcomes"""
 
 
491
    def __init__(self, *args, **kwargs):
 
 
492
        self.always_throttled = True
 
 
493
        self.never_throttle = False
 
 
494
        TTYProgressBar.__init__(self, *args, **kwargs)
 
 
496
    def throttle(self, old_message):
 
 
497
        if self.never_throttle:
 
 
500
            result = TTYProgressBar.throttle(self, old_message)
 
 
502
            self.always_throttled = False
 
 
505
def str_tdelta(delt):
 
 
508
    delt = int(round(delt))
 
 
509
    return '%d:%02d:%02d' % (delt/3600,
 
 
514
def get_eta(start_time, current, total, enough_samples=3, last_updates=None, n_recent=10):
 
 
515
    if start_time is None:
 
 
521
    if current < enough_samples:
 
 
527
    elapsed = time.time() - start_time
 
 
529
    if elapsed < 2.0:                   # not enough time to estimate
 
 
532
    total_duration = float(elapsed) * float(total) / float(current)
 
 
534
    assert total_duration >= elapsed
 
 
536
    if last_updates and len(last_updates) >= n_recent:
 
 
537
        avg = sum(last_updates) / float(len(last_updates))
 
 
538
        time_left = avg * (total - current)
 
 
540
        old_time_left = total_duration - elapsed
 
 
542
        # We could return the average, or some other value here
 
 
543
        return (time_left + old_time_left) / 2
 
 
545
    return total_duration - elapsed
 
 
548
class ProgressPhase(object):
 
 
549
    """Update progress object with the current phase"""
 
 
550
    def __init__(self, message, total, pb):
 
 
551
        object.__init__(self)
 
 
553
        self.message = message
 
 
555
        self.cur_phase = None
 
 
557
    def next_phase(self):
 
 
558
        if self.cur_phase is None:
 
 
562
        assert self.cur_phase < self.total
 
 
563
        self.pb.update(self.message, self.cur_phase, self.total)