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