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