/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/progress.py

  • Committer: Martin Pool
  • Date: 2009-01-21 05:49:18 UTC
  • mto: This revision was merged to the branch mainline in revision 3956.
  • Revision ID: mbp@sourcefrog.net-20090121054918-174smoskf8srdm41
rename to _progress_all_finished

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006, 2008, 2009 Canonical Ltd
 
2
#
 
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.
 
7
#
 
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.
 
12
#
 
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
 
16
 
 
17
 
 
18
"""Progress indicators.
 
19
 
 
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.
 
22
"""
 
23
 
 
24
 
 
25
import sys
 
26
import time
 
27
import os
 
28
import warnings
 
29
 
 
30
 
 
31
from bzrlib import (
 
32
    errors,
 
33
    osutils,
 
34
    trace,
 
35
    ui,
 
36
    )
 
37
from bzrlib.trace import mutter
 
38
 
 
39
 
 
40
def _supports_progress(f):
 
41
    """Detect if we can use pretty progress bars on the output stream f.
 
42
 
 
43
    If this returns true we expect that a human may be looking at that 
 
44
    output, and that we can repaint a line to update it.
 
45
    """
 
46
    isatty = getattr(f, 'isatty', None)
 
47
    if isatty is None:
 
48
        return False
 
49
    if not isatty():
 
50
        return False
 
51
    if os.environ.get('TERM') == 'dumb':
 
52
        # e.g. emacs compile window
 
53
        return False
 
54
    return True
 
55
 
 
56
 
 
57
class ProgressTask(object):
 
58
    """Model component of a progress indicator.
 
59
 
 
60
    Most code that needs to indicate progress should update one of these, 
 
61
    and it will in turn update the display, if one is present.
 
62
 
 
63
    Code updating the task may also set fields as hints about how to display
 
64
    it: show_pct, show_spinner, show_eta, show_count, show_bar.  UIs
 
65
    will not necessarily respect all these fields.
 
66
    """
 
67
 
 
68
    def __init__(self, parent_task=None, ui_factory=None):
 
69
        self._parent_task = parent_task
 
70
        self._last_update = 0
 
71
        self.total_cnt = None
 
72
        self.current_cnt = None
 
73
        self.msg = ''
 
74
        self.ui_factory = ui_factory
 
75
        self.show_pct = False
 
76
        self.show_spinner = True
 
77
        self.show_eta = False,
 
78
        self.show_count = True
 
79
        self.show_bar = True
 
80
 
 
81
    def __repr__(self):
 
82
        return '%s(%r/%r, msg=%r)' % (
 
83
            self.__class__.__name__,
 
84
            self.current_cnt,
 
85
            self.total_cnt,
 
86
            self.msg)
 
87
 
 
88
    def update(self, msg, current_cnt=None, total_cnt=None):
 
89
        self.msg = msg
 
90
        self.current_cnt = current_cnt
 
91
        if total_cnt:
 
92
            self.total_cnt = total_cnt
 
93
        self.ui_factory._progress_updated(self)
 
94
 
 
95
    def tick(self):
 
96
        self.update(self.msg)
 
97
 
 
98
    def finished(self):
 
99
        self.ui_factory._progress_finished(self)
 
100
 
 
101
    def make_sub_task(self):
 
102
        return ProgressTask(self, self.ui_factory)
 
103
 
 
104
    def _overall_completion_fraction(self, child_fraction=0.0):
 
105
        """Return fractional completion of this task and its parents
 
106
        
 
107
        Returns None if no completion can be computed."""
 
108
        if self.total_cnt:
 
109
            own_fraction = (float(self.current_cnt) + child_fraction) / self.total_cnt
 
110
        else:
 
111
            own_fraction = None
 
112
        if self._parent_task is None:
 
113
            return own_fraction
 
114
        else:
 
115
            if own_fraction is None:
 
116
                own_fraction = 0.0
 
117
            return self._parent_task._overall_completion_fraction(own_fraction)
 
