1*4882a593Smuzhiyun""" 2*4882a593SmuzhiyunBitBake progress handling code 3*4882a593Smuzhiyun""" 4*4882a593Smuzhiyun 5*4882a593Smuzhiyun# Copyright (C) 2016 Intel Corporation 6*4882a593Smuzhiyun# 7*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 8*4882a593Smuzhiyun# 9*4882a593Smuzhiyun 10*4882a593Smuzhiyunimport re 11*4882a593Smuzhiyunimport time 12*4882a593Smuzhiyunimport inspect 13*4882a593Smuzhiyunimport bb.event 14*4882a593Smuzhiyunimport bb.build 15*4882a593Smuzhiyunfrom bb.build import StdoutNoopContextManager 16*4882a593Smuzhiyun 17*4882a593Smuzhiyun 18*4882a593Smuzhiyun# from https://stackoverflow.com/a/14693789/221061 19*4882a593SmuzhiyunANSI_ESCAPE_REGEX = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]') 20*4882a593Smuzhiyun 21*4882a593Smuzhiyun 22*4882a593Smuzhiyundef filter_color(string): 23*4882a593Smuzhiyun """ 24*4882a593Smuzhiyun Filter ANSI escape codes out of |string|, return new string 25*4882a593Smuzhiyun """ 26*4882a593Smuzhiyun return ANSI_ESCAPE_REGEX.sub('', string) 27*4882a593Smuzhiyun 28*4882a593Smuzhiyun 29*4882a593Smuzhiyundef filter_color_n(string): 30*4882a593Smuzhiyun """ 31*4882a593Smuzhiyun Filter ANSI escape codes out of |string|, returns tuple of 32*4882a593Smuzhiyun (new string, # of ANSI codes removed) 33*4882a593Smuzhiyun """ 34*4882a593Smuzhiyun return ANSI_ESCAPE_REGEX.subn('', string) 35*4882a593Smuzhiyun 36*4882a593Smuzhiyun 37*4882a593Smuzhiyunclass ProgressHandler: 38*4882a593Smuzhiyun """ 39*4882a593Smuzhiyun Base class that can pretend to be a file object well enough to be 40*4882a593Smuzhiyun used to build objects to intercept console output and determine the 41*4882a593Smuzhiyun progress of some operation. 42*4882a593Smuzhiyun """ 43*4882a593Smuzhiyun def __init__(self, d, outfile=None): 44*4882a593Smuzhiyun self._progress = 0 45*4882a593Smuzhiyun self._data = d 46*4882a593Smuzhiyun self._lastevent = 0 47*4882a593Smuzhiyun if outfile: 48*4882a593Smuzhiyun self._outfile = outfile 49*4882a593Smuzhiyun else: 50*4882a593Smuzhiyun self._outfile = StdoutNoopContextManager() 51*4882a593Smuzhiyun 52*4882a593Smuzhiyun def __enter__(self): 53*4882a593Smuzhiyun self._outfile.__enter__() 54*4882a593Smuzhiyun return self 55*4882a593Smuzhiyun 56*4882a593Smuzhiyun def __exit__(self, *excinfo): 57*4882a593Smuzhiyun self._outfile.__exit__(*excinfo) 58*4882a593Smuzhiyun 59*4882a593Smuzhiyun def _fire_progress(self, taskprogress, rate=None): 60*4882a593Smuzhiyun """Internal function to fire the progress event""" 61*4882a593Smuzhiyun bb.event.fire(bb.build.TaskProgress(taskprogress, rate), self._data) 62*4882a593Smuzhiyun 63*4882a593Smuzhiyun def write(self, string): 64*4882a593Smuzhiyun self._outfile.write(string) 65*4882a593Smuzhiyun 66*4882a593Smuzhiyun def flush(self): 67*4882a593Smuzhiyun self._outfile.flush() 68*4882a593Smuzhiyun 69*4882a593Smuzhiyun def update(self, progress, rate=None): 70*4882a593Smuzhiyun ts = time.time() 71*4882a593Smuzhiyun if progress > 100: 72*4882a593Smuzhiyun progress = 100 73*4882a593Smuzhiyun if progress != self._progress or self._lastevent + 1 < ts: 74*4882a593Smuzhiyun self._fire_progress(progress, rate) 75*4882a593Smuzhiyun self._lastevent = ts 76*4882a593Smuzhiyun self._progress = progress 77*4882a593Smuzhiyun 78*4882a593Smuzhiyun 79*4882a593Smuzhiyunclass LineFilterProgressHandler(ProgressHandler): 80*4882a593Smuzhiyun """ 81*4882a593Smuzhiyun A ProgressHandler variant that provides the ability to filter out 82*4882a593Smuzhiyun the lines if they contain progress information. Additionally, it 83*4882a593Smuzhiyun filters out anything before the last line feed on a line. This can 84*4882a593Smuzhiyun be used to keep the logs clean of output that we've only enabled for 85*4882a593Smuzhiyun getting progress, assuming that that can be done on a per-line 86*4882a593Smuzhiyun basis. 87*4882a593Smuzhiyun """ 88*4882a593Smuzhiyun def __init__(self, d, outfile=None): 89*4882a593Smuzhiyun self._linebuffer = '' 90*4882a593Smuzhiyun super().__init__(d, outfile) 91*4882a593Smuzhiyun 92*4882a593Smuzhiyun def write(self, string): 93*4882a593Smuzhiyun self._linebuffer += string 94*4882a593Smuzhiyun while True: 95*4882a593Smuzhiyun breakpos = self._linebuffer.find('\n') + 1 96*4882a593Smuzhiyun if breakpos == 0: 97*4882a593Smuzhiyun # for the case when the line with progress ends with only '\r' 98*4882a593Smuzhiyun breakpos = self._linebuffer.find('\r') + 1 99*4882a593Smuzhiyun if breakpos == 0: 100*4882a593Smuzhiyun break 101*4882a593Smuzhiyun line = self._linebuffer[:breakpos] 102*4882a593Smuzhiyun self._linebuffer = self._linebuffer[breakpos:] 103*4882a593Smuzhiyun # Drop any line feeds and anything that precedes them 104*4882a593Smuzhiyun lbreakpos = line.rfind('\r') + 1 105*4882a593Smuzhiyun if lbreakpos and lbreakpos != breakpos: 106*4882a593Smuzhiyun line = line[lbreakpos:] 107*4882a593Smuzhiyun if self.writeline(filter_color(line)): 108*4882a593Smuzhiyun super().write(line) 109*4882a593Smuzhiyun 110*4882a593Smuzhiyun def writeline(self, line): 111*4882a593Smuzhiyun return True 112*4882a593Smuzhiyun 113*4882a593Smuzhiyun 114*4882a593Smuzhiyunclass BasicProgressHandler(ProgressHandler): 115*4882a593Smuzhiyun def __init__(self, d, regex=r'(\d+)%', outfile=None): 116*4882a593Smuzhiyun super().__init__(d, outfile) 117*4882a593Smuzhiyun self._regex = re.compile(regex) 118*4882a593Smuzhiyun # Send an initial progress event so the bar gets shown 119*4882a593Smuzhiyun self._fire_progress(0) 120*4882a593Smuzhiyun 121*4882a593Smuzhiyun def write(self, string): 122*4882a593Smuzhiyun percs = self._regex.findall(filter_color(string)) 123*4882a593Smuzhiyun if percs: 124*4882a593Smuzhiyun progress = int(percs[-1]) 125*4882a593Smuzhiyun self.update(progress) 126*4882a593Smuzhiyun super().write(string) 127*4882a593Smuzhiyun 128*4882a593Smuzhiyun 129*4882a593Smuzhiyunclass OutOfProgressHandler(ProgressHandler): 130*4882a593Smuzhiyun def __init__(self, d, regex, outfile=None): 131*4882a593Smuzhiyun super().__init__(d, outfile) 132*4882a593Smuzhiyun self._regex = re.compile(regex) 133*4882a593Smuzhiyun # Send an initial progress event so the bar gets shown 134*4882a593Smuzhiyun self._fire_progress(0) 135*4882a593Smuzhiyun 136*4882a593Smuzhiyun def write(self, string): 137*4882a593Smuzhiyun nums = self._regex.findall(filter_color(string)) 138*4882a593Smuzhiyun if nums: 139*4882a593Smuzhiyun progress = (float(nums[-1][0]) / float(nums[-1][1])) * 100 140*4882a593Smuzhiyun self.update(progress) 141*4882a593Smuzhiyun super().write(string) 142*4882a593Smuzhiyun 143*4882a593Smuzhiyun 144*4882a593Smuzhiyunclass MultiStageProgressReporter: 145*4882a593Smuzhiyun """ 146*4882a593Smuzhiyun Class which allows reporting progress without the caller 147*4882a593Smuzhiyun having to know where they are in the overall sequence. Useful 148*4882a593Smuzhiyun for tasks made up of python code spread across multiple 149*4882a593Smuzhiyun classes / functions - the progress reporter object can 150*4882a593Smuzhiyun be passed around or stored at the object level and calls 151*4882a593Smuzhiyun to next_stage() and update() made wherever needed. 152*4882a593Smuzhiyun """ 153*4882a593Smuzhiyun def __init__(self, d, stage_weights, debug=False): 154*4882a593Smuzhiyun """ 155*4882a593Smuzhiyun Initialise the progress reporter. 156*4882a593Smuzhiyun 157*4882a593Smuzhiyun Parameters: 158*4882a593Smuzhiyun * d: the datastore (needed for firing the events) 159*4882a593Smuzhiyun * stage_weights: a list of weight values, one for each stage. 160*4882a593Smuzhiyun The value is scaled internally so you only need to specify 161*4882a593Smuzhiyun values relative to other values in the list, so if there 162*4882a593Smuzhiyun are two stages and the first takes 2s and the second takes 163*4882a593Smuzhiyun 10s you would specify [2, 10] (or [1, 5], it doesn't matter). 164*4882a593Smuzhiyun * debug: specify True (and ensure you call finish() at the end) 165*4882a593Smuzhiyun in order to show a printout of the calculated stage weights 166*4882a593Smuzhiyun based on timing each stage. Use this to determine what the 167*4882a593Smuzhiyun weights should be when you're not sure. 168*4882a593Smuzhiyun """ 169*4882a593Smuzhiyun self._data = d 170*4882a593Smuzhiyun total = sum(stage_weights) 171*4882a593Smuzhiyun self._stage_weights = [float(x)/total for x in stage_weights] 172*4882a593Smuzhiyun self._stage = -1 173*4882a593Smuzhiyun self._base_progress = 0 174*4882a593Smuzhiyun # Send an initial progress event so the bar gets shown 175*4882a593Smuzhiyun self._fire_progress(0) 176*4882a593Smuzhiyun self._debug = debug 177*4882a593Smuzhiyun self._finished = False 178*4882a593Smuzhiyun if self._debug: 179*4882a593Smuzhiyun self._last_time = time.time() 180*4882a593Smuzhiyun self._stage_times = [] 181*4882a593Smuzhiyun self._stage_total = None 182*4882a593Smuzhiyun self._callers = [] 183*4882a593Smuzhiyun 184*4882a593Smuzhiyun def __enter__(self): 185*4882a593Smuzhiyun return self 186*4882a593Smuzhiyun 187*4882a593Smuzhiyun def __exit__(self, *excinfo): 188*4882a593Smuzhiyun pass 189*4882a593Smuzhiyun 190*4882a593Smuzhiyun def _fire_progress(self, taskprogress): 191*4882a593Smuzhiyun bb.event.fire(bb.build.TaskProgress(taskprogress), self._data) 192*4882a593Smuzhiyun 193*4882a593Smuzhiyun def next_stage(self, stage_total=None): 194*4882a593Smuzhiyun """ 195*4882a593Smuzhiyun Move to the next stage. 196*4882a593Smuzhiyun Parameters: 197*4882a593Smuzhiyun * stage_total: optional total for progress within the stage, 198*4882a593Smuzhiyun see update() for details 199*4882a593Smuzhiyun NOTE: you need to call this before the first stage. 200*4882a593Smuzhiyun """ 201*4882a593Smuzhiyun self._stage += 1 202*4882a593Smuzhiyun self._stage_total = stage_total 203*4882a593Smuzhiyun if self._stage == 0: 204*4882a593Smuzhiyun # First stage 205*4882a593Smuzhiyun if self._debug: 206*4882a593Smuzhiyun self._last_time = time.time() 207*4882a593Smuzhiyun else: 208*4882a593Smuzhiyun if self._stage < len(self._stage_weights): 209*4882a593Smuzhiyun self._base_progress = sum(self._stage_weights[:self._stage]) * 100 210*4882a593Smuzhiyun if self._debug: 211*4882a593Smuzhiyun currtime = time.time() 212*4882a593Smuzhiyun self._stage_times.append(currtime - self._last_time) 213*4882a593Smuzhiyun self._last_time = currtime 214*4882a593Smuzhiyun self._callers.append(inspect.getouterframes(inspect.currentframe())[1]) 215*4882a593Smuzhiyun elif not self._debug: 216*4882a593Smuzhiyun bb.warn('ProgressReporter: current stage beyond declared number of stages') 217*4882a593Smuzhiyun self._base_progress = 100 218*4882a593Smuzhiyun self._fire_progress(self._base_progress) 219*4882a593Smuzhiyun 220*4882a593Smuzhiyun def update(self, stage_progress): 221*4882a593Smuzhiyun """ 222*4882a593Smuzhiyun Update progress within the current stage. 223*4882a593Smuzhiyun Parameters: 224*4882a593Smuzhiyun * stage_progress: progress value within the stage. If stage_total 225*4882a593Smuzhiyun was specified when next_stage() was last called, then this 226*4882a593Smuzhiyun value is considered to be out of stage_total, otherwise it should 227*4882a593Smuzhiyun be a percentage value from 0 to 100. 228*4882a593Smuzhiyun """ 229*4882a593Smuzhiyun progress = None 230*4882a593Smuzhiyun if self._stage_total: 231*4882a593Smuzhiyun stage_progress = (float(stage_progress) / self._stage_total) * 100 232*4882a593Smuzhiyun if self._stage < 0: 233*4882a593Smuzhiyun bb.warn('ProgressReporter: update called before first call to next_stage()') 234*4882a593Smuzhiyun elif self._stage < len(self._stage_weights): 235*4882a593Smuzhiyun progress = self._base_progress + (stage_progress * self._stage_weights[self._stage]) 236*4882a593Smuzhiyun else: 237*4882a593Smuzhiyun progress = self._base_progress 238*4882a593Smuzhiyun if progress: 239*4882a593Smuzhiyun if progress > 100: 240*4882a593Smuzhiyun progress = 100 241*4882a593Smuzhiyun self._fire_progress(progress) 242*4882a593Smuzhiyun 243*4882a593Smuzhiyun def finish(self): 244*4882a593Smuzhiyun if self._finished: 245*4882a593Smuzhiyun return 246*4882a593Smuzhiyun self._finished = True 247*4882a593Smuzhiyun if self._debug: 248*4882a593Smuzhiyun import math 249*4882a593Smuzhiyun self._stage_times.append(time.time() - self._last_time) 250*4882a593Smuzhiyun mintime = max(min(self._stage_times), 0.01) 251*4882a593Smuzhiyun self._callers.append(None) 252*4882a593Smuzhiyun stage_weights = [int(math.ceil(x / mintime)) for x in self._stage_times] 253*4882a593Smuzhiyun bb.warn('Stage weights: %s' % stage_weights) 254*4882a593Smuzhiyun out = [] 255*4882a593Smuzhiyun for stage_weight, caller in zip(stage_weights, self._callers): 256*4882a593Smuzhiyun if caller: 257*4882a593Smuzhiyun out.append('Up to %s:%d: %d' % (caller[1], caller[2], stage_weight)) 258*4882a593Smuzhiyun else: 259*4882a593Smuzhiyun out.append('Up to finish: %d' % stage_weight) 260*4882a593Smuzhiyun bb.warn('Stage times:\n %s' % '\n '.join(out)) 261*4882a593Smuzhiyun 262*4882a593Smuzhiyun 263*4882a593Smuzhiyunclass MultiStageProcessProgressReporter(MultiStageProgressReporter): 264*4882a593Smuzhiyun """ 265*4882a593Smuzhiyun Version of MultiStageProgressReporter intended for use with 266*4882a593Smuzhiyun standalone processes (such as preparing the runqueue) 267*4882a593Smuzhiyun """ 268*4882a593Smuzhiyun def __init__(self, d, processname, stage_weights, debug=False): 269*4882a593Smuzhiyun self._processname = processname 270*4882a593Smuzhiyun self._started = False 271*4882a593Smuzhiyun super().__init__(d, stage_weights, debug) 272*4882a593Smuzhiyun 273*4882a593Smuzhiyun def start(self): 274*4882a593Smuzhiyun if not self._started: 275*4882a593Smuzhiyun bb.event.fire(bb.event.ProcessStarted(self._processname, 100), self._data) 276*4882a593Smuzhiyun self._started = True 277*4882a593Smuzhiyun 278*4882a593Smuzhiyun def _fire_progress(self, taskprogress): 279*4882a593Smuzhiyun if taskprogress == 0: 280*4882a593Smuzhiyun self.start() 281*4882a593Smuzhiyun return 282*4882a593Smuzhiyun bb.event.fire(bb.event.ProcessProgress(self._processname, taskprogress), self._data) 283*4882a593Smuzhiyun 284*4882a593Smuzhiyun def finish(self): 285*4882a593Smuzhiyun MultiStageProgressReporter.finish(self) 286*4882a593Smuzhiyun bb.event.fire(bb.event.ProcessFinished(self._processname), self._data) 287*4882a593Smuzhiyun 288*4882a593Smuzhiyun 289*4882a593Smuzhiyunclass DummyMultiStageProcessProgressReporter(MultiStageProgressReporter): 290*4882a593Smuzhiyun """ 291*4882a593Smuzhiyun MultiStageProcessProgressReporter that takes the calls and does nothing 292*4882a593Smuzhiyun with them (to avoid a bunch of "if progress_reporter:" checks) 293*4882a593Smuzhiyun """ 294*4882a593Smuzhiyun def __init__(self): 295*4882a593Smuzhiyun super().__init__(None, []) 296*4882a593Smuzhiyun 297*4882a593Smuzhiyun def _fire_progress(self, taskprogress, rate=None): 298*4882a593Smuzhiyun pass 299*4882a593Smuzhiyun 300*4882a593Smuzhiyun def start(self): 301*4882a593Smuzhiyun pass 302*4882a593Smuzhiyun 303*4882a593Smuzhiyun def next_stage(self, stage_total=None): 304*4882a593Smuzhiyun pass 305*4882a593Smuzhiyun 306*4882a593Smuzhiyun def update(self, stage_progress): 307*4882a593Smuzhiyun pass 308*4882a593Smuzhiyun 309*4882a593Smuzhiyun def finish(self): 310*4882a593Smuzhiyun pass 311