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