118
 
 
119
    def note(self, fmt_string, *args):
 
120
        """Record a note without disrupting the progress bar."""
 
121
        # XXX: shouldn't be here; put it in mutter or the ui instead
 
122
        if args:
 
123
            self.ui_factory.note(fmt_string % args)
 
124
        else:
 
125
            self.ui_factory.note(fmt_string)
 
126
 
 
127
    def clear(self):
 
128
        # XXX: shouldn't be here; put it in mutter or the ui instead
 
129
        self.ui_factory.clear_term()
 
130
 
 
131
 
 
132
def ProgressBar(to_file=None, **kwargs):
 
133
    """Abstract factory"""
 
134
    if to_file is None:
 
135
        to_file = sys.stderr
 
136
    requested_bar_type = os.environ.get('BZR_PROGRESS_BAR')
 
137
    # An value of '' or not set reverts to standard processing
 
138
    if requested_bar_type in (None, ''):
 
139
        if _supports_progress(to_file):
 
140
            return TTYProgressBar(to_file=to_file, **kwargs)
 
141
        else:
 
142
            return DummyProgress(to_file=to_file, **kwargs)
 
143
    else:
 
144
        # Minor sanitation to prevent spurious errors
 
145
        requested_bar_type = requested_bar_type.lower().strip()
 
146
        # TODO: jam 20060710 Arguably we shouldn't raise an exception
 
147
        #       but should instead just disable progress bars if we
 
148
        #       don't recognize the type
 
149
        if requested_bar_type not in _progress_bar_types:
 
150
            raise errors.InvalidProgressBarType(requested_bar_type,
 
151
                                                _progress_bar_types.keys())
 
152
        return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
 
153
 
 
154
 
 
155
class ProgressBarStack(object):
 
156
    """A stack of progress bars."""
 
157
 
 
158
    def __init__(self,
 
159
                 to_file=None,
 
160
                 show_pct=False,
 
161
                 show_spinner=True,
 
162
                 show_eta=False,
 
163
                 show_bar=True,
 
164
                 show_count=True,
 
165
                 to_messages_file=None,
 
166
                 klass=None):
 
167
        """Setup the stack with the parameters the progress bars should have."""
 
168
        if to_file is None:
 
169
            to_file = sys.stderr
 
170
        if to_messages_file is None:
 
171
            to_messages_file = sys.stdout
 
172
        self._to_file = to_file
 
173
        self._show_pct = show_pct
 
174
        self._show_spinner = show_spinner
 
175
        self._show_eta = show_eta
 
176
        self._show_bar = show_bar
 
177
        self._show_count = show_count
 
178
        self._to_messages_file = to_messages_file
 
179
        self._stack = []
 
180
        self._klass = klass or ProgressBar
 
181
 
 
182
    def top(self):
 
183
        if len(self._stack) != 0:
 
184
            return self._stack[-1]
 
185
        else:
 
186
            return None
 
187
 
 
188
    def bottom(self):
 
189
        if len(self._stack) != 0:
 
190
            return self._stack[0]
 
191
        else:
 
192
            return None
 
193
 
 
194
    def get_nested(self):
 
195
        """Return a nested progress bar."""
 
196
        if len(self._stack) == 0:
 
197
            func = self._klass
 
198
        else:
 
199
            func = self.top().child_progress
 
200
        new_bar = func(to_file=self._to_file,
 
201
                       show_pct=self._show_pct,
 
202
                       show_spinner=self._show_spinner,
 
203
                       show_eta=self._show_eta,
 
204
                       show_bar=self._show_bar,
 
205
                       show_count=self._show_count,
 
206
                       to_messages_file=self._to_messages_file,
 
207
                       _stack=self)
 
208
        self._stack.append(new_bar)
 
209
        return new_bar
 
210
 
 
211
    def return_pb(self, bar):
 
212
        """Return bar after its been used."""
 
213
        if bar is not self._stack[-1]:
 
