""" BitBake progress handling code """ # Copyright (C) 2016 Intel Corporation # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import sys import re import time import inspect import bb.event import bb.build class ProgressHandler(object): """ Base class that can pretend to be a file object well enough to be used to build objects to intercept console output and determine the progress of some operation. """ def __init__(self, d, outfile=None): self._progress = 0 self._data = d self._lastevent = 0 if outfile: self._outfile = outfile else: self._outfile = sys.stdout def _fire_progress(self, taskprogress, rate=None): """Internal function to fire the progress event""" bb.event.fire(bb.build.TaskProgress(taskprogress, rate), self._data) def write(self, string): self._outfile.write(string) def flush(self): self._outfile.flush() def update(self, progress, rate=None): ts = time.time() if progress > 100: progress = 100 if progress != self._progress or self._lastevent + 1 < ts: self._fire_progress(progress, rate) self._lastevent = ts self._progress = progress class LineFilterProgressHandler(ProgressHandler): """ A ProgressHandler variant that provides the ability to filter out the lines if they contain progress information. Additionally, it filters out anything before the last line feed on a line. This can be used to keep the logs clean of output that we've only enabled for getting progress, assuming that that can be done on a per-line basis. """ def __init__(self, d, outfile=None): self._linebuffer = '' super(LineFilterProgressHandler, self).__init__(d, outfile) def write(self, string): self._linebuffer += string while True: breakpos = self._linebuffer.find('\n') + 1 if breakpos == 0: break line = self._linebuffer[:breakpos] self._linebuffer = self._linebuffer[breakpos:] # Drop any line feeds and anything that precedes them lbreakpos = line.rfind('\r') + 1 if lbreakpos: line = line[lbreakpos:] if self.writeline(line): super(LineFilterProgressHandler, self).write(line) def writeline(self, line): return True class BasicProgressHandler(ProgressHandler): def __init__(self, d, regex=r'(\d+)%', outfile=None): super(BasicProgressHandler, self).__init__(d, outfile) self._regex = re.compile(regex) # Send an initial progress event so the bar gets shown self._fire_progress(0) def write(self, string): percs = self._regex.findall(string) if percs: progress = int(percs[-1]) self.update(progress) super(BasicProgressHandler, self).write(string) class OutOfProgressHandler(ProgressHandler): def __init__(self, d, regex, outfile=None): super(OutOfProgressHandler, self).__init__(d, outfile) self._regex = re.compile(regex) # Send an initial progress event so the bar gets shown self._fire_progress(0) def write(self, string): nums = self._regex.findall(string) if nums: progress = (float(nums[-1][0]) / float(nums[-1][1])) * 100 self.update(progress) super(OutOfProgressHandler, self).write(string) class MultiStageProgressReporter(object): """ Class which allows reporting progress without the caller having to know where they are in the overall sequence. Useful for tasks made up of python code spread across multiple classes / functions - the progress reporter object can be passed around or stored at the object level and calls to next_stage() and update() made whereever needed. """ def __init__(self, d, stage_weights, debug=False): """ Initialise the progress reporter. Parameters: * d: the datastore (needed for firing the events) * stage_weights: a list of weight values, one for each stage. The value is scaled internally so you only need to specify values relative to other values in the list, so if there are two stages and the first takes 2s and the second takes 10s you would specify [2, 10] (or [1, 5], it doesn't matter). * debug: specify True (and ensure you call finish() at the end) in order to show a printout of the calculated stage weights based on timing each stage. Use this to determine what the weights should be when you're not sure. """ self._data = d total = sum(stage_weights) self._stage_weights = [float(x)/total for x in stage_weights] self._stage = -1 self._base_progress = 0 # Send an initial progress event so the bar gets shown self._fire_progress(0) self._debug = debug self._finished = False if self._debug: self._last_time = time.time() self._stage_times = [] self._stage_total = None self._callers = [] def _fire_progress(self, taskprogress): bb.event.fire(bb.build.TaskProgress(taskprogress), self._data) def next_stage(self, stage_total=None): """ Move to the next stage. Parameters: * stage_total: optional total for progress within the stage, see update() for details NOTE: you need to call this before the first stage. """ self._stage += 1 self._stage_total = stage_total if self._stage == 0: # First stage if self._debug: self._last_time = time.time() else: if self._stage < len(self._stage_weights): self._base_progress = sum(self._stage_weights[:self._stage]) * 100 if self._debug: currtime = time.time() self._stage_times.append(currtime - self._last_time) self._last_time = currtime self._callers.append(inspect.getouterframes(inspect.currentframe())[1]) elif not self._debug: bb.warn('ProgressReporter: current stage beyond declared number of stages') self._base_progress = 100 self._fire_progress(self._base_progress) def update(self, stage_progress): """ Update progress within the current stage. Parameters: * stage_progress: progress value within the stage. If stage_total was specified when next_stage() was last called, then this value is considered to be out of stage_total, otherwise it should be a percentage value from 0 to 100. """ if self._stage_total: stage_progress = (float(stage_progress) / self._stage_total) * 100 if self._stage < 0: bb.warn('ProgressReporter: update called before first call to next_stage()') elif self._stage < len(self._stage_weights): progress = self._base_progress + (stage_progress * self._stage_weights[self._stage]) else: progress = self._base_progress if progress > 100: progress = 100 self._fire_progress(progress) def finish(self): if self._finished: return self._finished = True if self._debug: import math self._stage_times.append(time.time() - self._last_time) mintime = max(min(self._stage_times), 0.01) self._callers.append(None) stage_weights = [int(math.ceil(x / mintime)) for x in self._stage_times] bb.warn('Stage weights: %s' % stage_weights) out = [] for stage_weight, caller in zip(stage_weights, self._callers): if caller: out.append('Up to %s:%d: %d' % (caller[1], caller[2], stage_weight)) else: out.append('Up to finish: %d' % stage_weight) bb.warn('Stage times:\n %s' % '\n '.join(out)) class MultiStageProcessProgressReporter(MultiStageProgressReporter): """ Version of MultiStageProgressReporter intended for use with standalone processes (such as preparing the runqueue) """ def __init__(self, d, processname, stage_weights, debug=False): self._processname = processname self._started = False MultiStageProgressReporter.__init__(self, d, stage_weights, debug) def start(self): if not self._started: bb.event.fire(bb.event.ProcessStarted(self._processname, 100), self._data) self._started = True def _fire_progress(self, taskprogress): if taskprogress == 0: self.start() return bb.event.fire(bb.event.ProcessProgress(self._processname, taskprogress), self._data) def finish(self): MultiStageProgressReporter.finish(self) bb.event.fire(bb.event.ProcessFinished(self._processname), self._data) class DummyMultiStageProcessProgressReporter(MultiStageProgressReporter): """ MultiStageProcessProgressReporter that takes the calls and does nothing with them (to avoid a bunch of "if progress_reporter:" checks) """ def __init__(self): MultiStageProcessProgressReporter.__init__(self, "", None, []) def _fire_progress(self, taskprogress, rate=None): pass def start(self): pass def next_stage(self, stage_total=None): pass def update(self, stage_progress): pass def finish(self): pass