1#!/usr/bin/env python3 2 3# Copyright (C) 2014 by Thomas Petazzoni <thomas.petazzoni@free-electrons.com> 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 19import sys 20import os 21import os.path 22import argparse 23import csv 24import collections 25import math 26 27try: 28 import matplotlib 29 matplotlib.use('Agg') 30 import matplotlib.font_manager as fm 31 import matplotlib.pyplot as plt 32except ImportError: 33 sys.stderr.write("You need python-matplotlib to generate the size graph\n") 34 exit(1) 35 36 37class Config: 38 biggest_first = False 39 iec = False 40 size_limit = 0.01 41 colors = ['#e60004', '#f28e00', '#ffed00', '#940084', 42 '#2e1d86', '#0068b5', '#009836', '#97c000'] 43 44 45# 46# This function adds a new file to 'filesdict', after checking its 47# size. The 'filesdict' contain the relative path of the file as the 48# key, and as the value a tuple containing the name of the package to 49# which the file belongs and the size of the file. 50# 51# filesdict: the dict to which the file is added 52# relpath: relative path of the file 53# fullpath: absolute path to the file 54# pkg: package to which the file belongs 55# 56def add_file(filesdict, relpath, abspath, pkg): 57 if not os.path.exists(abspath): 58 return 59 if os.path.islink(abspath): 60 return 61 sz = os.stat(abspath).st_size 62 filesdict[relpath] = (pkg, sz) 63 64 65# 66# This function returns a dict where each key is the path of a file in 67# the root filesystem, and the value is a tuple containing two 68# elements: the name of the package to which this file belongs and the 69# size of the file. 70# 71# builddir: path to the Buildroot output directory 72# 73def build_package_dict(builddir): 74 filesdict = {} 75 with open(os.path.join(builddir, "build", "packages-file-list.txt")) as f: 76 for line in f.readlines(): 77 pkg, fpath = line.split(",", 1) 78 # remove the initial './' in each file path 79 fpath = fpath.strip()[2:] 80 fullpath = os.path.join(builddir, "target", fpath) 81 add_file(filesdict, fpath, fullpath, pkg) 82 return filesdict 83 84 85# 86# This function builds a dictionary that contains the name of a 87# package as key, and the size of the files installed by this package 88# as the value. 89# 90# filesdict: dictionary with the name of the files as key, and as 91# value a tuple containing the name of the package to which the files 92# belongs, and the size of the file. As returned by 93# build_package_dict. 94# 95# builddir: path to the Buildroot output directory 96# 97def build_package_size(filesdict, builddir): 98 pkgsize = collections.defaultdict(int) 99 100 seeninodes = set() 101 for root, _, files in os.walk(os.path.join(builddir, "target")): 102 for f in files: 103 fpath = os.path.join(root, f) 104 if os.path.islink(fpath): 105 continue 106 107 st = os.stat(fpath) 108 if st.st_ino in seeninodes: 109 # hard link 110 continue 111 else: 112 seeninodes.add(st.st_ino) 113 114 frelpath = os.path.relpath(fpath, os.path.join(builddir, "target")) 115 if frelpath not in filesdict: 116 print("WARNING: %s is not part of any package" % frelpath) 117 pkg = "unknown" 118 else: 119 pkg = filesdict[frelpath][0] 120 121 pkgsize[pkg] += st.st_size 122 123 return pkgsize 124 125 126# 127# Given a dict returned by build_package_size(), this function 128# generates a pie chart of the size installed by each package. 129# 130# pkgsize: dictionary with the name of the package as a key, and the 131# size as the value, as returned by build_package_size. 132# 133# outputf: output file for the graph 134# 135def draw_graph(pkgsize, outputf): 136 def size2string(sz): 137 if Config.iec: 138 divider = 1024.0 139 prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti'] 140 else: 141 divider = 1000.0 142 prefixes = ['', 'k', 'M', 'G', 'T'] 143 while sz > divider and len(prefixes) > 1: 144 prefixes = prefixes[1:] 145 sz = sz/divider 146 # precision is made so that there are always at least three meaningful 147 # digits displayed (e.g. '3.14' and '10.4', not just '3' and '10') 148 precision = int(2-math.floor(math.log10(sz))) if sz < 1000 else 0 149 return '{:.{prec}f} {}B'.format(sz, prefixes[0], prec=precision) 150 151 total = sum(pkgsize.values()) 152 labels = [] 153 values = [] 154 other_value = 0 155 unknown_value = 0 156 for (p, sz) in sorted(pkgsize.items(), key=lambda x: x[1], 157 reverse=Config.biggest_first): 158 if sz < (total * Config.size_limit): 159 other_value += sz 160 elif p == "unknown": 161 unknown_value = sz 162 else: 163 labels.append("%s (%s)" % (p, size2string(sz))) 164 values.append(sz) 165 if unknown_value != 0: 166 labels.append("Unknown (%s)" % (size2string(unknown_value))) 167 values.append(unknown_value) 168 if other_value != 0: 169 labels.append("Other (%s)" % (size2string(other_value))) 170 values.append(other_value) 171 172 plt.figure() 173 patches, texts, autotexts = plt.pie(values, labels=labels, 174 autopct='%1.1f%%', shadow=True, 175 colors=Config.colors) 176 # Reduce text size 177 proptease = fm.FontProperties() 178 proptease.set_size('xx-small') 179 plt.setp(autotexts, fontproperties=proptease) 180 plt.setp(texts, fontproperties=proptease) 181 182 plt.suptitle("Filesystem size per package", fontsize=18, y=.97) 183 plt.title("Total filesystem size: %s" % (size2string(total)), fontsize=10, 184 y=.96) 185 plt.savefig(outputf) 186 187 188# 189# Generate a CSV file with statistics about the size of each file, its 190# size contribution to the package and to the overall system. 191# 192# filesdict: dictionary with the name of the files as key, and as 193# value a tuple containing the name of the package to which the files 194# belongs, and the size of the file. As returned by 195# build_package_dict. 196# 197# pkgsize: dictionary with the name of the package as a key, and the 198# size as the value, as returned by build_package_size. 199# 200# outputf: output CSV file 201# 202def gen_files_csv(filesdict, pkgsizes, outputf): 203 total = 0 204 for (p, sz) in pkgsizes.items(): 205 total += sz 206 with open(outputf, 'w') as csvfile: 207 wr = csv.writer(csvfile, delimiter=',', quoting=csv.QUOTE_MINIMAL) 208 wr.writerow(["File name", 209 "Package name", 210 "File size", 211 "Package size", 212 "File size in package (%)", 213 "File size in system (%)"]) 214 for f, (pkgname, filesize) in filesdict.items(): 215 pkgsize = pkgsizes[pkgname] 216 217 if pkgsize == 0: 218 percent_pkg = 0 219 else: 220 percent_pkg = float(filesize) / pkgsize * 100 221 222 percent_total = float(filesize) / total * 100 223 224 wr.writerow([f, pkgname, filesize, pkgsize, 225 "%.1f" % percent_pkg, 226 "%.1f" % percent_total]) 227 228 229# 230# Generate a CSV file with statistics about the size of each package, 231# and their size contribution to the overall system. 232# 233# pkgsize: dictionary with the name of the package as a key, and the 234# size as the value, as returned by build_package_size. 235# 236# outputf: output CSV file 237# 238def gen_packages_csv(pkgsizes, outputf): 239 total = sum(pkgsizes.values()) 240 with open(outputf, 'w') as csvfile: 241 wr = csv.writer(csvfile, delimiter=',', quoting=csv.QUOTE_MINIMAL) 242 wr.writerow(["Package name", "Package size", 243 "Package size in system (%)"]) 244 for (pkg, size) in pkgsizes.items(): 245 wr.writerow([pkg, size, "%.1f" % (float(size) / total * 100)]) 246 247 248# 249# Our special action for --iec, --binary, --si, --decimal 250# 251class PrefixAction(argparse.Action): 252 def __init__(self, option_strings, dest, **kwargs): 253 for key in ["type", "nargs"]: 254 if key in kwargs: 255 raise ValueError('"{}" not allowed'.format(key)) 256 super(PrefixAction, self).__init__(option_strings, dest, nargs=0, 257 type=bool, **kwargs) 258 259 def __call__(self, parser, namespace, values, option_string=None): 260 setattr(namespace, self.dest, option_string in ["--iec", "--binary"]) 261 262 263def main(): 264 parser = argparse.ArgumentParser(description='Draw size statistics graphs') 265 266 parser.add_argument("--builddir", '-i', metavar="BUILDDIR", required=True, 267 help="Buildroot output directory") 268 parser.add_argument("--graph", '-g', metavar="GRAPH", 269 help="Graph output file (.pdf or .png extension)") 270 parser.add_argument("--file-size-csv", '-f', metavar="FILE_SIZE_CSV", 271 help="CSV output file with file size statistics") 272 parser.add_argument("--package-size-csv", '-p', metavar="PKG_SIZE_CSV", 273 help="CSV output file with package size statistics") 274 parser.add_argument("--biggest-first", action='store_true', 275 help="Sort packages in decreasing size order, " + 276 "rather than in increasing size order") 277 parser.add_argument("--iec", "--binary", "--si", "--decimal", 278 action=PrefixAction, 279 help="Use IEC (binary, powers of 1024) or SI (decimal, " 280 "powers of 1000, the default) prefixes") 281 parser.add_argument("--size-limit", "-l", type=float, 282 help='Under this size ratio, files are accounted to ' + 283 'the generic "Other" package. Default: 0.01 (1%%)') 284 args = parser.parse_args() 285 286 Config.biggest_first = args.biggest_first 287 Config.iec = args.iec 288 if args.size_limit is not None: 289 if args.size_limit < 0.0 or args.size_limit > 1.0: 290 parser.error("--size-limit must be in [0.0..1.0]") 291 Config.size_limit = args.size_limit 292 293 # Find out which package installed what files 294 pkgdict = build_package_dict(args.builddir) 295 296 # Collect the size installed by each package 297 pkgsize = build_package_size(pkgdict, args.builddir) 298 299 if args.graph: 300 draw_graph(pkgsize, args.graph) 301 if args.file_size_csv: 302 gen_files_csv(pkgdict, pkgsize, args.file_size_csv) 303 if args.package_size_csv: 304 gen_packages_csv(pkgsize, args.package_size_csv) 305 306 307if __name__ == "__main__": 308 main() 309