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