214
            warnings.warn("%r is not currently active" % (bar,))
 
215
        else:
 
216
            self._stack.pop()
 
217
 
 
218
 
 
219
class _BaseProgressBar(object):
 
220
 
 
221
    def __init__(self,
 
222
                 to_file=None,
 
223
                 show_pct=False,
 
224
                 show_spinner=False,
 
225
                 show_eta=False,
 
226
                 show_bar=True,
 
227
                 show_count=True,
 
228
                 to_messages_file=None,
 
229
                 _stack=None):
 
230
        object.__init__(self)
 
231
        if to_file is None:
 
232
            to_file = sys.stderr
 
233
        if to_messages_file is None:
 
234
            to_messages_file = sys.stdout
 
235
        self.to_file = to_file
 
236
        self.to_messages_file = to_messages_file
 
237
        self.last_msg = None
 
238
        self.last_cnt = None
 
239
        self.last_total = None
 
240
        self.show_pct = show_pct
 
241
        self.show_spinner = show_spinner
 
242
        self.show_eta = show_eta
 
243
        self.show_bar = show_bar
 
244
        self.show_count = show_count
 
245
        self._stack = _stack
 
246
        # seed throttler
 
247
        self.MIN_PAUSE = 0.1 # seconds
 
248
        now = time.time()
 
249
        # starting now
 
250
        self.start_time = now
 
251
        # next update should not throttle
 
252
        self.last_update = now - self.MIN_PAUSE - 1
 
253
 
 
254
    def finished(self):
 
255
        """Return this bar to its progress stack."""
 
256
        self.clear()
 
257
        self._stack.return_pb(self)
 
258
 
 
259
    def note(self, fmt_string, *args, **kwargs):
 
260
        """Record a note without disrupting the progress bar."""
 
261
        self.clear()
 
262
        self.to_messages_file.write(fmt_string % args)
 
263
        self.to_messages_file.write('\n')
 
264
 
 
265
    def child_progress(self, **kwargs):
 
266
        return ChildProgress(**kwargs)
 
267
 
 
268
 
 
269
class DummyProgress(_BaseProgressBar):
 
270
    """Progress-bar standin that does nothing.
 
271
 
 
272
    This can be used as the default argument for methods that
 
273
    take an optional progress indicator."""
 
274
 
 
275
    def tick(self):
 
276
        pass
 
277
 
 
278
    def update(self, msg=None, current=None, total=None):
 
279
        pass
 
280
 
 
281
    def child_update(self, message, current, total):
 
282
        pass
 
283
 
 
284
    def clear(self):
 
285
        pass
 
286
        
 
287
    def note(self, fmt_string, *args, **kwargs):
 
288
        """See _BaseProgressBar.note()."""
 
289
 
 
290
    def child_progress(self, **kwargs):
 
291
        return DummyProgress(**kwargs)
 
292
 
 
293
 
 
294
class DotsProgressBar(_BaseProgressBar):
 
295
 
 
296
    def __init__(self, **kwargs):
 
297
        _BaseProgressBar.__init__(self, **kwargs)
 
298
        self.last_msg = None
 
299
        self.need_nl = False
 
300
        
 
301
    def tick(self):
 
302
        self.update()
 
303
        
 
304
    def update(self, msg=None, current_cnt=None, total_cnt=None):
 
305
        if msg and msg != self.last_msg:
 
306
            if self.need_nl:
 
307
                self.to_file.write('\n')
 
308
            self.to_file.write(msg + ': ')
 
309
            self.last_msg = msg
 
310
        self.need_nl = True
 
311
        self.to_file.write('.')
 
312
        
 
313
    def clear(self):
 
314
        if self.need_nl:
 
315
            self.to_file.write('\n')
 
316
        self.need_nl = False
 
317
        
 
318
    def child_update(self, message, current, total):
 
319
        self.tick()
 
320
 
 
321
 
 
322
 
 
323
    
 
324
class TTYProgressBar(_BaseProgressBar):
 
