~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/progress.py

add a clean target

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# Copyright (C) 2005 Aaron Bentley <aaron.bentley@utoronto.ca>
2
 
# Copyright (C) 2005, 2006 Canonical <canonical.com>
 
2
# Copyright (C) 2005 Canonical <canonical.com>
3
3
#
4
4
#    This program is free software; you can redistribute it and/or modify
5
5
#    it under the terms of the GNU General Public License as published by
39
39
import sys
40
40
import time
41
41
import os
42
 
from collections import deque
43
 
 
44
 
 
45
 
import bzrlib.errors as errors
46
 
from bzrlib.trace import mutter 
 
42
 
 
43
 
 
44
def _width():
 
45
    """Return estimated terminal width.
 
46
 
 
47
    TODO: Do something smart on Windows?
 
48
 
 
49
    TODO: Is there anything that gets a better update when the window
 
50
          is resized while the program is running?
 
51
    """
 
52
    try:
 
53
        return int(os.environ['COLUMNS'])
 
54
    except (IndexError, KeyError, ValueError):
 
55
        return 80
47
56
 
48
57
 
49
58
def _supports_progress(f):
58
67
 
59
68
 
60
69
 
61
 
def ProgressBar(to_file=None, **kwargs):
 
70
def ProgressBar(to_file=sys.stderr, **kwargs):
62
71
    """Abstract factory"""
63
 
    if to_file is None:
64
 
        to_file = sys.stderr
65
72
    if _supports_progress(to_file):
66
73
        return TTYProgressBar(to_file=to_file, **kwargs)
67
74
    else:
68
75
        return DotsProgressBar(to_file=to_file, **kwargs)
69
76
    
70
 
 
71
 
class ProgressBarStack(object):
72
 
    """A stack of progress bars."""
73
 
 
74
 
    def __init__(self,
75
 
                 to_file=None,
76
 
                 show_pct=False,
77
 
                 show_spinner=True,
78
 
                 show_eta=False,
79
 
                 show_bar=True,
80
 
                 show_count=True,
81
 
                 to_messages_file=None,
82
 
                 klass=None):
83
 
        """Setup the stack with the parameters the progress bars should have."""
84
 
        if to_file is None:
85
 
            to_file = sys.stderr
86
 
        if to_messages_file is None:
87
 
            to_messages_file = sys.stdout
88
 
        self._to_file = to_file
89
 
        self._show_pct = show_pct
90
 
        self._show_spinner = show_spinner
91
 
        self._show_eta = show_eta
92
 
        self._show_bar = show_bar
93
 
        self._show_count = show_count
94
 
        self._to_messages_file = to_messages_file
95
 
        self._stack = []
96
 
        self._klass = klass or TTYProgressBar
97
 
 
98
 
    def top(self):
99
 
        if len(self._stack) != 0:
100
 
            return self._stack[-1]
101
 
        else:
102
 
            return None
103
 
 
104
 
    def bottom(self):
105
 
        if len(self._stack) != 0:
106
 
            return self._stack[0]
107
 
        else:
108
 
            return None
109
 
 
110
 
    def get_nested(self):
111
 
        """Return a nested progress bar."""
112
 
        if len(self._stack) == 0:
113
 
            func = self._klass
114
 
        else:
115
 
            func = self.top().child_progress
116
 
        new_bar = func(to_file=self._to_file,
117
 
                       show_pct=self._show_pct,
118
 
                       show_spinner=self._show_spinner,
119
 
                       show_eta=self._show_eta,
120
 
                       show_bar=self._show_bar,
121
 
                       show_count=self._show_count,
122
 
                       to_messages_file=self._to_messages_file,
123
 
                       _stack=self)
124
 
        self._stack.append(new_bar)
125
 
        return new_bar
126
 
 
127
 
    def return_pb(self, bar):
128
 
        """Return bar after its been used."""
129
 
        if bar is not self._stack[-1]:
130
 
            raise errors.MissingProgressBarFinish()
131
 
        self._stack.pop()
132
 
 
133
 
 
 
77
    
134
78
class _BaseProgressBar(object):
135
 
 
136
79
    def __init__(self,
137
 
                 to_file=None,
 
80
                 to_file=sys.stderr,
138
81
                 show_pct=False,
139
82
                 show_spinner=False,
140
83
                 show_eta=True,
141
84
                 show_bar=True,
142
 
                 show_count=True,
143
 
                 to_messages_file=None,
144
 
                 _stack=None):
 
85
                 show_count=True):
145
86
        object.__init__(self)
146
 
        if to_file is None:
147
 
            to_file = sys.stderr
148
 
        if to_messages_file is None:
149
 
            to_messages_file = sys.stdout
150
87
        self.to_file = to_file
