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
 
 
329
                mutter('not updating child fraction')
 
 
330
        if self.last_msg is None:
 
 
334
    def update(self, msg, current_cnt=None, total_cnt=None,
 
 
336
        """Update and redraw progress bar."""
 
 
340
        if total_cnt is None:
 
 
341
            total_cnt = self.last_total
 
 
346
        if current_cnt > total_cnt:
 
 
347
            total_cnt = current_cnt
 
 
349
        ## # optional corner case optimisation 
 
 
350
        ## # currently does not seem to fire so costs more than saved.
 
 
351
        ## # trivial optimal case:
 
 
352
        ## # NB if callers are doing a clear and restore with
 
 
353
        ## # the saved values, this will prevent that:
 
 
354
        ## # in that case add a restore method that calls
 
 
355
        ## # _do_update or some such
 
 
356
        ## if (self.last_msg == msg and
 
 
357
        ##     self.last_cnt == current_cnt and
 
 
358
        ##     self.last_total == total_cnt and
 
 
359
        ##     self.child_fraction == child_fraction):
 
 
362
        old_msg = self.last_msg
 
 
363
        # save these for the tick() function
 
 
365
        self.last_cnt = current_cnt
 
 
366
        self.last_total = total_cnt
 
 
367
        self.child_fraction = child_fraction
 
 
369
        # each function call takes 20ms/4000 = 0.005 ms, 
 
 
370
        # but multiple that by 4000 calls -> starts to cost.
 
 
371
        # so anything to make this function call faster
 
 
372
        # will improve base 'diff' time by up to 0.1 seconds.
 
 
373
        if self.throttle(old_msg):
 
 
376
        if self.show_eta and self.start_time and self.last_total:
 
 
377
            eta = get_eta(self.start_time, self.last_cnt + self.child_fraction, 
 
 
378
                    self.last_total, last_updates = self.last_updates)
 
 
379
            eta_str = " " + str_tdelta(eta)
 
 
383
        if self.show_spinner:
 
 
384
            spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '            
 
 
388
        # always update this; it's also used for the bar
 
 
391
        if self.show_pct and self.last_total and self.last_cnt:
 
 
392
            pct = 100.0 * ((self.last_cnt + self.child_fraction) / self.last_total)
 
 
393
            pct_str = ' (%5.1f%%)' % pct
 
 
397
        if not self.show_count:
 
 
399
        elif self.last_cnt is None:
 
 
401
        elif self.last_total is None:
 
 
402
            count_str = ' %i' % (self.last_cnt)
 
 
404
            # make both fields the same size
 
 
405
            t = '%i' % (self.last_total)
 
 
406
            c = '%*i' % (len(t), self.last_cnt)
 
 
407
            count_str = ' ' + c + '/' + t 
 
 
410
            # progress bar, if present, soaks up all remaining space
 
 
411
            cols = self.width - 1 - len(self.last_msg) - len(spin_str) - len(pct_str) \
 
 
412
                   - len(eta_str) - len(count_str) - 3
 
 
415
                # number of markers highlighted in bar
 
 
416
                markers = int(round(float(cols) * 
 
 
417
                              (self.last_cnt + self.child_fraction) / self.last_total))
 
 
418
                bar_str = '[' + ('=' * markers).ljust(cols) + '] '
 
 
420
                # don't know total, so can't show completion.
 
 
421
                # so just show an expanded spinning thingy
 
 
422
                m = self.spin_pos % cols
 
 
423
                ms = (' ' * m + '*').ljust(cols)
 
 
425
                bar_str = '[' + ms + '] '
 
 
431
        m = spin_str + bar_str + self.last_msg + count_str + pct_str + eta_str
 
 
432
        self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
 
 
433
        self._have_output = True
 
 
434
        #self.to_file.flush()
 
 
437
        if self._have_output:
 
 
438
            self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
 
 
439
        self._have_output = False
 
 
440
        #self.to_file.flush()        
 
 
443
_progress_bar_types['tty'] = TTYProgressBar
 
 
446
class ChildProgress(_BaseProgressBar):
 
 
447
    """A progress indicator that pushes its data to the parent"""
 
 
449
    def __init__(self, _stack, **kwargs):
 
 
450
        _BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
 
 
451
        self.parent = _stack.top()
 
 
454
        self.child_fraction = 0
 
 
457
    def update(self, msg, current_cnt=None, total_cnt=None):
 
 
458
        self.current = current_cnt
 
 
459
        if total_cnt is not None:
 
 
460
            self.total = total_cnt
 
 
462
        self.child_fraction = 0
 
 
465
    def child_update(self, message, current, total):
 
 
466
        if current is None or total == 0:
 
 
467
            self.child_fraction = 0
 
 
469
            self.child_fraction = float(current) / total
 
 
473
        if self.current is None:
 
 
476
            count = self.current+self.child_fraction
 
 
477
            if count > self.total:
 
 
479
                    mutter('clamping count of %d to %d' % (count, self.total))
 
 
481
        self.parent.child_update(self.message, count, self.total)
 
 
486
    def note(self, *args, **kwargs):
 
 
487
        self.parent.note(*args, **kwargs)
 
 
490
class InstrumentedProgress(TTYProgressBar):
 
 
491
    """TTYProgress variant that tracks outcomes"""
 
 
493
    def __init__(self, *args, **kwargs):
 
 
494
        self.always_throttled = True
 
 
495
        self.never_throttle = False
 
 
496
        TTYProgressBar.__init__(self, *args, **kwargs)
 
 
498
    def throttle(self, old_message):
 
 
499
        if self.never_throttle:
 
 
502
            result = TTYProgressBar.throttle(self, old_message)
 
 
504
            self.always_throttled = False
 
 
507
def str_tdelta(delt):
 
 
510
    delt = int(round(delt))
 
 
511
    return '%d:%02d:%02d' % (delt/3600,
 
 
516
def get_eta(start_time, current, total, enough_samples=3, last_updates=None, n_recent=10):
 
 
517
    if start_time is None:
 
 
523
    if current < enough_samples:
 
 
529
    elapsed = time.time() - start_time
 
 
531
    if elapsed < 2.0:                   # not enough time to estimate
 
 
534
    total_duration = float(elapsed) * float(total) / float(current)
 
 
536
    assert total_duration >= elapsed
 
 
538
    if last_updates and len(last_updates) >= n_recent:
 
 
539
        avg = sum(last_updates) / float(len(last_updates))
 
 
540
        time_left = avg * (total - current)
 
 
542
        old_time_left = total_duration - elapsed
 
 
544
        # We could return the average, or some other value here
 
 
545
        return (time_left + old_time_left) / 2
 
 
547
    return total_duration - elapsed
 
 
550
class ProgressPhase(object):
 
 
551
    """Update progress object with the current phase"""
 
 
552
    def __init__(self, message, total, pb):
 
 
553
        object.__init__(self)
 
 
555
        self.message = message
 
 
557
        self.cur_phase = None
 
 
559
    def next_phase(self):
 
 
560
        if self.cur_phase is None:
 
 
564
        assert self.cur_phase < self.total
 
 
565
        self.pb.update(self.message, self.cur_phase, self.total)