325
    """Progress bar display object.
 
326
 
 
327
    Several options are available to control the display.  These can
 
328
    be passed as parameters to the constructor or assigned at any time:
 
329
 
 
330
    show_pct
 
331
        Show percentage complete.
 
332
    show_spinner
 
333
        Show rotating baton.  This ticks over on every update even
 
334
        if the values don't change.
 
335
    show_eta
 
336
        Show predicted time-to-completion.
 
337
    show_bar
 
338
        Show bar graph.
 
339
    show_count
 
340
        Show numerical counts.
 
341
 
 
342
    The output file should be in line-buffered or unbuffered mode.
 
343
    """
 
344
    SPIN_CHARS = r'/-\|'
 
345
 
 
346
 
 
347
    def __init__(self, **kwargs):
 
348
        from bzrlib.osutils import terminal_width
 
349
        _BaseProgressBar.__init__(self, **kwargs)
 
350
        self.spin_pos = 0
 
351
        self.width = terminal_width()
 
352
        self.last_updates = []
 
353
        self._max_last_updates = 10
 
354
        self.child_fraction = 0
 
355
        self._have_output = False
 
356
    
 
357
    def throttle(self, old_msg):
 
358
        """Return True if the bar was updated too recently"""
 
359
        # time.time consistently takes 40/4000 ms = 0.01 ms.
 
360
        # time.clock() is faster, but gives us CPU time, not wall-clock time
 
361
        now = time.time()
 
362
        if self.start_time is not None and (now - self.start_time) < 1:
 
363
            return True
 
364
        if old_msg != self.last_msg:
 
365
            return False
 
366
        interval = now - self.last_update
 
367
        # if interval > 0
 
368
        if interval < self.MIN_PAUSE:
 
369
            return True
 
370
 
 
371
        self.last_updates.append(now - self.last_update)
 
372
        # Don't let the queue grow without bound
 
373
        self.last_updates = self.last_updates[-self._max_last_updates:]
 
374
        self.last_update = now
 
375
        return False
 
376
        
 
377
    def tick(self):
 
378
        self.update(self.last_msg, self.last_cnt, self.last_total,
 
379
                    self.child_fraction)
 
380
 
 
381
    def child_update(self, message, current, total):
 
382
        if current is not None and total != 0:
 
383
            child_fraction = float(current) / total
 
384
            if self.last_cnt is None:
 
385
                pass
 
386
            elif self.last_cnt + child_fraction <= self.last_total:
 
387
                self.child_fraction = child_fraction
 
388
        if self.last_msg is None:
 
389
            self.last_msg = ''
 
390
        self.tick()
 
391
 
 
392
    def update(self, msg, current_cnt=None, total_cnt=None,
 
393
            child_fraction=0):
 
394
        """Update and redraw progress bar.
 
395
        """
 
396
        if msg is None:
 
397
            msg = self.last_msg
 
398
 
 
399
        if total_cnt is None:
 
400
            total_cnt = self.last_total
 
401
 
 
402
        if current_cnt < 0:
 
403
            current_cnt = 0
 
404
            
 
405
        if current_cnt > total_cnt:
 
406
            total_cnt = current_cnt
 
407
        
 
408
        ## # optional corner case optimisation 
 
409
        ## # currently does not seem to fire so costs more than saved.
 
410
        ## # trivial optimal case:
 
411
        ## # NB if callers are doing a clear and restore with
 
412
        ## # the saved values, this will prevent that:
 
413
        ## # in that case add a restore method that calls
 
414
        ## # _do_update or some such
 
415
        ## if (self.last_msg == msg and
 
416
        ##     self.last_cnt == current_cnt and
 
417
        ##     self.last_total == total_cnt and
 
418
        ##     self.child_fraction == child_fraction):
 
419
        ##     return
 
420
 
 
421
        if msg is None:
 
422
            msg = ''
 
423
 
 
424
        old_msg = self.last_msg
 