151
 
        self.to_messages_file = to_messages_file
 
88
 
152
89
        self.last_msg = None
153
90
        self.last_cnt = None
154
91
        self.last_total = None
157
94
        self.show_eta = show_eta
158
95
        self.show_bar = show_bar
159
96
        self.show_count = show_count
160
 
        self._stack = _stack
161
 
        # seed throttler
162
 
        self.MIN_PAUSE = 0.1 # seconds
163
 
        now = time.clock()
164
 
        # starting now
165
 
        self.start_time = now
166
 
        # next update should not throttle
167
 
        self.last_update = now - self.MIN_PAUSE - 1
168
 
 
169
 
    def finished(self):
170
 
        """Return this bar to its progress stack."""
171
 
        self.clear()
172
 
        assert self._stack is not None
173
 
        self._stack.return_pb(self)
174
 
 
175
 
    def note(self, fmt_string, *args, **kwargs):
176
 
        """Record a note without disrupting the progress bar."""
177
 
        self.clear()
178
 
        self.to_messages_file.write(fmt_string % args)
179
 
        self.to_messages_file.write('\n')
180
 
 
181
 
    def child_progress(self, **kwargs):
182
 
        return ChildProgress(**kwargs)
 
97
 
183
98
 
184
99
 
185
100
class DummyProgress(_BaseProgressBar):
193
108
    def update(self, msg=None, current=None, total=None):
194
109
        pass
195
110
 
196
 
    def child_update(self, message, current, total):
197
 
        pass
198
 
 
199
111
    def clear(self):
200
112
        pass
201
113
        
202
 
    def note(self, fmt_string, *args, **kwargs):
203
 
        """See _BaseProgressBar.note()."""
204
 
 
205
 
    def child_progress(self, **kwargs):
206
 
        return DummyProgress(**kwargs)
207
 
 
208
 
 
 
114
    
209
115
class DotsProgressBar(_BaseProgressBar):
210
 
 
211
116
    def __init__(self, **kwargs):
212
117
        _BaseProgressBar.__init__(self, **kwargs)
213
118
        self.last_msg = None
220
125
        if msg and msg != self.last_msg:
221
126
            if self.need_nl:
222
127
                self.to_file.write('\n')
 
128
            
223
129
            self.to_file.write(msg + ': ')
224
130
            self.last_msg = msg
225
131
        self.need_nl = True
228
134
    def clear(self):
229
135
        if self.need_nl:
230
136
            self.to_file.write('\n')
231
 
        self.need_nl = False
232
137
        
233
 
    def child_update(self, message, current, total):
234
 
        self.tick()
235
 
 
236
138
    
237
139
class TTYProgressBar(_BaseProgressBar):
238
140
    """Progress bar display object.
255
157
    The output file should be in line-buffered or unbuffered mode.
256
158
    """
257
159
    SPIN_CHARS = r'/-\|'
 
160
    MIN_PAUSE = 0.1 # seconds
258
161
 
259
162
 
260
163
    def __init__(self, **kwargs):
261
 
        from bzrlib.osutils import terminal_width
262
164
        _BaseProgressBar.__init__(self, **kwargs)
263
165
        self.spin_pos = 0
264
 
        self.width = terminal_width()
 
166
        self.width = _width()
265
167
        self.start_time = None
266
 
        self.last_updates = deque()
267
 
        self.child_fraction = 0
 
168
        self.last_update = None
268
169
    
269
170
 
270
171
    def throttle(self):
271
172
        """Return True if the bar was updated too recently"""
272
 
        # time.time consistently takes 40/4000 ms = 0.01 ms.
273
 
        # but every single update to the pb invokes it.
274
 
        # so we use time.clock which takes 20/4000 ms = 0.005ms
275
 
        # on the downside, time.clock() appears to have approximately
276
 
        # 10ms granularity, so we treat a zero-time change as 'throttled.'
277
 
        
278
 
        now = time.clock()
279
 
        interval = now - self.last_update
280
 
        # if interval > 0
281
 
        if interval < self.MIN_PAUSE:
282
 
            return True
 
173
        now = time.time()
 
174
        if self.start_time is None:
 
175
            self.start_time = self.last_update = now
 
176
            return False
 
177
        else:
 
178
            interval = now - self.last_update
 
179
            if interval > 0 and interval < self.MIN_PAUSE:
 
180
                return True
283
181
 
284
 
        self.last_updates.append(now - self.last_update)
285
182
        self.last_update = now
286
183
        return False
287
184
        
 
185
 
288
186
    def tick(self):
289
 
        self.update(self.last_msg, self.last_cnt, self.last_total, 
290
 
                    self.child_fraction)
291
 
 
292
 
    def child_update(self, message, current, total):
293
 
        if current is not None and total != 0:
