Graph about installed size per package
v3 cover Graph about installed size per package
Hello,
Here is the third iteration of the patches adding a 'size-stats' feature, which mainly allows to generate a pie chart of the size contribution of each package to the overall root filesystem size.
Changes since v2:
- Use print0 in find and -0 in xargs to support spaces in filenames, just in case. Suggested by J?r?me Pouiller.
- Support custom skeleton locations, instead of assuming the skeleton comes from system/skeleton.
- Fix the size-stats target to actually call the size-stats script, and not graph-size, which was the old name of the script in previous versions of the series.
- Fix usage of the size-stats target in out of tree build situations. For consistency reasons, we chose to mimic what graph-depends already does, even if size-stats does not necessarily need to be executed from within CONFIG_DIR.
- Add one patch to make sure that 'make clean' removes the graphs/ subdirectory in the output directory, which wasn't done today. This issue isn't specific to size-stats, and already existed with graph-build and graph-depends.
Comments:
- J?r?me Puiller suggested to add a warning for removed files. However, many files are removed in the target-finalize step (man pages, etc.) and we don't want to have gazillions of warnings about these completely normal file removals.
Thomas Petazzoni (5):
Makefile: remove the graphs/ dir on 'make clean'
pkg-generic: add step_pkg_size global instrumentation hook support/scripts: add size-stats script
Makefile: implement a size-stats target
Dummy testing packages
Config.in | 9 ++ Makefile | 19 +++- package/Config.in | 2 + package/foo1/Config.in | 2 + package/foo1/file.img | Bin 0 -> 2097152 bytes package/foo1/foo1.mk | 8 ++ package/foo2/Config.in | 2 + package/foo2/file.img | Bin 0 -> 4194304 bytes package/foo2/foo2.mk | 8 ++ package/pkg-generic.mk | 36 +++++++ support/scripts/size-stats | 231 +++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 package/foo1/Config.in create mode 100644 package/foo1/file.img create mode 100644 package/foo1/foo1.mk create mode 100644 package/foo2/Config.in create mode 100644 package/foo2/file.img create mode 100644 package/foo2/foo2.mk create mode 100755 support/scripts/size-stats
Signature · 1 line
2.1.0
PATCH v3 1/5 Makefile: remove the graphs/ dir on 'make clean'
Currently, a 'make clean' leaves the graphs/ subdirectory in the output directory. This commit defines a GRAPHS_DIR variable, used by the different graph-generating targets, and which gets cleaned up in the 'clean' target.
--- Makefile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-)
Makefile +4 -3
@@ -161,6 +161,7 @@ TARGET_DIR := $(BASE_DIR)/target # initial definition so that 'make clean' works for most users, even without # .config. HOST_DIR will be overwritten later when .config is included. HOST_DIR := $(BASE_DIR)/host +GRAPHS_DIR := $(BASE_DIR)/graphs LEGAL_INFO_DIR = $(BASE_DIR)/legal-info REDIST_SOURCES_DIR_TARGET = $(LEGAL_INFO_DIR)/sources
@@ -658,7 +659,7 @@ show-targets: @echo $(HOST_DEPS) $(TARGETS_HOST_DEPS) $(TARGETS) $(TARGETS_ROOTFS) graph-build: $(O)/build/build-time.log - @install -d $(O)/graphs + @install -d $(GRAPHS_DIR) $(foreach o,name build duration,./support/scripts/graph-build-time \ --type=histogram --order=$(o) --input=$(<) \ --output=$(O)/graphs/build.hist-$(o).$(BR_GRAPH_OUT) \
@@ -673,7 +674,7 @@ graph-depends-requirements: { echo "ERROR: The 'dot' program from Graphviz is needed for graph-depends" >&2; exit 1; } graph-depends: graph-depends-requirements - @$(INSTALL) -d $(O)/graphs + @$(INSTALL) -d $(GRAPHS_DIR) @cd "$(CONFIG_DIR)"; \ $(TOPDIR)/support/scripts/graph-depends $(BR2_GRAPH_DEPS_OPTS) \ |tee $(BASE_DIR)/graphs/$(@).dot \
@@ -834,7 +835,7 @@ printvars: clean: rm -rf $(TARGET_DIR) $(BINARIES_DIR) $(HOST_DIR) \ $(BUILD_DIR) $(BASE_DIR)/staging \ - $(LEGAL_INFO_DIR) + $(LEGAL_INFO_DIR) $(GRAPHS_DIR) distclean: clean ifeq ($(DL_DIR),$(TOPDIR)/dl)
Signature · 1 line
2.1.0
PATCH v3 1/5 Makefile: remove the graphs/ dir on 'make clean'
Thomas, All,
On 2015-02-05 22:19 +0100, Thomas Petazzoni spake thusly:
Quoted 23 lines
Currently, a 'make clean' leaves the graphs/ subdirectory in the output directory. This commit defines a
GRAPHS_DIRvariable, used by the different graph-generating targets, and which gets cleaned up in the 'clean' target.Signed-off-by Thomas Petazzoni <[email protected]>--- Makefile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-)Makefile +1
-0@@ -161,6 +161,7 @@ TARGET_DIR := $(BASE_DIR)/target # initial definition so that 'make clean' works for most users, even without # .config. HOST_DIR will be overwritten later when .config is included. HOST_DIR := $(BASE_DIR)/host +GRAPHS_DIR := $(BASE_DIR)/graphs LEGAL_INFO_DIR = $(BASE_DIR)/legal-info REDIST_SOURCES_DIR_TARGET = $(LEGAL_INFO_DIR)/sources
Makefile show-targets: · line 665
Quoted 8 lines
Makefile +1
-1@@ -658,7 +659,7 @@ show-targets: @echo $(HOST_DEPS) $(TARGETS_HOST_DEPS) $(TARGETS) $(TARGETS_ROOTFS) graph-build: $(O)/build/build-time.log - @install -d $(O)/graphs + @install -d $(GRAPHS_DIR) $(foreach o,name build duration,./support/scripts/graph-build-time \ --type=histogram --order=$(o) --input=$(<) \ --output=$(O)/graphs/build.hist-$(o).$(BR_GRAPH_OUT) \
While at it, maybe you could take the ooportunity to change it here, too?
Makefile graph-depends-requirements: · line 680
Quoted 9 lines
Makefile +1
-1@@ -673,7 +674,7 @@ graph-depends-requirements: { echo "ERROR: The 'dot' program from Graphviz is needed for graph-depends" >&2; exit 1; } graph-depends: graph-depends-requirements - @$(INSTALL) -d $(O)/graphs + @$(INSTALL) -d $(GRAPHS_DIR) @cd "$(CONFIG_DIR)"; \ $(TOPDIR)/support/scripts/graph-depends $(BR2_GRAPH_DEPS_OPTS) \ |tee $(BASE_DIR)/graphs/$(@).dot \
And here?
Also, you're missing some occurences in package/pkg-generic.mk:
580 $(1)-graph-depends: graph-depends-requirements 581 @$$(INSTALL) -d $$(O)/graphs 582 @cd "$$(CONFIG_DIR)"; \ 583 $$(TOPDIR)/support/scripts/graph-depends -p $(1) $$(BR2_GRAPH_DEPS_OPTS) \ 584 |tee $$(O)/graphs/$$(@).dot \ 585 |dot $$(BR2_GRAPH_DOT_OPTS) -T$$(BR_GRAPH_OUT) -o $$(O)/graphs/$$(@).$$(BR_GRAPH_OUT)
Otherwise, looks good.
Regards,
Yann E. MORIN.
Quoted 16 lines
Makefile +1
-1@@ -834,7 +835,7 @@ printvars: clean: rm -rf $(TARGET_DIR) $(BINARIES_DIR) $(HOST_DIR) \ $(BUILD_DIR) $(BASE_DIR)/staging \ - $(LEGAL_INFO_DIR) + $(LEGAL_INFO_DIR) $(GRAPHS_DIR) distclean: clean ifeq ($(DL_DIR),$(TOPDIR)/dl)Signature · 6 lines
2.1.0 _______________________________________________ buildroot mailing list buildroot at busybox.net http://lists.busybox.net/mailman/listinfo/buildroot
Signature · 6 lines
.-----------------.--------------------.------------------.--------------------. | Yann E. MORIN | Real-Time Embedded | /"\ ASCII RIBBON | Erics' conspiracy: | | +33 662 376 056 | Software Designer | \ / CAMPAIGN | ___ | | +33 223 225 172 `------------.-------: X AGAINST | \e/ There is no | | http://ymorin.is-a-geek.org/ | _/*\_ | / \ HTML MAIL | v conspiracy. | '------------------------------^-------^------------------^--------------------'
PATCH v3 1/5 Makefile: remove the graphs/ dir on 'make clean'
In reply to Thomas PetazzoniHello,
Quoted 6 lines · Thomas Petazzoni
Currently, a 'make clean' leaves the graphs/ subdirectory in the output directory. This commit defines a
GRAPHS_DIRvariable, used by the different graph-generating targets, and which gets cleaned up in the 'clean' target.Signed-off-by Thomas Petazzoni <[email protected]>
As suggested by Yann, I've changed the patch to use GRAPHS_DIR in various other places to replace $(O)/graphs, and applied.
Thomas
Signature · 3 lines
Thomas Petazzoni, CTO, Free Electrons Embedded Linux, Kernel and Android engineering http://free-electrons.com
PATCH v3 2/5 pkg-generic: add step_pkg_size global instrumentation hook
In reply to Thomas PetazzoniThis patch adds a global instrumentation hook that collects the list of files installed in $(TARGET_DIR) by each package, and stores this list into a file called $(BUILD_DIR)/packages-file-list.txt. It can later be used to determine the size contribution of each package to the target root filesystem.
Note that in order to detect if a file installed by one package is later overriden by another package, we calculate the md5 of installed files and compare them at each installation of a new package.
This commit also adds a Config.in option to enable the collection of this data, as calculating the md5 of all installed files at the beginning and end of the installation of each package can be considered a time-consuming process which maybe some users will not be willing to suffer from.
--- Config.in | 9 +++++++++ package/pkg-generic.mk | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+)
Config.in +9 -0
@@ -613,6 +613,15 @@ config BR2_COMPILER_PARANOID_UNSAFE_PATH toolchain (through gcc and binutils patches) and external toolchain backends (through the external toolchain wrapper). +config BR2_COLLECT_FILE_SIZE_STATS + bool "collect statistics about installed file size" + help + Enable this option to let Buildroot collect data about the + installed files. When this option is enabled, you will be + able to use the 'size-stats' make target, which will + generate a graph and CSV files giving statistics about the + installed size of each file and each package. + endmenu endmenu
package/pkg-generic.mk +36 -0
@@ -55,6 +55,42 @@ define step_time endef GLOBAL_INSTRUMENTATION_HOOKS += step_time +# Hooks to collect statistics about installed files +ifeq ($(BR2_COLLECT_FILE_SIZE_STATS),y) + +# This hook will be called before the target installation of a +# package. We store in a file named $(1).filelist_before the list of +# files currently installed in the target. Note that the MD5 is also +# stored, in order to identify if the files are overwritten. +define step_pkg_size_start + (cd $(TARGET_DIR) ; find . -type f -print0 | xargs -0 md5sum) | sort > \ + $(BUILD_DIR)/$(1).filelist_before +endef + +# This hook will be called after the target installation of a +# package. We store in a file named $(1).filelist_after the list +# of files (and their MD5) currently installed in the target. We then +# do a diff with the $(1).filelist_before to compute the list of +# files installed by this package. +define step_pkg_size_end + (cd $(TARGET_DIR); find . -type f -print0 | xargs -0 md5sum) | sort > \ + $(BUILD_DIR)/$(1).filelist_after + comm -13 $(BUILD_DIR)/$(1).filelist_before $(BUILD_DIR)/$(1).filelist_after | \ + while read hash file ; do \ + echo "$(1),$${file}" >> $(BUILD_DIR)/packages-file-list.txt ; \ + done + $(RM) -f $(BUILD_DIR)/$(1).filelist_before \ + $(BUILD_DIR)/$(1).filelist_after +endef + +define step_pkg_size + $(if $(filter install-target,$(2)),\ + $(if $(filter start,$(1)),$(call step_pkg_size_start,$(3))) \ + $(if $(filter end,$(1)),$(call step_pkg_size_end,$(3)))) +endef +GLOBAL_INSTRUMENTATION_HOOKS += step_pkg_size +endif + # User-supplied script ifneq ($(BR2_INSTRUMENTATION_SCRIPTS),) define step_user
Signature · 1 line
2.1.0
PATCH v3 2/5 pkg-generic: add step_pkg_size global instrumentation hook
Thomas, All,
On 2015-02-05 22:19 +0100, Thomas Petazzoni spake thusly:
Quoted 10 lines
This patch adds a global instrumentation hook that collects the list of files installed in $(TARGET_DIR) by each package, and stores this list into a file called $(BUILD_DIR)/packages-file-list.txt. It can later be used to determine the size contribution of each package to the target root filesystem.
Note that in order to detect if a file installed by one package is later overriden by another package, we calculate the md5 of installed files and compare them at each installation of a new package.
Quoted 5 lines
This commit also adds a Config.in option to enable the collection of this data, as calculating the md5 of all installed files at the beginning and end of the installation of each package can be considered a time-consuming process which maybe some users will not be willing to suffer from.
Well, I'd like to challenge that assertion, so I did a pretty "big" build:
Kodi on the RPi, with all Kodi addons enabled, plus a few additional usefull packages (connman, dropbear and the likes).
The config has about 148 packages (make show-targets |wc -w), and takes roughly 1h and 20min on my machine.
Because I did not time md5sum after each package was installed, I simply timed the md5sum at the end, on a completely-populated target/ . That gives a pretty good upper-bound of the overhead each package would incur (i.e. the very first packages would be so much faster as there are far fewer files installed).
$ du -hs target/
247M target/ $ find target -type f |wc -l
5150 $ tar cf - target/ |wc -c
242923520 $ tar cf - target/ |time md5sum
1d393aaf76ef6a7a462519f4b8b861e7 -
0.36user 0.03system 0:00.41elapsed 96%CPU (0avgtext+0avgdata 748maxresident)k
0inputs+0outputs (0major+233minor)pagefaults 0swaps $ date '+%s.%N'; \
find target -type f -print0 2>/dev/null \
| xargs -0 md5sum >/dev/null 2>&1; \
date '+%s.%N'
1424018960.577764719
1424018960.994814894So, the overhead of md5sum-ing each file independently (on a cache-hot target/) is about less than 0.5s (only so-slightly bigger than md5sum-ing the whole tarball thereof) .
Yes, 0.5s. Half-a-second. ;-)
That would give an upper-bound of the overhead for the whole build somewhere in the 2-minute range (148*2*0.5). Out of a 1h 20min build.
Yes, md5 is a very fast hash. For reference, hashing a 512MiB blob takes about less than a second.
I believe this overhead is negligible and we should unconditionally enable that feature.
Quoted 34 lines
Signed-off-by Thomas Petazzoni <[email protected]>--- Config.in | 9 +++++++++ package/pkg-generic.mk | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+)Config.in +9
-0@@ -613,6 +613,15 @@ config BR2_COMPILER_PARANOID_UNSAFE_PATH toolchain (through gcc and binutils patches) and external toolchain backends (through the external toolchain wrapper). +config BR2_COLLECT_FILE_SIZE_STATS + bool "collect statistics about installed file size" + help + Enable this option to let Buildroot collect data about the + installed files. When this option is enabled, you will be + able to use the 'size-stats' make target, which will + generate a graph and CSV files giving statistics about the + installed size of each file and each package. + endmenu endmenupackage/pkg-generic.mk +0
-0@@ -55,6 +55,42 @@ define step_time endef GLOBAL_INSTRUMENTATION_HOOKS += step_time
package/pkg-generic.mk define step_time · line 67
Quoted 10 lines
package/pkg-generic.mk +10
-0+# Hooks to collect statistics about installed files +ifeq ($(BR2_COLLECT_FILE_SIZE_STATS),y) + +# This hook will be called before the target installation of a +# package. We store in a file named $(1).filelist_before the list of +# files currently installed in the target. Note that the MD5 is also +# stored, in order to identify if the files are overwritten. +define step_pkg_size_start + (cd $(TARGET_DIR) ; find . -type f -print0 | xargs -0 md5sum) | sort > \ + $(BUILD_DIR)/$(1).filelist_before
Why don't you store that in the package's $(@D) ?
I don't really care, but if we're going to use $(BUILD_DIR) to store temporary files, it might be time we introduce a better location (probably somthing like BR2_TMP_DIR=$(BUILD_DIR)/.tmp/ )
We alreaduy ahve some temporary stuff written in there, and I find it ugly (yes, I added some myself!).
Note: not related to your changes, of course, just prompted by them.
Quoted 13 lines
package/pkg-generic.mk +13
-0+endef + +# This hook will be called after the target installation of a +# package. We store in a file named $(1).filelist_after the list +# of files (and their MD5) currently installed in the target. We then +# do a diff with the $(1).filelist_before to compute the list of +# files installed by this package. +define step_pkg_size_end + (cd $(TARGET_DIR); find . -type f -print0 | xargs -0 md5sum) | sort > \ + $(BUILD_DIR)/$(1).filelist_after + comm -13 $(BUILD_DIR)/$(1).filelist_before $(BUILD_DIR)/$(1).filelist_after | \ + while read hash file ; do \ + echo "$(1),$${file}" >> $(BUILD_DIR)/packages-file-list.txt ; \
package/pkg-generic.mk define step_time · line 90
Quoted 10 lines
package/pkg-generic.mk +10
-0+ done + $(RM) -f $(BUILD_DIR)/$(1).filelist_before \ + $(BUILD_DIR)/$(1).filelist_after +endef + +define step_pkg_size + $(if $(filter install-target,$(2)),\ + $(if $(filter start,$(1)),$(call step_pkg_size_start,$(3))) \ + $(if $(filter end,$(1)),$(call step_pkg_size_end,$(3)))) +endef
When I introduced the instrumentation hooks, I did not envision they would be used like that, directly as Makefile code.
What I expected is we would be using scripts (python, shell, whatever!) somewhere in support/ , that would do their own filtering.
It's pretty fascinating how we all differ in reasoning! :-)
Regards,
Yann E. MORIN.
Quoted 13 lines
package/pkg-generic.mk +3
-0+GLOBAL_INSTRUMENTATION_HOOKS += step_pkg_size +endif + # User-supplied script ifneq ($(BR2_INSTRUMENTATION_SCRIPTS),) define step_userSignature · 6 lines
2.1.0 _______________________________________________ buildroot mailing list buildroot at busybox.net http://lists.busybox.net/mailman/listinfo/buildroot
Signature · 6 lines
.-----------------.--------------------.------------------.--------------------. | Yann E. MORIN | Real-Time Embedded | /"\ ASCII RIBBON | Erics' conspiracy: | | +33 662 376 056 | Software Designer | \ / CAMPAIGN | ___ | | +33 223 225 172 `------------.-------: X AGAINST | \e/ There is no | | http://ymorin.is-a-geek.org/ | _/*\_ | / \ HTML MAIL | v conspiracy. | '------------------------------^-------^------------------^--------------------'
PATCH v3 3/5 support/scripts: add size-stats script
In reply to Thomas PetazzoniThis new script uses the data collected by the step_pkg_size instrumentation hook to generate a pie chart of the size contribution of each package to the target root filesystem, and two CSV files with statistics about the package size and file size. To achieve this, it looks at each file in $(TARGET_DIR), and using the packages-file-list.txt information collected by the step_pkg_size hook, it determines to which package the file belongs. It is therefore able to give the size installed by each package.
--- support/scripts/size-stats | 231 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100755 support/scripts/size-stats
support/scripts/size-stats new file +231 -0
@@ -0,0 +1,231 @@ +#!/usr/bin/env python + +# Copyright (C) 2014 by Thomas Petazzoni <[email protected]> + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import sys +import os +import os.path +import argparse +import csv + +try: + import matplotlib.font_manager as fm + import matplotlib.pyplot as plt +except ImportError: + sys.stderr.write("You need python-matplotlib to generate the size graph\n") + exit(1) + +colors = ['#e60004', '#009836', '#2e1d86', '#ffed00', + '#0068b5', '#f28e00', '#940084', '#97c000'] + +# +# This function returns a dict containing as keys the files present in +# the filesystem skeleton, and as value, the string "skeleton". It is +# used to simulate a fake "skeleton" package, to assign the files from +# the skeleton to some package. +# +# builddir: path to the Buildroot output directory +# skeleton_path: path to the rootfs skeleton +# +def build_skeleton_dict(builddir, skeleton_path): + skeleton_files = {} + for root, _, files in os.walk(skeleton_path): + for f in files: + if f == ".empty": + continue + frelpath = os.path.relpath(os.path.join(root, f), skeleton_path) + # Get the real size of the installed file + targetpath = os.path.join(builddir, "target", frelpath) + if os.path.islink(targetpath): + continue + sz = os.stat(targetpath).st_size + skeleton_files[frelpath] = { 'pkg': "skeleton", 'size': sz } + return skeleton_files + +# +# This function returns a dict where each key is the path of a file in +# the root filesystem, and the value is a dict containing two +# elements: the name of the package to which this file belongs (key: +# pkg) and the size of the file (key: size). +# +# builddir: path to the Buildroot output directory +# +def build_package_dict(builddir): + pkgdict = {} + with open(os.path.join(builddir, "build", "packages-file-list.txt")) as filelistf: + for l in filelistf.readlines(): + f = l.split(",") + fpath = f[1].strip().replace("./", "") + fullpath = os.path.join(builddir, "target", fpath) + if not os.path.exists(fullpath): + continue + pkg = f[0] + sz = os.stat(fullpath).st_size + pkgdict[fpath] = { 'pkg': pkg, 'size': sz } + return pkgdict + +# +# This function builds a dictionary that contains the name of a +# package as key, and the size of the files installed by this package +# as the value. +# +# pkgdict: dictionary with the name of the files as key, and as value +# a dict containing the name of the package to which the files +# belongs, and the size of the file. As returned by +# build_package_dict. +# +# builddir: path to the Buildroot output directory +# +def build_package_size(pkgdict, builddir): + pkgsize = {} + + for root, _, files in os.walk(os.path.join(builddir, "target")): + for f in files: + fpath = os.path.join(root, f) + if os.path.islink(fpath): + continue + frelpath = os.path.relpath(fpath, os.path.join(builddir, "target")) + if not frelpath in pkgdict: + print("WARNING: %s is not part of any package" % frelpath) + pkg = "unknown" + else: + pkg = pkgdict[frelpath]["pkg"] + + if pkg in pkgsize: + pkgsize[pkg] += os.path.getsize(fpath) + else: + pkgsize[pkg] = os.path.getsize(fpath) + + return pkgsize + +# +# Given a dict returned by build_package_size(), this function +# generates a pie chart of the size installed by each package. +# +# pkgsize: dictionary with the name of the package as a key, and the +# size as the value, as returned by build_package_size. +# +# outputf: output file for the graph +# +def draw_graph(pkgsize, outputf): + total = 0 + for (p, sz) in pkgsize.items(): + total += sz + labels = [] + values = [] + other_value = 0 + for (p, sz) in pkgsize.items(): + if sz < (total * 0.01): + other_value += sz + else: + labels.append("%s (%d kB)" % (p, sz / 1000.)) + values.append(sz) + labels.append("Other (%d kB)" % (other_value / 1000.)) + values.append(other_value) + + plt.figure() + patches, texts, autotexts = plt.pie(values, labels=labels, + autopct='%1.1f%%', shadow=True, + colors=colors) + # Reduce text size + proptease = fm.FontProperties() + proptease.set_size('xx-small') + plt.setp(autotexts, fontproperties=proptease) + plt.setp(texts, fontproperties=proptease) + + plt.title('Size per package') + plt.savefig(outputf) + +# +# Generate a CSV file with statistics about the size of each file, its +# size contribution to the package and to the overall system. +# +# pkgdict: dictionary with the name of the files as key, and as value +# a dict containing the name of the package to which the files +# belongs, and the size of the file. As returned by +# build_package_dict. +# +# pkgsize: dictionary with the name of the package as a key, and the +# size as the value, as returned by build_package_size. +# +# outputf: output CSV file +# +def gen_files_csv(pkgdict, pkgsizes, outputf): + total = 0 + for (p, sz) in pkgsizes.items(): + total += sz + with open(outputf, 'w') as csvfile: + wr = csv.writer(csvfile, delimiter=',', quoting=csv.QUOTE_MINIMAL) + wr.writerow(["File name", + "Package name", + "File size", + "Package size", + "File size in package (%)", + "File size in system (%)"]) + for (f, info) in pkgdict.items(): + pkgname = info["pkg"] + filesize = info["size"] + pkgsize = pkgsizes[pkgname] + wr.writerow([f, pkgname, filesize, pkgsize, "%.4f" % (float(filesize) / pkgsize * 100), "%.4f" % (float(filesize) / total * 100)]) + + +# +# Generate a CSV file with statistics about the size of each package, +# and their size contribution to the overall system. +# +# pkgsize: dictionary with the name of the package as a key, and the +# size as the value, as returned by build_package_size. +# +# outputf: output CSV file +# +def gen_packages_csv(pkgsizes, outputf): + total = 0 + for (p, sz) in pkgsizes.items(): + total += sz + with open(outputf, 'w') as csvfile: + wr = csv.writer(csvfile, delimiter=',', quoting=csv.QUOTE_MINIMAL) + wr.writerow(["Package name", "Package size", "Package size in system (%)"]) + for (pkg, size) in pkgsizes.items(): + wr.writerow([pkg, size, "%.4f" % (float(size) / total * 100)]) + +parser = argparse.ArgumentParser(description='Draw build time graphs') + +parser.add_argument("--builddir", '-i', metavar="BUILDDIR", required=True, + help="Buildroot output directory") +parser.add_argument("--graph", '-g', metavar="GRAPH", + help="Graph output file (.pdf or .png extension)") +parser.add_argument("--file-size-csv", '-f', metavar="FILE_SIZE_CSV", + help="CSV output file with file size statistics") +parser.add_argument("--package-size-csv", '-p', metavar="PKG_SIZE_CSV", + help="CSV output file with package size statistics") +parser.add_argument("--skeleton-path", '-s', metavar="SKELETON_PATH", required=True, + help="Path to the skeleton used for the system") +args = parser.parse_args() + +# Find out which package installed what files +pkgdict = build_package_dict(args.builddir) +pkgdict.update(build_skeleton_dict(args.builddir, args.skeleton_path)) + +# Collect the size installed by each package +pkgsize = build_package_size(pkgdict, args.builddir) + +if args.graph: + draw_graph(pkgsize, args.graph) +if args.file_size_csv: + gen_files_csv(pkgdict, pkgsize, args.file_size_csv) +if args.package_size_csv: + gen_packages_csv(pkgsize, args.package_size_csv)
Signature · 1 line
2.1.0
PATCH v3 3/5 support/scripts: add size-stats script
Quoted 8 lines · Thomas Petazzoni
This new script uses the data collected by the
step_pkg_sizeinstrumentation hook to generate a pie chart of the size contribution of each package to the target root filesystem, and two CSV files with statistics about the package size and file size. To achieve this, it looks at each file in $(TARGET_DIR), and using the packages-file-list.txt information collected by thestep_pkg_sizehook, it determines to which package the file belongs. It is therefore able to give the size installed by each package.
I'm reviewing purely the python coding style here. Found no major shortcomings,
just a few things that can be done in a nicer way, so
Quoted 57 lines
Signed-off-by Thomas Petazzoni <[email protected]>--- support/scripts/size-stats | 231 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100755 support/scripts/size-statssupport/scripts/size-stats new file +44
-0@@ -0,0 +1,231 @@ +#!/usr/bin/env python + +# Copyright (C) 2014 by Thomas Petazzoni <[email protected]> + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import sys +import os +import os.path +import argparse +import csv + +try: + import matplotlib.font_manager as fm + import matplotlib.pyplot as plt +except ImportError: + sys.stderr.write("You need python-matplotlib to generate the size graph\n") + exit(1) + +colors = ['#e60004', '#009836', '#2e1d86', '#ffed00', + '#0068b5', '#f28e00', '#940084', '#97c000'] + +# +# This function returns a dict containing as keys the files present in +# the filesystem skeleton, and as value, the string "skeleton". It is +# used to simulate a fake "skeleton" package, to assign the files from +# the skeleton to some package. +# +# builddir: path to the Buildroot output directory +# skeleton_path: path to the rootfs skeleton +# +def build_skeleton_dict(builddir, skeleton_path):
support/scripts/size-stats line 54
Quoted 10 lines
support/scripts/size-stats new file +10
-0+ skeleton_files = {} + for root, _, files in os.walk(skeleton_path): + for f in files: + if f == ".empty": + continue + frelpath = os.path.relpath(os.path.join(root, f), skeleton_path) + # Get the real size of the installed file + targetpath = os.path.join(builddir, "target", frelpath) + if os.path.islink(targetpath): + continue
There should be a similar escape for files that don't exist in target (removed
by finalize script).
Quoted 7 lines
support/scripts/size-stats new file +7
-0+ sz = os.stat(targetpath).st_size + skeleton_files[frelpath] = { 'pkg': "skeleton", 'size': sz } + return skeleton_files + +# +# This function returns a dict where each key is the path of a file in +# the root filesystem, and the value is a dict containing two
support/scripts/size-stats line 71
Quoted 10 lines
support/scripts/size-stats new file +10
-0+# elements: the name of the package to which this file belongs (key: +# pkg) and the size of the file (key: size). +# +# builddir: path to the Buildroot output directory +# +def build_package_dict(builddir): + pkgdict = {} + with open(os.path.join(builddir, "build", "packages-file-list.txt")) as filelistf: + for l in filelistf.readlines(): + f = l.split(",")
I think a nicer way to write this is
pkg,fpath = l.split(",")
support/scripts/size-stats line 72
Quoted 1 line
support/scripts/size-stats new file +1
-0+ fpath = f[1].strip().replace("./", "")
Not likely to occur, but this will also replace foo./bar to foobar.
support/scripts/size-stats line 78
Quoted 6 lines
support/scripts/size-stats new file +6
-0+ fullpath = os.path.join(builddir, "target", fpath) + if not os.path.exists(fullpath): + continue + pkg = f[0] + sz = os.stat(fullpath).st_size + pkgdict[fpath] = { 'pkg': pkg, 'size': sz }
This entire part could be refactored with the skeleton handling. I think that
escaping symlinks is also relevant here.
Quoted 12 lines
support/scripts/size-stats new file +12
-0+ return pkgdict + +# +# This function builds a dictionary that contains the name of a +# package as key, and the size of the files installed by this package +# as the value. +# +# pkgdict: dictionary with the name of the files as key, and as value +# a dict containing the name of the package to which the files +# belongs, and the size of the file. As returned by +# build_package_dict. +#
support/scripts/size-stats line 100
Quoted 10 lines
support/scripts/size-stats new file +10
-0+# builddir: path to the Buildroot output directory +# +def build_package_size(pkgdict, builddir): + pkgsize = {} + + for root, _, files in os.walk(os.path.join(builddir, "target")): + for f in files: + fpath = os.path.join(root, f) + if os.path.islink(fpath): + continue
Ah, you escape the link here... Makes sense of course otherwise it would become
unknown. But why is it different for the skeleton?
support/scripts/size-stats line 111
Quoted 11 lines
support/scripts/size-stats new file +11
-0+ frelpath = os.path.relpath(fpath, os.path.join(builddir, "target")) + if not frelpath in pkgdict: + print("WARNING: %s is not part of any package" % frelpath) + pkg = "unknown" + else: + pkg = pkgdict[frelpath]["pkg"] + + if pkg in pkgsize: + pkgsize[pkg] += os.path.getsize(fpath) + else: + pkgsize[pkg] = os.path.getsize(fpath)
Use a defaultdict [1]
import collections
pkgsize = collections.defaultdict(int)
pkgsize[pkg] += os.path.getsize(fpath)
Quoted 6 lines
support/scripts/size-stats new file +6
-0+ + return pkgsize + +# +# Given a dict returned by build_package_size(), this function +# generates a pie chart of the size installed by each package.
support/scripts/size-stats line 127
Quoted 10 lines
support/scripts/size-stats new file +10
-0+# +# pkgsize: dictionary with the name of the package as a key, and the +# size as the value, as returned by build_package_size. +# +# outputf: output file for the graph +# +def draw_graph(pkgsize, outputf): + total = 0 + for (p, sz) in pkgsize.items(): + total += sz
total = sum(pkgsize.values())
Quoted 18 lines
support/scripts/size-stats new file +18
-0+ labels = [] + values = [] + other_value = 0 + for (p, sz) in pkgsize.items(): + if sz < (total * 0.01): + other_value += sz + else: + labels.append("%s (%d kB)" % (p, sz / 1000.)) + values.append(sz) + labels.append("Other (%d kB)" % (other_value / 1000.)) + values.append(other_value) + + plt.figure() + patches, texts, autotexts = plt.pie(values, labels=labels, + autopct='%1.1f%%', shadow=True, + colors=colors) + # Reduce text size + proptease = fm.FontProperties()
support/scripts/size-stats line 155
Quoted 10 lines
support/scripts/size-stats new file +10
-0+ proptease.set_size('xx-small') + plt.setp(autotexts, fontproperties=proptease) + plt.setp(texts, fontproperties=proptease) + + plt.title('Size per package') + plt.savefig(outputf) + +# +# Generate a CSV file with statistics about the size of each file, its +# size contribution to the package and to the overall system.
I don't think this file is very useful, but obviously it doesn't hurt to
generate it.
support/scripts/size-stats line 89
Quoted 5 lines
support/scripts/size-stats new file +5
-0+# +# pkgdict: dictionary with the name of the files as key, and as value +# a dict containing the name of the package to which the files +# belongs, and the size of the file. As returned by +# build_package_dict.
Since it has files as dict, I would think it makes more sense to call it filesdict.
support/scripts/size-stats line 170
Quoted 10 lines
support/scripts/size-stats new file +10
-0+# +# pkgsize: dictionary with the name of the package as a key, and the +# size as the value, as returned by build_package_size. +# +# outputf: output CSV file +# +def gen_files_csv(pkgdict, pkgsizes, outputf): + total = 0 + for (p, sz) in pkgsizes.items(): + total += sz
total = sum(pkgsizes.values())
support/scripts/size-stats line 181
Quoted 11 lines
support/scripts/size-stats new file +11
-0+ with open(outputf, 'w') as csvfile: + wr = csv.writer(csvfile, delimiter=',', quoting=csv.QUOTE_MINIMAL) + wr.writerow(["File name", + "Package name", + "File size", + "Package size", + "File size in package (%)", + "File size in system (%)"]) + for (f, info) in pkgdict.items(): + pkgname = info["pkg"] + filesize = info["size"]
Given the way that this is used, I would define the pkgdict as a dict of pairs
instead of a dict of dicts. So this would become
for f, (pkgname, filesize) in pkgdict.items():
support/scripts/size-stats line 183
Quoted 2 lines
support/scripts/size-stats new file +2
-0+ pkgsize = pkgsizes[pkgname] + wr.writerow([f, pkgname, filesize, pkgsize, "%.4f" % (float(filesize) / pkgsize * 100), "%.4f" % (float(filesize) / total * 100)])
Can this long line be split? Also, I find .4f a bit too many digits - .1 ought
to be enough. Anything below .1% is really 0, no?
support/scripts/size-stats line 195
Quoted 12 lines
support/scripts/size-stats new file +12
-0+ + +# +# Generate a CSV file with statistics about the size of each package, +# and their size contribution to the overall system. +# +# pkgsize: dictionary with the name of the package as a key, and the +# size as the value, as returned by build_package_size. +# +# outputf: output CSV file +# +def gen_packages_csv(pkgsizes, outputf):
In the other functions it's called pkgsize instead of pkgsizes.
support/scripts/size-stats line 170
Quoted 3 lines
support/scripts/size-stats new file +3
-0+ total = 0 + for (p, sz) in pkgsizes.items(): + total += sz
total = sum(well you get the drift :-)
Quoted 13 lines
support/scripts/size-stats new file +13
-0+ with open(outputf, 'w') as csvfile: + wr = csv.writer(csvfile, delimiter=',', quoting=csv.QUOTE_MINIMAL) + wr.writerow(["Package name", "Package size", "Package size in system (%)"]) + for (pkg, size) in pkgsizes.items(): + wr.writerow([pkg, size, "%.4f" % (float(size) / total * 100)]) + +parser = argparse.ArgumentParser(description='Draw build time graphs') + +parser.add_argument("--builddir", '-i', metavar="BUILDDIR", required=True, + help="Buildroot output directory") +parser.add_argument("--graph", '-g', metavar="GRAPH", + help="Graph output file (.pdf or .png extension)") +parser.add_argument("--file-size-csv", '-f', metavar="FILE_SIZE_CSV",
support/scripts/size-stats line 221
Quoted 10 lines
support/scripts/size-stats new file +10
-0+ help="CSV output file with file size statistics") +parser.add_argument("--package-size-csv", '-p', metavar="PKG_SIZE_CSV", + help="CSV output file with package size statistics") +parser.add_argument("--skeleton-path", '-s', metavar="SKELETON_PATH", required=True, + help="Path to the skeleton used for the system") +args = parser.parse_args() + +# Find out which package installed what files +pkgdict = build_package_dict(args.builddir) +pkgdict.update(build_skeleton_dict(args.builddir, args.skeleton_path))
No, skeleton should be first, packages afterwards. If a package overwrites a
skeleton file, the package should win.
Also, there should be something similar like skeleton for the overlay(s), and
that should of course come after the packages.
Regards,
Arnout
support/scripts/size-stats line 231
Quoted 11 lines
support/scripts/size-stats new file +10
-0+ +# Collect the size installed by each package +pkgsize = build_package_size(pkgdict, args.builddir) + +if args.graph: + draw_graph(pkgsize, args.graph) +if args.file_size_csv: + gen_files_csv(pkgdict, pkgsize, args.file_size_csv) +if args.package_size_csv: + gen_packages_csv(pkgsize, args.package_size_csv)
[1] http://jtauber.com/blog/2008/02/27/evolution_of_default_dictionaries_in_python/
Signature · 6 lines
Arnout Vandecappelle arnout at mind be Senior Embedded Software Architect +32-16-286500 Essensium/Mind http://www.mind.be G.Geenslaan 9, 3001 Leuven, Belgium BE 872 984 063 RPR Leuven LinkedIn profile: http://www.linkedin.com/in/arnoutvandecappelle GPG fingerprint: 7CB5 E4CC 6C2E EFD4 6E3D A754 F963 ECAB 2450 2F1F
PATCH v3 4/5 Makefile: implement a size-stats target
In reply to Thomas PetazzoniThis commit implements a size-stats target that calls the script of the same name to generate the graph and CSV files related to package and file sizes.
--- Makefile | 12 ++++++++++++ 1 file changed, 12 insertions(+)
Makefile +12 -0
@@ -680,6 +680,17 @@ graph-depends: graph-depends-requirements |tee $(BASE_DIR)/graphs/$(@).dot \ |dot $(BR2_GRAPH_DOT_OPTS) -T$(BR_GRAPH_OUT) -o $(BASE_DIR)/graphs/$(@).$(BR_GRAPH_OUT) +size-stats: + @[ -f $(O)/build/packages-file-list.txt ] || \ + { echo "ERROR: No package size information available, please rebuild with BR2_COLLECT_FILE_SIZE_STATS" ; exit 1; } + @$(INSTALL) -d $(GRAPHS_DIR) + @cd "$(CONFIG_DIR)"; \ + $(TOPDIR)/support/scripts/size-stats --builddir $(BASE_DIR) \ + --graph $(BASE_DIR)/graphs/graph-size.$(BR_GRAPH_OUT) \ + --file-size-csv $(BASE_DIR)/build/file-size-stats.csv \ + --package-size-csv $(BASE_DIR)/build/package-size-stats.csv \ + --skeleton-path $(TARGET_SKELETON) + else # ifeq ($(BR2_HAVE_DOT_CONFIG),y) all: menuconfig
@@ -896,6 +907,7 @@ endif @echo ' manual-epub - build manual in ePub' @echo ' graph-build - generate graphs of the build times' @echo ' graph-depends - generate graph of the dependency tree' + @echo ' size-stats - generate stats of the filesystem size' @echo @echo 'Miscellaneous:' @echo ' source - download all sources needed for offline-build'
Signature · 1 line
2.1.0
PATCH v3 4/5 Makefile: implement a size-stats target
Quoted 14 lines · Thomas Petazzoni
This commit implements a size-stats target that calls the script of the same name to generate the graph and CSV files related to package and file sizes.
Signed-off-by Thomas Petazzoni <[email protected]>--- Makefile | 12 ++++++++++++ 1 file changed, 12 insertions(+)Makefile +0
-0
Makefile graph-depends: graph-depends-requirements · line 685
Quoted 6 lines
Makefile +3
-0@@ -680,6 +680,17 @@ graph-depends: graph-depends-requirements |tee $(BASE_DIR)/graphs/$(@).dot \ |dot $(BR2_GRAPH_DOT_OPTS) -T$(BR_GRAPH_OUT) -o $(BASE_DIR)/graphs/$(@).$(BR_GRAPH_OUT) +size-stats: + @[ -f $(O)/build/packages-file-list.txt ] || \ + { echo "ERROR: No package size information available, please rebuild with BR2_COLLECT_FILE_SIZE_STATS" ; exit 1; }
BR2_COLLECT_FILE_SIZE_STATS=y
Also the line is too long. And perhaps instead of "rebuild" it should say "do a
clean build", otherwise stupid people will just do another make with the option enabled but nothing will actually be done.
Makefile graph-depends: graph-depends-requirements · line 686
Quoted 1 line
Makefile +1
-0+ @$(INSTALL) -d $(GRAPHS_DIR)
graph-depends is the only one that uses install -d, all the others use mkdir
-p. So let's stick to that.
Makefile graph-depends: graph-depends-requirements · line 687
Quoted 1 line
Makefile +1
-0+ @cd "$(CONFIG_DIR)"; \
Why is this needed? In fact, it's wrong, because TARGET_SKELETON will be
relative to TOPDIR if it's given as a relative path. All the other paths are absolute paths, so there's no reason to cd AFAICS.
Also, I'd prefer $(Q) instead of @.
Regards,
Arnout
Quoted 18 lines
Makefile +7
-0+ $(TOPDIR)/support/scripts/size-stats --builddir $(BASE_DIR) \ + --graph $(BASE_DIR)/graphs/graph-size.$(BR_GRAPH_OUT) \ + --file-size-csv $(BASE_DIR)/build/file-size-stats.csv \ + --package-size-csv $(BASE_DIR)/build/package-size-stats.csv \ + --skeleton-path $(TARGET_SKELETON) + else # ifeq ($(BR2_HAVE_DOT_CONFIG),y) all: menuconfig@@ -896,6 +907,7 @@ endif @echo ' manual-epub - build manual in ePub' @echo ' graph-build - generate graphs of the build times' @echo ' graph-depends - generate graph of the dependency tree' + @echo ' size-stats - generate stats of the filesystem size' @echo @echo 'Miscellaneous:' @echo ' source - download all sources needed for offline-build'
Signature · 6 lines
Arnout Vandecappelle arnout at mind be Senior Embedded Software Architect +32-16-286500 Essensium/Mind http://www.mind.be G.Geenslaan 9, 3001 Leuven, Belgium BE 872 984 063 RPR Leuven LinkedIn profile: http://www.linkedin.com/in/arnoutvandecappelle GPG fingerprint: 7CB5 E4CC 6C2E EFD4 6E3D A754 F963 ECAB 2450 2F1F
v3 cover Graph about installed size per package
In reply to Thomas PetazzoniHello,
Quoted 6 lines · Thomas Petazzoni
Thomas Petazzoni (5):
Makefile: remove the graphs/ dir on 'make clean'
pkg-generic: addstep_pkg_sizeglobal instrumentation hook support/scripts: add size-stats script
Makefile: implement a size-stats target
Dummy testing packages
Oops, this last patch shouldn't have been sent. Fortunately, since it is huge, it hasn't passed the filter of the list.
Peter, just reject this mail from me, as it doesn't make sense. The series is really only the first four patches.
Sorry for the mess,
Thomas
Signature · 3 lines
Thomas Petazzoni, CTO, Free Electrons Embedded Linux, Kernel and Android engineering http://free-electrons.com
v3 cover Graph about installed size per package
In reply to Thomas PetazzoniHi Thomas,
Le 05/02/2015 22:19, Thomas Petazzoni a ?crit :
Quoted 35 lines
Hello,
Here is the third iteration of the patches adding a 'size-stats' feature, which mainly allows to generate a pie chart of the size contribution of each package to the overall root filesystem size.
Changes since v2:
- Use print0 in find and -0 in xargs to support spaces in filenames, just in case. Suggested by J?r?me Pouiller.
- Support custom skeleton locations, instead of assuming the skeleton comes from system/skeleton.
- Fix the size-stats target to actually call the size-stats script, and not graph-size, which was the old name of the script in previous versions of the series.
- Fix usage of the size-stats target in out of tree build situations. For consistency reasons, we chose to mimic what graph-depends already does, even if size-stats does not necessarily need to be executed from within
CONFIG_DIR.- Add one patch to make sure that 'make clean' removes the graphs/ subdirectory in the output directory, which wasn't done today. This issue isn't specific to size-stats, and already existed with graph-build and graph-depends.
Comments:
- J?r?me Puiller suggested to add a warning for removed files. However, many files are removed in the target-finalize step (man pages, etc.) and we don't want to have gazillions of warnings about these completely normal file removals.
Quoted 6 lines
Thomas Petazzoni (5):
Makefile: remove the graphs/ dir on 'make clean'
pkg-generic: addstep_pkg_sizeglobal instrumentation hook support/scripts: add size-stats script
Makefile: implement a size-stats target
Dummy testing packages
I reviewed and tested this series without encountering any particular problems.
However, I noticed that the files coming from the BR2_ROOTFS_OVERLAY produce a warning. It is true that these files are not part of any package, but we can do something like for the rootfs skeleton.
This requires to do something like that:
$(call step_pkg_size_start,rootfs-overlay) @$(foreach d, $(call qstrip,$(BR2_ROOTFS_OVERLAY)), \ $(call MESSAGE,"Copying overlay $(d)"); \ rsync -a --ignore-times $(RSYNC_VCS_EXCLUSIONS) \ --chmod=Du+w --exclude .empty --exclude '*~' \ $(d)/ $(TARGET_DIR)$(sep)) $(call step_pkg_size_end,rootfs-overlay)
Or call step_pkg_size-* for each overlay directories ?
This will allow to show the size contribution for each rootfs overlay.
What do you think ?
Best regards,
Romain