425
        # save these for the tick() function
 
426
        self.last_msg = msg
 
427
        self.last_cnt = current_cnt
 
428
        self.last_total = total_cnt
 
429
        self.child_fraction = child_fraction
 
430
 
 
431
        # each function call takes 20ms/4000 = 0.005 ms, 
 
432
        # but multiple that by 4000 calls -> starts to cost.
 
433
        # so anything to make this function call faster
 
434
        # will improve base 'diff' time by up to 0.1 seconds.
 
435
        if self.throttle(old_msg):
 
436
            return
 
437
 
 
438
        if self.show_eta and self.start_time and self.last_total:
 
439
            eta = get_eta(self.start_time, self.last_cnt + self.child_fraction, 
 
440
                    self.last_total, last_updates = self.last_updates)
 
441
            eta_str = " " + str_tdelta(eta)
 
442
        else:
 
443
            eta_str = ""
 
444
 
 
445
        if self.show_spinner:
 
446
            spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '            
 
447
        else:
 
448
            spin_str = ''
 
449
 
 
450
        # always update this; it's also used for the bar
 
451
        self.spin_pos += 1
 
452
 
 
453
        if self.show_pct and self.last_total and self.last_cnt:
 
454
            pct = 100.0 * ((self.last_cnt + self.child_fraction) / self.last_total)
 
455
            pct_str = ' (%5.1f%%)' % pct
 
456
        else:
 
457
            pct_str = ''
 
458
 
 
459
        if not self.show_count:
 
460
            count_str = ''
 
461
        elif self.last_cnt is None:
 
462
            count_str = ''
 
463
        elif self.last_total is None:
 
464
            count_str = ' %i' % (self.last_cnt)
 
465
        else:
 
466
            # make both fields the same size
 
467
            t = '%i' % (self.last_total)
 
468
            c = '%*i' % (len(t), self.last_cnt)
 
469
            count_str = ' ' + c + '/' + t
 
470
 
 
471
        if self.show_bar:
 
472
            # progress bar, if present, soaks up all remaining space
 
473
            cols = self.width - 1 - len(self.last_msg) - len(spin_str) - len(pct_str) \
 
474
                   - len(eta_str) - len(count_str) - 3
 
475
 
 
476
            if self.last_total:
 
477
                # number of markers highlighted in bar
 
478
                markers = int(round(float(cols) * 
 
479
                              (self.last_cnt + self.child_fraction) / self.last_total))
 
480
                bar_str = '[' + ('=' * markers).ljust(cols) + '] '
 
481
            elif False:
 
482
                # don't know total, so can't show completion.
 
483
                # so just show an expanded spinning thingy
 
484
                m = self.spin_pos % cols
 
485
                ms = (' ' * m + '*').ljust(cols)
 
486
                
 
487
                bar_str = '[' + ms + '] '
 
488
            else:
 
489
                bar_str = ''
 
490
        else:
 
491
            bar_str = ''
 
492
 
 
493
        m = spin_str + bar_str + self.last_msg + count_str \
 
494
            + pct_str + eta_str
 
495
        self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
 
496
        self._have_output = True
 
497
        #self.to_file.flush()
 
498
            
 
499
    def clear(self):
 
500
        if self._have_output:
 
501
            self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
 
502
        self._have_output = False
 
503
        #self.to_file.flush()        
 
504
 
 
505
 
 
506
 
 
507
 
 
508
class ChildProgress(_BaseProgressBar):
 
509
    """A progress indicator that pushes its data to the parent"""
 
510
 
 
511
    def __init__(self, _stack, **kwargs):
 
512
        _BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
 
513
        self.parent = _stack.top()
 
514
        self.current = None
 
515
        self.total = None
 
516
        self.child_fraction = 0
 
517
        self.message = None
 
518
 
 
519
    def update(self, msg, current_cnt=None, total_cnt=None):
 
520
        self.current = current_cnt
 
521
        if total_cnt is not None:
 
