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