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
        self._parent_task = parent_task
 
 
76
        self.current_cnt = None
 
 
78
        self.ui_factory = ui_factory
 
 
80
        self.show_spinner = True
 
 
81
        self.show_eta = False,
 
 
82
        self.show_count = True
 
 
86
        return '%s(%r/%r, msg=%r)' % (
 
 
87
            self.__class__.__name__,
 
 
92
    def update(self, msg, current_cnt=None, total_cnt=None):
 
 
94
        self.current_cnt = current_cnt
 
 
96
            self.total_cnt = total_cnt
 
 
97
        self.ui_factory._progress_updated(self)
 
 
100
        self.update(self.msg)
 
 
103
        self.ui_factory._progress_finished(self)
 
 
105
    def make_sub_task(self):
 
 
106
        return ProgressTask(self, self.ui_factory)
 
 
108
    def _overall_completion_fraction(self, child_fraction=0.0):
 
 
109
        """Return fractional completion of this task and its parents
 
 
111
        Returns None if no completion can be computed."""
 
 
113
            own_fraction = (float(self.current_cnt) + child_fraction) / self.total_cnt
 
 
116
        if self._parent_task is None:
 
 
119
            if own_fraction is None:
 
 
121
            return self._parent_task._overall_completion_fraction(own_fraction)
 
 
123
    def note(self, fmt_string, *args):
 
 
124
        """Record a note without disrupting the progress bar."""
 
 
125
        # XXX: shouldn't be here; put it in mutter or the ui instead
 
 
127
            self.ui_factory.note(fmt_string % args)
 
 
129
            self.ui_factory.note(fmt_string)
 
 
132
        # XXX: shouldn't be here; put it in mutter or the ui instead
 
 
133
        self.ui_factory.clear_term()
 
 
136
def ProgressBar(to_file=None, **kwargs):
 
 
137
    """Abstract factory"""
 
 
140
    requested_bar_type = os.environ.get('BZR_PROGRESS_BAR')
 
 
141
    # An value of '' or not set reverts to standard processing
 
 
142
    if requested_bar_type in (None, ''):
 
 
143
        if _supports_progress(to_file):
 
 
144
            return TTYProgressBar(to_file=to_file, **kwargs)
 
 
146
            return DummyProgress(to_file=to_file, **kwargs)
 
 
148
        # Minor sanitation to prevent spurious errors
 
 
149
        requested_bar_type = requested_bar_type.lower().strip()
 
 
150
        # TODO: jam 20060710 Arguably we shouldn't raise an exception
 
 
151
        #       but should instead just disable progress bars if we
 
 
152
        #       don't recognize the type
 
 
153
        if requested_bar_type not in _progress_bar_types:
 
 
154
            raise errors.InvalidProgressBarType(requested_bar_type,
 
 
155
                                                _progress_bar_types.keys())
 
 
156
        return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
 
 
159
class ProgressBarStack(object):
 
 
160
    """A stack of progress bars.
 
 
162
    This class is deprecated: instead, ask the ui factory for a new progress
 
 
163
    task and finish it when it's done.
 
 
166
    @deprecated_method(deprecated_in((1, 12, 0)))
 
 
174
                 to_messages_file=None,
 
 
176
        """Setup the stack with the parameters the progress bars should have."""
 
 
179
        if to_messages_file is None:
 
 
180
            to_messages_file = sys.stdout
 
 
181
        self._to_file = to_file
 
 
182
        self._show_pct = show_pct
 
 
183
        self._show_spinner = show_spinner
 
 
184
        self._show_eta = show_eta
 
 
185
        self._show_bar = show_bar
 
 
186
        self._show_count = show_count
 
 
187
        self._to_messages_file = to_messages_file
 
 
189
        self._klass = klass or ProgressBar
 
 
192
        if len(self._stack) != 0:
 
 
193
            return self._stack[-1]
 
 
198
        if len(self._stack) != 0:
 
 
199
            return self._stack[0]
 
 
203
    def get_nested(self):
 
 
204
        """Return a nested progress bar."""
 
 
205
        if len(self._stack) == 0:
 
 
208
            func = self.top().child_progress
 
 
209
        new_bar = func(to_file=self._to_file,
 
 
210
                       show_pct=self._show_pct,
 
 
211
                       show_spinner=self._show_spinner,
 
 
212
                       show_eta=self._show_eta,
 
 
213
                       show_bar=self._show_bar,
 
 
214
                       show_count=self._show_count,
 
 
215
                       to_messages_file=self._to_messages_file,
 
 
217
        self._stack.append(new_bar)
 
 
220
    def return_pb(self, bar):
 
 
221
        """Return bar after its been used."""
 
 
222
        if bar is not self._stack[-1]:
 
 
223
            warnings.warn("%r is not currently active" % (bar,))
 
 
228
class _BaseProgressBar(object):
 
 
237
                 to_messages_file=None,
 
 
239
        object.__init__(self)
 
 
242
        if to_messages_file is None:
 
 
243
            to_messages_file = sys.stdout
 
 
244
        self.to_file = to_file
 
 
245
        self.to_messages_file = to_messages_file
 
 
248
        self.last_total = None
 
 
249
        self.show_pct = show_pct
 
 
250
        self.show_spinner = show_spinner
 
 
251
        self.show_eta = show_eta
 
 
252
        self.show_bar = show_bar
 
 
253
        self.show_count = show_count
 
 
256
        self.MIN_PAUSE = 0.1 # seconds
 
 
259
        self.start_time = now
 
 
260
        # next update should not throttle
 
 
261
        self.last_update = now - self.MIN_PAUSE - 1
 
 
264
        """Return this bar to its progress stack."""
 
 
266
        self._stack.return_pb(self)
 
 
268
    def note(self, fmt_string, *args, **kwargs):
 
 
269
        """Record a note without disrupting the progress bar."""
 
 
271
        self.to_messages_file.write(fmt_string % args)
 
 
272
        self.to_messages_file.write('\n')
 
 
274
    def child_progress(self, **kwargs):
 
 
275
        return ChildProgress(**kwargs)
 
 
278
class DummyProgress(_BaseProgressBar):
 
 
279
    """Progress-bar standin that does nothing.
 
 
281
    This can be used as the default argument for methods that
 
 
282
    take an optional progress indicator."""
 
 
287
    def update(self, msg=None, current=None, total=None):
 
 
290
    def child_update(self, message, current, total):
 
 
296
    def note(self, fmt_string, *args, **kwargs):
 
 
297
        """See _BaseProgressBar.note()."""
 
 
299
    def child_progress(self, **kwargs):
 
 
300
        return DummyProgress(**kwargs)
 
 
303
class DotsProgressBar(_BaseProgressBar):
 
 
305
    def __init__(self, **kwargs):
 
 
306
        _BaseProgressBar.__init__(self, **kwargs)
 
 
313
    def update(self, msg=None, current_cnt=None, total_cnt=None):
 
 
314
        if msg and msg != self.last_msg:
 
 
316
                self.to_file.write('\n')
 
 
317
            self.to_file.write(msg + ': ')
 
 
320
        self.to_file.write('.')
 
 
324
            self.to_file.write('\n')
 
 
327
    def child_update(self, message, current, total):
 
 
333
class TTYProgressBar(_BaseProgressBar):
 
 
334
    """Progress bar display object.
 
 
336
    Several options are available to control the display.  These can
 
 
337
    be passed as parameters to the constructor or assigned at any time:
 
 
340
        Show percentage complete.
 
 
342
        Show rotating baton.  This ticks over on every update even
 
 
343
        if the values don't change.
 
 
345
        Show predicted time-to-completion.
 
 
349
        Show numerical counts.
 
 
351
    The output file should be in line-buffered or unbuffered mode.
 
 
356
    def __init__(self, **kwargs):
 
 
357
        from bzrlib.osutils import terminal_width
 
 
358
        _BaseProgressBar.__init__(self, **kwargs)
 
 
360
        self.width = terminal_width()
 
 
361
        self.last_updates = []
 
 
362
        self._max_last_updates = 10
 
 
363
        self.child_fraction = 0
 
 
364
        self._have_output = False
 
 
366
    def throttle(self, old_msg):
 
 
367
        """Return True if the bar was updated too recently"""
 
 
368
        # time.time consistently takes 40/4000 ms = 0.01 ms.
 
 
369
        # time.clock() is faster, but gives us CPU time, not wall-clock time
 
 
371
        if self.start_time is not None and (now - self.start_time) < 1:
 
 
373
        if old_msg != self.last_msg:
 
 
375
        interval = now - self.last_update
 
 
377
        if interval < self.MIN_PAUSE:
 
 
380
        self.last_updates.append(now - self.last_update)
 
 
381
        # Don't let the queue grow without bound
 
 
382
        self.last_updates = self.last_updates[-self._max_last_updates:]
 
 
383
        self.last_update = now
 
 
387
        self.update(self.last_msg, self.last_cnt, self.last_total,
 
 
390
    def child_update(self, message, current, total):
 
 
391
        if current is not None and total != 0:
 
 
392
            child_fraction = float(current) / total
 
 
393
            if self.last_cnt is None:
 
 
395
            elif self.last_cnt + child_fraction <= self.last_total:
 
 
396
                self.child_fraction = child_fraction
 
 
397
        if self.last_msg is None:
 
 
401
    def update(self, msg, current_cnt=None, total_cnt=None,
 
 
403
        """Update and redraw progress bar.
 
 
408
        if total_cnt is None:
 
 
409
            total_cnt = self.last_total
 
 
414
        if current_cnt > total_cnt:
 
 
415
            total_cnt = current_cnt
 
 
417
        ## # optional corner case optimisation 
 
 
418
        ## # currently does not seem to fire so costs more than saved.
 
 
419
        ## # trivial optimal case:
 
 
420
        ## # NB if callers are doing a clear and restore with
 
 
421
        ## # the saved values, this will prevent that:
 
 
422
        ## # in that case add a restore method that calls
 
 
423
        ## # _do_update or some such
 
 
424
        ## if (self.last_msg == msg and
 
 
425
        ##     self.last_cnt == current_cnt and
 
 
426
        ##     self.last_total == total_cnt and
 
 
427
        ##     self.child_fraction == child_fraction):
 
 
433
        old_msg = self.last_msg
 
 
434
        # save these for the tick() function
 
 
436
        self.last_cnt = current_cnt
 
 
437
        self.last_total = total_cnt
 
 
438
        self.child_fraction = child_fraction
 
 
440
        # each function call takes 20ms/4000 = 0.005 ms, 
 
 
441
        # but multiple that by 4000 calls -> starts to cost.
 
 
442
        # so anything to make this function call faster
 
 
443
        # will improve base 'diff' time by up to 0.1 seconds.
 
 
444
        if self.throttle(old_msg):
 
 
447
        if self.show_eta and self.start_time and self.last_total:
 
 
448
            eta = get_eta(self.start_time, self.last_cnt + self.child_fraction, 
 
 
449
                    self.last_total, last_updates = self.last_updates)
 
 
450
            eta_str = " " + str_tdelta(eta)
 
 
454
        if self.show_spinner:
 
 
455
            spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '            
 
 
459
        # always update this; it's also used for the bar
 
 
462
        if self.show_pct and self.last_total and self.last_cnt:
 
 
463
            pct = 100.0 * ((self.last_cnt + self.child_fraction) / self.last_total)
 
 
464
            pct_str = ' (%5.1f%%)' % pct
 
 
468
        if not self.show_count:
 
 
470
        elif self.last_cnt is None:
 
 
472
        elif self.last_total is None:
 
 
473
            count_str = ' %i' % (self.last_cnt)
 
 
475
            # make both fields the same size
 
 
476
            t = '%i' % (self.last_total)
 
 
477
            c = '%*i' % (len(t), self.last_cnt)
 
 
478
            count_str = ' ' + c + '/' + t
 
 
481
            # progress bar, if present, soaks up all remaining space
 
 
482
            cols = self.width - 1 - len(self.last_msg) - len(spin_str) - len(pct_str) \
 
 
483
                   - len(eta_str) - len(count_str) - 3
 
 
486
                # number of markers highlighted in bar
 
 
487
                markers = int(round(float(cols) * 
 
 
488
                              (self.last_cnt + self.child_fraction) / self.last_total))
 
 
489
                bar_str = '[' + ('=' * markers).ljust(cols) + '] '
 
 
491
                # don't know total, so can't show completion.
 
 
492
                # so just show an expanded spinning thingy
 
 
493
                m = self.spin_pos % cols
 
 
494
                ms = (' ' * m + '*').ljust(cols)
 
 
496
                bar_str = '[' + ms + '] '
 
 
502
        m = spin_str + bar_str + self.last_msg + count_str \
 
 
504
        self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
 
 
505
        self._have_output = True
 
 
506
        #self.to_file.flush()
 
 
509
        if self._have_output:
 
 
510
            self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
 
 
511
        self._have_output = False
 
 
512
        #self.to_file.flush()        
 
 
517
class ChildProgress(_BaseProgressBar):
 
 
518
    """A progress indicator that pushes its data to the parent"""
 
 
520
    def __init__(self, _stack, **kwargs):
 
 
521
        _BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
 
 
522
        self.parent = _stack.top()
 
 
525
        self.child_fraction = 0
 
 
528
    def update(self, msg, current_cnt=None, total_cnt=None):
 
 
529
        self.current = current_cnt
 
 
530
        if total_cnt is not None:
 
 
531
            self.total = total_cnt
 
 
533
        self.child_fraction = 0
 
 
536
    def child_update(self, message, current, total):
 
 
537
        if current is None or total == 0:
 
 
538
            self.child_fraction = 0
 
 
540
            self.child_fraction = float(current) / total
 
 
544
        if self.current is None:
 
 
547
            count = self.current+self.child_fraction
 
 
548
            if count > self.total:
 
 
550
                    mutter('clamping count of %d to %d' % (count, self.total))
 
 
552
        self.parent.child_update(self.message, count, self.total)
 
 
557
    def note(self, *args, **kwargs):
 
 
558
        self.parent.note(*args, **kwargs)
 
 
561
class InstrumentedProgress(TTYProgressBar):
 
 
562
    """TTYProgress variant that tracks outcomes"""
 
 
564
    def __init__(self, *args, **kwargs):
 
 
565
        self.always_throttled = True
 
 
566
        self.never_throttle = False
 
 
567
        TTYProgressBar.__init__(self, *args, **kwargs)
 
 
569
    def throttle(self, old_message):
 
 
570
        if self.never_throttle:
 
 
573
            result = TTYProgressBar.throttle(self, old_message)
 
 
575
            self.always_throttled = False
 
 
578
def str_tdelta(delt):
 
 
581
    delt = int(round(delt))
 
 
582
    return '%d:%02d:%02d' % (delt/3600,
 
 
587
def get_eta(start_time, current, total, enough_samples=3, last_updates=None, n_recent=10):
 
 
588
    if start_time is None:
 
 
594
    if current < enough_samples:
 
 
600
    elapsed = time.time() - start_time
 
 
602
    if elapsed < 2.0:                   # not enough time to estimate
 
 
605
    total_duration = float(elapsed) * float(total) / float(current)
 
 
607
    if last_updates and len(last_updates) >= n_recent:
 
 
608
        avg = sum(last_updates) / float(len(last_updates))
 
 
609
        time_left = avg * (total - current)
 
 
611
        old_time_left = total_duration - elapsed
 
 
613
        # We could return the average, or some other value here
 
 
614
        return (time_left + old_time_left) / 2
 
 
616
    return total_duration - elapsed
 
 
619
class ProgressPhase(object):
 
 
620
    """Update progress object with the current phase"""
 
 
621
    def __init__(self, message, total, pb):
 
 
622
        object.__init__(self)
 
 
624
        self.message = message
 
 
626
        self.cur_phase = None
 
 
628
    def next_phase(self):
 
 
629
        if self.cur_phase is None:
 
 
633
        self.pb.update(self.message, self.cur_phase, self.total)
 
 
636
_progress_bar_types = {}
 
 
637
_progress_bar_types['dummy'] = DummyProgress
 
 
638
_progress_bar_types['none'] = DummyProgress
 
 
639
_progress_bar_types['tty'] = TTYProgressBar
 
 
640
_progress_bar_types['dots'] = DotsProgressBar