522
            self.total = total_cnt
 
523
        self.message = msg
 
524
        self.child_fraction = 0
 
525
        self.tick()
 
526
 
 
527
    def child_update(self, message, current, total):
 
528
        if current is None or total == 0:
 
529
            self.child_fraction = 0
 
530
        else:
 
531
            self.child_fraction = float(current) / total
 
532
        self.tick()
 
533
 
 
534
    def tick(self):
 
535
        if self.current is None:
 
536
            count = None
 
537
        else:
 
538
            count = self.current+self.child_fraction
 
539
            if count > self.total:
 
540
                if __debug__:
 
541
                    mutter('clamping count of %d to %d' % (count, self.total))
 
542
                count = self.total
 
543
        self.parent.child_update(self.message, count, self.total)
 
544
 
 
545
    def clear(self):
 
546
        pass
 
547
 
 
548
    def note(self, *args, **kwargs):
 
549
        self.parent.note(*args, **kwargs)
 
550
 
 
551
 
 
552
class InstrumentedProgress(TTYProgressBar):
 
553
    """TTYProgress variant that tracks outcomes"""
 
554
 
 
555
    def __init__(self, *args, **kwargs):
 
556
        self.always_throttled = True
 
557
        self.never_throttle = False
 
558
        TTYProgressBar.__init__(self, *args, **kwargs)
 
559
 
 
560
    def throttle(self, old_message):
 
561
        if self.never_throttle:
 
562
            result =  False
 
563
        else:
 
564
            result = TTYProgressBar.throttle(self, old_message)
 
565
        if result is False:
 
566
            self.always_throttled = False
 
567
 
 
568
 
 
569
def str_tdelta(delt):
 
570
    if delt is None:
 
571
        return "-:--:--"
 
572
    delt = int(round(delt))
 
573
    return '%d:%02d:%02d' % (delt/3600,
 
574
                             (delt/60) % 60,
 
575
                             delt % 60)
 
576
 
 
577
 
 
578
def get_eta(start_time, current, total, enough_samples=3, last_updates=None, n_recent=10):
 
579
    if start_time is None:
 
580
        return None
 
581
 
 
582
    if not total:
 
583
        return None
 
584
 
 
585
    if current < enough_samples:
 
586
        return None
 
587
 
 
588
    if current > total:
 
589
        return None                     # wtf?
 
590
 
 
591
    elapsed = time.time() - start_time
 
592
 
 
593
    if elapsed < 2.0:                   # not enough time to estimate
 
594
        return None
 
595
    
 
596
    total_duration = float(elapsed) * float(total) / float(current)
 
597
 
 
598
    if last_updates and len(last_updates) >= n_recent:
 
599
        avg = sum(last_updates) / float(len(last_updates))
 
600
        time_left = avg * (total - current)
 
601
 
 
602
        old_time_left = total_duration - elapsed
 
603
 
 
604
        # We could return the average, or some other value here
 
605
        return (time_left + old_time_left) / 2
 
606
 
 
607
    return total_duration - elapsed
 
608
 
 
609
 
 
610
class ProgressPhase(object):
 
611
    """Update progress object with the current phase"""
 
612
    def __init__(self, message, total, pb):
 
613
        object.__init__(self)
 
614
        self.pb = pb
 
615
        self.message = message
 
616
        self.total = total
 
617
        self.cur_phase = None
 
618
 
 
619
    def next_phase(self):
 
620
        if self.cur_phase is None:
 
621
            self.cur_phase = 0
 
622
        else:
 
623
            self.cur_phase += 1
 
624
        self.pb.update(self.message, self.cur_phase, self.total)
 
625
 
 
626
 
 
627
_progress_bar_types = {}
 
628
_progress_bar_types['dummy'] = DummyProgress
 
629
_progress_bar_types['none'] = DummyProgress
 
630
_progress_bar_types['tty'] = TTYProgressBar
 
631
_progress_bar_types['dots'] = DotsProgressBar