graph-build-time 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. #!/usr/bin/env python
  2. # Copyright (C) 2011 by Thomas Petazzoni <thomas.petazzoni@free-electrons.com>
  3. # Copyright (C) 2013 by Yann E. MORIN <yann.morin.1998@free.fr>
  4. #
  5. # This program is free software; you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation; either version 2 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. # General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  18. # This script generates graphs of packages build time, from the timing
  19. # data generated by Buildroot in the $(O)/build-time.log file.
  20. #
  21. # Example usage:
  22. #
  23. # cat $(O)/build-time.log | ./support/scripts/graph-build-time --type=histogram --output=foobar.pdf
  24. #
  25. # Three graph types are available :
  26. #
  27. # * histogram, which creates an histogram of the build time for each
  28. # package, decomposed by each step (extract, patch, configure,
  29. # etc.). The order in which the packages are shown is
  30. # configurable: by package name, by build order, or by duration
  31. # order. See the --order option.
  32. #
  33. # * pie-packages, which creates a pie chart of the build time of
  34. # each package (without decomposition in steps). Packages that
  35. # contributed to less than 1% of the overall build time are all
  36. # grouped together in an "Other" entry.
  37. #
  38. # * pie-steps, which creates a pie chart of the time spent globally
  39. # on each step (extract, patch, configure, etc...)
  40. #
  41. # The default is to generate an histogram ordered by package name.
  42. #
  43. # Requirements:
  44. #
  45. # * matplotlib (python-matplotlib on Debian/Ubuntu systems)
  46. # * numpy (python-numpy on Debian/Ubuntu systems)
  47. # * argparse (by default in Python 2.7, requires python-argparse if
  48. # Python 2.6 is used)
  49. import sys
  50. try:
  51. import matplotlib as mpl
  52. import numpy
  53. except ImportError:
  54. sys.stderr.write("You need python-matplotlib and python-numpy to generate build graphs\n")
  55. exit(1)
  56. # Use the Agg backend (which produces a PNG output, see
  57. # http://matplotlib.org/faq/usage_faq.html#what-is-a-backend),
  58. # otherwise an incorrect backend is used on some host machines).
  59. # Note: matplotlib.use() must be called *before* matplotlib.pyplot.
  60. mpl.use('Agg')
  61. import matplotlib.pyplot as plt # noqa: E402
  62. import matplotlib.font_manager as fm # noqa: E402
  63. import csv # noqa: E402
  64. import argparse # noqa: E402
  65. steps = ['download', 'extract', 'patch', 'configure', 'build',
  66. 'install-target', 'install-staging', 'install-images',
  67. 'install-host']
  68. default_colors = ['#8d02ff', '#e60004', '#009836', '#2e1d86', '#ffed00',
  69. '#0068b5', '#f28e00', '#940084', '#97c000']
  70. alternate_colors = ['#ffbe0a', '#96bdff', '#3f7f7f', '#ff0000', '#00c000',
  71. '#0080ff', '#c000ff', '#00eeee', '#e0e000']
  72. class Package:
  73. def __init__(self, name):
  74. self.name = name
  75. self.steps_duration = {}
  76. self.steps_start = {}
  77. self.steps_end = {}
  78. def add_step(self, step, state, time):
  79. if state == "start":
  80. self.steps_start[step] = time
  81. else:
  82. self.steps_end[step] = time
  83. if step in self.steps_start and step in self.steps_end:
  84. self.steps_duration[step] = self.steps_end[step] - self.steps_start[step]
  85. def get_duration(self, step=None):
  86. if step is None:
  87. duration = 0
  88. for step in list(self.steps_duration.keys()):
  89. duration += self.steps_duration[step]
  90. return duration
  91. if step in self.steps_duration:
  92. return self.steps_duration[step]
  93. return 0
  94. # Generate an histogram of the time spent in each step of each
  95. # package.
  96. def pkg_histogram(data, output, order="build"):
  97. n_pkgs = len(data)
  98. ind = numpy.arange(n_pkgs)
  99. if order == "duration":
  100. data = sorted(data, key=lambda p: p.get_duration(), reverse=True)
  101. elif order == "name":
  102. data = sorted(data, key=lambda p: p.name, reverse=False)
  103. # Prepare the vals array, containing one entry for each step
  104. vals = []
  105. for step in steps:
  106. val = []
  107. for p in data:
  108. val.append(p.get_duration(step))
  109. vals.append(val)
  110. bottom = [0] * n_pkgs
  111. legenditems = []
  112. plt.figure()
  113. # Draw the bars, step by step
  114. for i in range(0, len(vals)):
  115. b = plt.bar(ind+0.1, vals[i], width=0.8, color=colors[i], bottom=bottom, linewidth=0.25)
  116. legenditems.append(b[0])
  117. bottom = [bottom[j] + vals[i][j] for j in range(0, len(vals[i]))]
  118. # Draw the package names
  119. plt.xticks(ind + .6, [p.name for p in data], rotation=-60, rotation_mode="anchor", fontsize=8, ha='left')
  120. # Adjust size of graph depending on the number of packages
  121. # Ensure a minimal size twice as the default
  122. # Magic Numbers do Magic Layout!
  123. ratio = max(((n_pkgs + 10) / 48, 2))
  124. borders = 0.1 / ratio
  125. sz = plt.gcf().get_figwidth()
  126. plt.gcf().set_figwidth(sz * ratio)
  127. # Adjust space at borders, add more space for the
  128. # package names at the bottom
  129. plt.gcf().subplots_adjust(bottom=0.2, left=borders, right=1-borders)
  130. # Remove ticks in the graph for each package
  131. axes = plt.gcf().gca()
  132. for line in axes.get_xticklines():
  133. line.set_markersize(0)
  134. axes.set_ylabel('Time (seconds)')
  135. # Reduce size of legend text
  136. leg_prop = fm.FontProperties(size=6)
  137. # Draw legend
  138. plt.legend(legenditems, steps, prop=leg_prop)
  139. if order == "name":
  140. plt.title('Build time of packages\n')
  141. elif order == "build":
  142. plt.title('Build time of packages, by build order\n')
  143. elif order == "duration":
  144. plt.title('Build time of packages, by duration order\n')
  145. # Save graph
  146. plt.savefig(output)
  147. # Generate a pie chart with the time spent building each package.
  148. def pkg_pie_time_per_package(data, output):
  149. # Compute total build duration
  150. total = 0
  151. for p in data:
  152. total += p.get_duration()
  153. # Build the list of labels and values, and filter the packages
  154. # that account for less than 1% of the build time.
  155. labels = []
  156. values = []
  157. other_value = 0
  158. for p in sorted(data, key=lambda p: p.get_duration()):
  159. if p.get_duration() < (total * 0.01):
  160. other_value += p.get_duration()
  161. else:
  162. labels.append(p.name)
  163. values.append(p.get_duration())
  164. labels.append('Other')
  165. values.append(other_value)
  166. plt.figure()
  167. # Draw pie graph
  168. patches, texts, autotexts = plt.pie(values, labels=labels,
  169. autopct='%1.1f%%', shadow=True,
  170. colors=colors)
  171. # Reduce text size
  172. proptease = fm.FontProperties()
  173. proptease.set_size('xx-small')
  174. plt.setp(autotexts, fontproperties=proptease)
  175. plt.setp(texts, fontproperties=proptease)
  176. plt.title('Build time per package')
  177. plt.savefig(output)
  178. # Generate a pie chart with a portion for the overall time spent in
  179. # each step for all packages.
  180. def pkg_pie_time_per_step(data, output):
  181. steps_values = []
  182. for step in steps:
  183. val = 0
  184. for p in data:
  185. val += p.get_duration(step)
  186. steps_values.append(val)
  187. plt.figure()
  188. # Draw pie graph
  189. patches, texts, autotexts = plt.pie(steps_values, labels=steps,
  190. autopct='%1.1f%%', shadow=True,
  191. colors=colors)
  192. # Reduce text size
  193. proptease = fm.FontProperties()
  194. proptease.set_size('xx-small')
  195. plt.setp(autotexts, fontproperties=proptease)
  196. plt.setp(texts, fontproperties=proptease)
  197. plt.title('Build time per step')
  198. plt.savefig(output)
  199. # Parses the csv file passed on standard input and returns a list of
  200. # Package objects, filed with the duration of each step and the total
  201. # duration of the package.
  202. def read_data(input_file):
  203. if input_file is None:
  204. input_file = sys.stdin
  205. else:
  206. input_file = open(input_file)
  207. reader = csv.reader(input_file, delimiter=':')
  208. pkgs = []
  209. # Auxilliary function to find a package by name in the list.
  210. def getpkg(name):
  211. for p in pkgs:
  212. if p.name == name:
  213. return p
  214. return None
  215. for row in reader:
  216. time = float(row[0].strip())
  217. state = row[1].strip()
  218. step = row[2].strip()
  219. pkg = row[3].strip()
  220. p = getpkg(pkg)
  221. if p is None:
  222. p = Package(pkg)
  223. pkgs.append(p)
  224. p.add_step(step, state, time)
  225. return pkgs
  226. parser = argparse.ArgumentParser(description='Draw build time graphs')
  227. parser.add_argument("--type", '-t', metavar="GRAPH_TYPE",
  228. help="Type of graph (histogram, pie-packages, pie-steps)")
  229. parser.add_argument("--order", '-O', metavar="GRAPH_ORDER",
  230. help="Ordering of packages: build or duration (for histogram only)")
  231. parser.add_argument("--alternate-colors", '-c', action="store_true",
  232. help="Use alternate colour-scheme")
  233. parser.add_argument("--input", '-i', metavar="INPUT",
  234. help="Input file (usually $(O)/build/build-time.log)")
  235. parser.add_argument("--output", '-o', metavar="OUTPUT", required=True,
  236. help="Output file (.pdf or .png extension)")
  237. args = parser.parse_args()
  238. d = read_data(args.input)
  239. if args.alternate_colors:
  240. colors = alternate_colors
  241. else:
  242. colors = default_colors
  243. if args.type == "histogram" or args.type is None:
  244. if args.order == "build" or args.order == "duration" or args.order == "name":
  245. pkg_histogram(d, args.output, args.order)
  246. elif args.order is None:
  247. pkg_histogram(d, args.output, "name")
  248. else:
  249. sys.stderr.write("Unknown ordering: %s\n" % args.order)
  250. exit(1)
  251. elif args.type == "pie-packages":
  252. pkg_pie_time_per_package(d, args.output)
  253. elif args.type == "pie-steps":
  254. pkg_pie_time_per_step(d, args.output)
  255. else:
  256. sys.stderr.write("Unknown type: %s\n" % args.type)
  257. exit(1)