294
 
            child_fraction = float(current) / total
295
 
            if self.last_cnt is None:
296
 
                pass
297
 
            elif self.last_cnt + child_fraction <= self.last_total:
298
 
                self.child_fraction = child_fraction
299
 
            else:
300
 
                mutter('not updating child fraction')
301
 
        if self.last_msg is None:
302
 
            self.last_msg = ''
303
 
        self.tick()
304
 
 
305
 
    def update(self, msg, current_cnt=None, total_cnt=None, 
306
 
               child_fraction=0):
 
187
        self.update(self.last_msg, self.last_cnt, self.last_total)
 
188
                 
 
189
 
 
190
 
 
191
    def update(self, msg, current_cnt=None, total_cnt=None):
307
192
        """Update and redraw progress bar."""
308
 
        if msg is None:
309
 
            msg = self.last_msg
310
 
 
311
 
        if total_cnt is None:
312
 
            total_cnt = self.last_total
313
 
 
314
 
        if current_cnt < 0:
315
 
            current_cnt = 0
316
 
            
317
 
        if current_cnt > total_cnt:
318
 
            total_cnt = current_cnt
319
 
        
320
 
        ## # optional corner case optimisation 
321
 
        ## # currently does not seem to fire so costs more than saved.
322
 
        ## # trivial optimal case:
323
 
        ## # NB if callers are doing a clear and restore with
324
 
        ## # the saved values, this will prevent that:
325
 
        ## # in that case add a restore method that calls
326
 
        ## # _do_update or some such
327
 
        ## if (self.last_msg == msg and
328
 
        ##     self.last_cnt == current_cnt and
329
 
        ##     self.last_total == total_cnt and
330
 
        ##     self.child_fraction == child_fraction):
331
 
        ##     return
332
 
 
333
 
        old_msg = self.last_msg
 
193
 
334
194
        # save these for the tick() function
335
195
        self.last_msg = msg
336
196
        self.last_cnt = current_cnt
337
197
        self.last_total = total_cnt
338
 
        self.child_fraction = child_fraction
339
 
 
340
 
        # each function call takes 20ms/4000 = 0.005 ms, 
341
 
        # but multiple that by 4000 calls -> starts to cost.
342
 
        # so anything to make this function call faster
343
 
        # will improve base 'diff' time by up to 0.1 seconds.
344
 
        if old_msg == self.last_msg and self.throttle():
345
 
            return
346
 
 
347
 
        if self.show_eta and self.start_time and self.last_total:
348
 
            eta = get_eta(self.start_time, self.last_cnt + self.child_fraction, 
349
 
                    self.last_total, last_updates = self.last_updates)
 
198
            
 
199
        if self.throttle():
 
200
            return 
 
201
        
 
202
        if total_cnt:
 
203
            assert current_cnt <= total_cnt
 
204
        if current_cnt:
 
205
            assert current_cnt >= 0
 
206
        
 
207
        if self.show_eta and self.start_time and total_cnt:
 
208
            eta = get_eta(self.start_time, current_cnt, total_cnt)
350
209
            eta_str = " " + str_tdelta(eta)
351
210
        else:
352
211
            eta_str = ""
359
218
        # always update this; it's also used for the bar
360
219
        self.spin_pos += 1
361
220
 
362
 
        if self.show_pct and self.last_total and self.last_cnt:
363
 
            pct = 100.0 * ((self.last_cnt + self.child_fraction) / self.last_total)
 
221
        if self.show_pct and total_cnt and current_cnt:
 
222
            pct = 100.0 * current_cnt / total_cnt
364
223
            pct_str = ' (%5.1f%%)' % pct
365
224
        else:
366
225
            pct_str = ''
367
226
 
368
227
        if not self.show_count:
369
228
            count_str = ''
370
 
        elif self.last_cnt is None:
 
229
        elif current_cnt is None:
371
230
            count_str = ''
372
 
        elif self.last_total is None:
373
 
            count_str = ' %i' % (self.last_cnt)
 
231
        elif total_cnt is None:
 
232
            count_str = ' %i' % (current_cnt)
374
233
        else:
375
234
            # make both fields the same size
376
 
            t = '%i' % (self.last_total)
377
 
            c = '%*i' % (len(t), self.last_cnt)
 
235
            t = '%i' % (total_cnt)
 
236
            c = '%*i' % (len(t), current_cnt)
378
237
            count_str = ' ' + c + '/' + t 
379
238
 
380
239
        if self.show_bar:
381
240
            # progress bar, if present, soaks up all remaining space
382
 
            cols = self.width - 1 - len(self.last_msg) - len(spin_str) - len(pct_str) \
 
241
            cols = self.width - 1 - len(msg) - len(spin_str) - len(pct_str) \
