progress.py 9.9 KB

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