02db524e76118ad380f18ecca505ea922a8b4742
[platform/upstream/mic.git] / mic / urlgrabber / progress.py
1 #   This library is free software; you can redistribute it and/or
2 #   modify it under the terms of the GNU Lesser General Public
3 #   License as published by the Free Software Foundation; either
4 #   version 2.1 of the License, or (at your option) any later version.
5 #
6 #   This library is distributed in the hope that it will be useful,
7 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
8 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
9 #   Lesser General Public License for more details.
10 #
11 #   You should have received a copy of the GNU Lesser General Public
12 #   License along with this library; if not, write to the 
13 #      Free Software Foundation, Inc., 
14 #      59 Temple Place, Suite 330, 
15 #      Boston, MA  02111-1307  USA
16
17 # This file is part of urlgrabber, a high-level cross-protocol url-grabber
18 # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
19
20 # $Id: progress.py,v 1.7 2005/08/19 21:59:07 mstenner Exp $
21
22 import sys
23 import time
24 import math
25 import thread
26     
27 class BaseMeter:
28     def __init__(self):
29         self.update_period = 0.3 # seconds
30
31         self.filename   = None
32         self.url        = None
33         self.basename   = None
34         self.text       = None
35         self.size       = None
36         self.start_time = None
37         self.last_amount_read = 0
38         self.last_update_time = None
39         self.re = RateEstimator()
40         
41     def start(self, filename=None, url=None, basename=None,
42               size=None, now=None, text=None):
43         self.filename = filename
44         self.url      = url
45         self.basename = basename
46         self.text     = text
47
48         #size = None #########  TESTING
49         self.size = size
50         if not size is None: self.fsize = format_number(size) + 'B'
51
52         if now is None: now = time.time()
53         self.start_time = now
54         self.re.start(size, now)
55         self.last_amount_read = 0
56         self.last_update_time = now
57         self._do_start(now)
58         
59     def _do_start(self, now=None):
60         pass
61
62     def update(self, amount_read, now=None):
63         # for a real gui, you probably want to override and put a call
64         # to your mainloop iteration function here
65         if now is None: now = time.time()
66         if (now >= self.last_update_time + self.update_period) or \
67                not self.last_update_time:
68             self.re.update(amount_read, now)
69             self.last_amount_read = amount_read
70             self.last_update_time = now
71             self._do_update(amount_read, now)
72
73     def _do_update(self, amount_read, now=None):
74         pass
75
76     def end(self, amount_read, now=None):
77         if now is None: now = time.time()
78         self.re.update(amount_read, now)
79         self.last_amount_read = amount_read
80         self.last_update_time = now
81         self._do_end(amount_read, now)
82
83     def _do_end(self, amount_read, now=None):
84         pass
85         
86 class TextMeter(BaseMeter):
87     def __init__(self, fo=sys.stderr):
88         BaseMeter.__init__(self)
89         self.fo = fo
90
91     def _do_update(self, amount_read, now=None):
92         etime = self.re.elapsed_time()
93         fetime = format_time(etime)
94         fread = format_number(amount_read)
95         #self.size = None
96         if self.text is not None:
97             text = self.text
98         else:
99             text = self.basename
100         if self.size is None:
101             out = '\r%-60.60s    %5sB %s ' % \
102                   (text, fread, fetime)
103         else:
104             rtime = self.re.remaining_time()
105             frtime = format_time(rtime)
106             frac = self.re.fraction_read()
107             bar = '='*int(25 * frac)
108
109             out = '\r%-25.25s %3i%% |%-25.25s| %5sB %8s ETA ' % \
110                   (text, frac*100, bar, fread, frtime)
111
112         self.fo.write(out)
113         self.fo.flush()
114
115     def _do_end(self, amount_read, now=None):
116         total_time = format_time(self.re.elapsed_time())
117         total_size = format_number(amount_read)
118         if self.text is not None:
119             text = self.text
120         else:
121             text = self.basename
122         if self.size is None:
123             out = '\r%-60.60s    %5sB %s ' % \
124                   (text, total_size, total_time)
125         else:
126             bar = '='*25
127             out = '\r%-25.25s %3i%% |%-25.25s| %5sB %8s     ' % \
128                   (text, 100, bar, total_size, total_time)
129         self.fo.write(out + '\n')
130         self.fo.flush()
131
132 text_progress_meter = TextMeter
133
134 class MultiFileHelper(BaseMeter):
135     def __init__(self, master):
136         BaseMeter.__init__(self)
137         self.master = master
138
139     def _do_start(self, now):
140         self.master.start_meter(self, now)
141
142     def _do_update(self, amount_read, now):
143         # elapsed time since last update
144         self.master.update_meter(self, now)
145
146     def _do_end(self, amount_read, now):
147         self.ftotal_time = format_time(now - self.start_time)
148         self.ftotal_size = format_number(self.last_amount_read)
149         self.master.end_meter(self, now)
150
151     def failure(self, message, now=None):
152         self.master.failure_meter(self, message, now)
153
154     def message(self, message):
155         self.master.message_meter(self, message)
156
157 class MultiFileMeter:
158     helperclass = MultiFileHelper
159     def __init__(self):
160         self.meters = []
161         self.in_progress_meters = []
162         self._lock = thread.allocate_lock()
163         self.update_period = 0.3 # seconds
164         
165         self.numfiles         = None
166         self.finished_files   = 0
167         self.failed_files     = 0
168         self.open_files       = 0
169         self.total_size       = None
170         self.failed_size      = 0
171         self.start_time       = None
172         self.finished_file_size = 0
173         self.last_update_time = None
174         self.re = RateEstimator()
175
176     def start(self, numfiles=None, total_size=None, now=None):
177         if now is None: now = time.time()
178         self.numfiles         = numfiles
179         self.finished_files   = 0
180         self.failed_files     = 0
181         self.open_files       = 0
182         self.total_size       = total_size
183         self.failed_size      = 0
184         self.start_time       = now
185         self.finished_file_size = 0
186         self.last_update_time = now
187         self.re.start(total_size, now)
188         self._do_start(now)
189
190     def _do_start(self, now):
191         pass
192
193     def end(self, now=None):
194         if now is None: now = time.time()
195         self._do_end(now)
196         
197     def _do_end(self, now):
198         pass
199
200     def lock(self): self._lock.acquire()
201     def unlock(self): self._lock.release()
202
203     ###########################################################
204     # child meter creation and destruction
205     def newMeter(self):
206         newmeter = self.helperclass(self)
207         self.meters.append(newmeter)
208         return newmeter
209     
210     def removeMeter(self, meter):
211         self.meters.remove(meter)
212         
213     ###########################################################
214     # child functions - these should only be called by helpers
215     def start_meter(self, meter, now):
216         if not meter in self.meters:
217             raise ValueError('attempt to use orphaned meter')
218         self._lock.acquire()
219         try:
220             if not meter in self.in_progress_meters:
221                 self.in_progress_meters.append(meter)
222                 self.open_files += 1
223         finally:
224             self._lock.release()
225         self._do_start_meter(meter, now)
226         
227     def _do_start_meter(self, meter, now):
228         pass
229         
230     def update_meter(self, meter, now):
231         if not meter in self.meters:
232             raise ValueError('attempt to use orphaned meter')
233         if (now >= self.last_update_time + self.update_period) or \
234                not self.last_update_time:
235             self.re.update(self._amount_read(), now)
236             self.last_update_time = now
237             self._do_update_meter(meter, now)
238
239     def _do_update_meter(self, meter, now):
240         pass
241
242     def end_meter(self, meter, now):
243         if not meter in self.meters:
244             raise ValueError('attempt to use orphaned meter')
245         self._lock.acquire()
246         try:
247             try: self.in_progress_meters.remove(meter)
248             except ValueError: pass
249             self.open_files     -= 1
250             self.finished_files += 1
251             self.finished_file_size += meter.last_amount_read
252         finally:
253             self._lock.release()
254         self._do_end_meter(meter, now)
255
256     def _do_end_meter(self, meter, now):
257         pass
258
259     def failure_meter(self, meter, message, now):
260         if not meter in self.meters:
261             raise ValueError('attempt to use orphaned meter')
262         self._lock.acquire()
263         try:
264             try: self.in_progress_meters.remove(meter)
265             except ValueError: pass
266             self.open_files     -= 1
267             self.failed_files   += 1
268             if meter.size and self.failed_size is not None:
269                 self.failed_size += meter.size
270             else:
271                 self.failed_size = None
272         finally:
273             self._lock.release()
274         self._do_failure_meter(meter, message, now)
275
276     def _do_failure_meter(self, meter, message, now):
277         pass
278
279     def message_meter(self, meter, message):
280         pass
281
282     ########################################################
283     # internal functions
284     def _amount_read(self):
285         tot = self.finished_file_size
286         for m in self.in_progress_meters:
287             tot += m.last_amount_read
288         return tot
289
290
291 class TextMultiFileMeter(MultiFileMeter):
292     def __init__(self, fo=sys.stderr):
293         self.fo = fo
294         MultiFileMeter.__init__(self)
295
296     # files: ###/### ###%  data: ######/###### ###%  time: ##:##:##/##:##:##
297     def _do_update_meter(self, meter, now):
298         self._lock.acquire()
299         try:
300             format = "files: %3i/%-3i %3i%%   data: %6.6s/%-6.6s %3i%%   " \
301                      "time: %8.8s/%8.8s"
302             df = self.finished_files
303             tf = self.numfiles or 1
304             pf = 100 * float(df)/tf + 0.49
305             dd = self.re.last_amount_read
306             td = self.total_size
307             pd = 100 * (self.re.fraction_read() or 0) + 0.49
308             dt = self.re.elapsed_time()
309             rt = self.re.remaining_time()
310             if rt is None: tt = None
311             else: tt = dt + rt
312
313             fdd = format_number(dd) + 'B'
314             ftd = format_number(td) + 'B'
315             fdt = format_time(dt, 1)
316             ftt = format_time(tt, 1)
317             
318             out = '%-79.79s' % (format % (df, tf, pf, fdd, ftd, pd, fdt, ftt))
319             self.fo.write('\r' + out)
320             self.fo.flush()
321         finally:
322             self._lock.release()
323
324     def _do_end_meter(self, meter, now):
325         self._lock.acquire()
326         try:
327             format = "%-30.30s %6.6s    %8.8s    %9.9s"
328             fn = meter.basename
329             size = meter.last_amount_read
330             fsize = format_number(size) + 'B'
331             et = meter.re.elapsed_time()
332             fet = format_time(et, 1)
333             frate = format_number(size / et) + 'B/s'
334             
335             out = '%-79.79s' % (format % (fn, fsize, fet, frate))
336             self.fo.write('\r' + out + '\n')
337         finally:
338             self._lock.release()
339         self._do_update_meter(meter, now)
340
341     def _do_failure_meter(self, meter, message, now):
342         self._lock.acquire()
343         try:
344             format = "%-30.30s %6.6s %s"
345             fn = meter.basename
346             if type(message) in (type(''), type(u'')):
347                 message = message.splitlines()
348             if not message: message = ['']
349             out = '%-79s' % (format % (fn, 'FAILED', message[0] or ''))
350             self.fo.write('\r' + out + '\n')
351             for m in message[1:]: self.fo.write('  ' + m + '\n')
352             self._lock.release()
353         finally:
354             self._do_update_meter(meter, now)
355
356     def message_meter(self, meter, message):
357         self._lock.acquire()
358         try:
359             pass
360         finally:
361             self._lock.release()
362
363     def _do_end(self, now):
364         self._do_update_meter(None, now)
365         self._lock.acquire()
366         try:
367             self.fo.write('\n')
368             self.fo.flush()
369         finally:
370             self._lock.release()
371         
372 ######################################################################
373 # support classes and functions
374
375 class RateEstimator:
376     def __init__(self, timescale=5.0):
377         self.timescale = timescale
378
379     def start(self, total=None, now=None):
380         if now is None: now = time.time()
381         self.total = total
382         self.start_time = now
383         self.last_update_time = now
384         self.last_amount_read = 0
385         self.ave_rate = None
386         
387     def update(self, amount_read, now=None):
388         if now is None: now = time.time()
389         if amount_read == 0:
390             # if we just started this file, all bets are off
391             self.last_update_time = now
392             self.last_amount_read = 0
393             self.ave_rate = None
394             return
395
396         #print 'times', now, self.last_update_time
397         time_diff = now         - self.last_update_time
398         read_diff = amount_read - self.last_amount_read
399         self.last_update_time = now
400         self.last_amount_read = amount_read
401         self.ave_rate = self._temporal_rolling_ave(\
402             time_diff, read_diff, self.ave_rate, self.timescale)
403         #print 'results', time_diff, read_diff, self.ave_rate
404         
405     #####################################################################
406     # result methods
407     def average_rate(self):
408         "get the average transfer rate (in bytes/second)"
409         return self.ave_rate
410
411     def elapsed_time(self):
412         "the time between the start of the transfer and the most recent update"
413         return self.last_update_time - self.start_time
414
415     def remaining_time(self):
416         "estimated time remaining"
417         if not self.ave_rate or not self.total: return None
418         return (self.total - self.last_amount_read) / self.ave_rate
419
420     def fraction_read(self):
421         """the fraction of the data that has been read
422         (can be None for unknown transfer size)"""
423         if self.total is None: return None
424         elif self.total == 0: return 1.0
425         else: return float(self.last_amount_read)/self.total
426
427     #########################################################################
428     # support methods
429     def _temporal_rolling_ave(self, time_diff, read_diff, last_ave, timescale):
430         """a temporal rolling average performs smooth averaging even when
431         updates come at irregular intervals.  This is performed by scaling
432         the "epsilon" according to the time since the last update.
433         Specifically, epsilon = time_diff / timescale
434
435         As a general rule, the average will take on a completely new value
436         after 'timescale' seconds."""
437         epsilon = time_diff / timescale
438         if epsilon > 1: epsilon = 1.0
439         return self._rolling_ave(time_diff, read_diff, last_ave, epsilon)
440     
441     def _rolling_ave(self, time_diff, read_diff, last_ave, epsilon):
442         """perform a "rolling average" iteration
443         a rolling average "folds" new data into an existing average with
444         some weight, epsilon.  epsilon must be between 0.0 and 1.0 (inclusive)
445         a value of 0.0 means only the old value (initial value) counts,
446         and a value of 1.0 means only the newest value is considered."""
447         
448         try:
449             recent_rate = read_diff / time_diff
450         except ZeroDivisionError:
451             recent_rate = None
452         if last_ave is None: return recent_rate
453         elif recent_rate is None: return last_ave
454
455         # at this point, both last_ave and recent_rate are numbers
456         return epsilon * recent_rate  +  (1 - epsilon) * last_ave
457
458     def _round_remaining_time(self, rt, start_time=15.0):
459         """round the remaining time, depending on its size
460         If rt is between n*start_time and (n+1)*start_time round downward
461         to the nearest multiple of n (for any counting number n).
462         If rt < start_time, round down to the nearest 1.
463         For example (for start_time = 15.0):
464          2.7  -> 2.0
465          25.2 -> 25.0
466          26.4 -> 26.0
467          35.3 -> 34.0
468          63.6 -> 60.0
469         """
470
471         if rt < 0: return 0.0
472         shift = int(math.log(rt/start_time)/math.log(2))
473         rt = int(rt)
474         if shift <= 0: return rt
475         return float(int(rt) >> shift << shift)
476         
477
478 def format_time(seconds, use_hours=0):
479     if seconds is None or seconds < 0:
480         if use_hours: return '--:--:--'
481         else:         return '--:--'
482     else:
483         seconds = int(seconds)
484         minutes = seconds / 60
485         seconds = seconds % 60
486         if use_hours:
487             hours = minutes / 60
488             minutes = minutes % 60
489             return '%02i:%02i:%02i' % (hours, minutes, seconds)
490         else:
491             return '%02i:%02i' % (minutes, seconds)
492             
493 def format_number(number, SI=0, space=' '):
494     """Turn numbers into human-readable metric-like numbers"""
495     symbols = ['',  # (none)
496                'k', # kilo
497                'M', # mega
498                'G', # giga
499                'T', # tera
500                'P', # peta
501                'E', # exa
502                'Z', # zetta
503                'Y'] # yotta
504     
505     if SI: step = 1000.0
506     else: step = 1024.0
507
508     thresh = 999
509     depth = 0
510     max_depth = len(symbols) - 1
511     
512     # we want numbers between 0 and thresh, but don't exceed the length
513     # of our list.  In that event, the formatting will be screwed up,
514     # but it'll still show the right number.
515     while number > thresh and depth < max_depth:
516         depth  = depth + 1
517         number = number / step
518
519     if type(number) == type(1) or type(number) == type(1L):
520         # it's an int or a long, which means it didn't get divided,
521         # which means it's already short enough
522         format = '%i%s%s'
523     elif number < 9.95:
524         # must use 9.95 for proper sizing.  For example, 9.99 will be
525         # rounded to 10.0 with the .1f format string (which is too long)
526         format = '%.1f%s%s'
527     else:
528         format = '%.0f%s%s'
529         
530     return(format % (float(number or 0), space, symbols[depth]))