383
242
                   - len(eta_str) - len(count_str) - 3
384
243
 
385
 
            if self.last_total:
 
244
            if total_cnt:
386
245
                # number of markers highlighted in bar
387
 
                markers = int(round(float(cols) * 
388
 
                              (self.last_cnt + self.child_fraction) / self.last_total))
 
246
                markers = int(round(float(cols) * current_cnt / total_cnt))
389
247
                bar_str = '[' + ('=' * markers).ljust(cols) + '] '
390
248
            elif False:
391
249
                # don't know total, so can't show completion.
399
257
        else:
400
258
            bar_str = ''
401
259
 
402
 
        m = spin_str + bar_str + self.last_msg + count_str + pct_str + eta_str
 
260
        m = spin_str + bar_str + msg + count_str + pct_str + eta_str
403
261
 
404
262
        assert len(m) < self.width
405
263
        self.to_file.write('\r' + m.ljust(self.width - 1))
406
264
        #self.to_file.flush()
407
265
            
 
266
 
408
267
    def clear(self):        
409
268
        self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
410
269
        #self.to_file.flush()        
411
 
 
412
 
 
413
 
class ChildProgress(_BaseProgressBar):
414
 
    """A progress indicator that pushes its data to the parent"""
415
 
 
416
 
    def __init__(self, _stack, **kwargs):
417
 
        _BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
418
 
        self.parent = _stack.top()
419
 
        self.current = None
420
 
        self.total = None
421
 
        self.child_fraction = 0
422
 
        self.message = None
423
 
 
424
 
    def update(self, msg, current_cnt=None, total_cnt=None):
425
 
        self.current = current_cnt
426
 
        self.total = total_cnt
427
 
        self.message = msg
428
 
        self.child_fraction = 0
429
 
        self.tick()
430
 
 
431
 
    def child_update(self, message, current, total):
432
 
        if current is None or total == 0:
433
 
            self.child_fraction = 0
434
 
        else:
435
 
            self.child_fraction = float(current) / total
436
 
        self.tick()
437
 
 
438
 
    def tick(self):
439
 
        if self.current is None:
440
 
            count = None
441
 
        else:
442
 
            count = self.current+self.child_fraction
443
 
            if count > self.total:
444
 
                if __debug__:
445
 
                    mutter('clamping count of %d to %d' % (count, self.total))
446
 
                count = self.total
447
 
        self.parent.child_update(self.message, count, self.total)
448
 
 
449
 
    def clear(self):
450
 
        pass
451
 
 
452
 
    def note(self, *args, **kwargs):
453
 
        self.parent.note(*args, **kwargs)
454
 
 
455
 
 
 
270
    
 
271
 
 
272
        
456
273
def str_tdelta(delt):
457
274
    if delt is None:
458
275
        return "-:--:--"
462
279
                             delt % 60)
463
280
 
464
281
 
465
 
def get_eta(start_time, current, total, enough_samples=3, last_updates=None, n_recent=10):
 
282
def get_eta(start_time, current, total, enough_samples=3):
466
283
    if start_time is None:
467
284
        return None
468
285
 
475
292
    if current > total:
476
293
        return None                     # wtf?
477
294
 
478
 
    elapsed = time.clock() - start_time
 
295
    elapsed = time.time() - start_time
479
296
 
480
297
    if elapsed < 2.0:                   # not enough time to estimate
481
298
        return None
484
301
 
485
302
    assert total_duration >= elapsed
486
303
 
487
 
    if last_updates and len(last_updates) >= n_recent:
488
 
        while len(last_updates) > n_recent:
489
 
            last_updates.popleft()
490
 
        avg = sum(last_updates) / float(len(last_updates))
491
 
        time_left = avg * (total - current)
492
 
 
493
 
        old_time_left = total_duration - elapsed
494
 
 
495
 
        # We could return the average, or some other value here
496
 
        return (time_left + old_time_left) / 2
497
 
 
498
304
    return total_duration - elapsed
499
305
 
500
306
 
501
 
class ProgressPhase(object):
502
 
    """Update progress object with the current phase"""
503
 
    def __init__(self, message, total, pb):
504
 
        object.__init__(self)
505
 
        self.pb = pb
506
 
        self.message = message
507
 
        self.total = total
508
 
        self.cur_phase = None
509
 
 
510
 
    def next_phase(self):
511
 
        if self.cur_phase is None:
512
 
            self.cur_phase = 0
513
 
        else:
514
 
            self.cur_phase += 1
515
 
        assert self.cur_phase < self.total 
516
 
        self.pb.update(self.message, self.cur_phase, self.total)
517
 
 
518
 
 
519
307
def run_tests():
520
308
    import doctest
521
309
    result = doctest.testmod()