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