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