size-stats-compare 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. #!/usr/bin/env python3
  2. # Copyright (C) 2016 Thomas De Schampheleire <thomas.de.schampheleire@gmail.com>
  3. # This program is free software; you can redistribute it and/or modify
  4. # it under the terms of the GNU General Public License as published by
  5. # the Free Software Foundation; either version 2 of the License, or
  6. # (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  11. # General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with this program; if not, write to the Free Software
  15. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  16. # TODO (improvements)
  17. # - support K,M,G size suffixes for threshold
  18. # - output CSV file in addition to stdout reporting
  19. import csv
  20. import argparse
  21. import sys
  22. def read_file_size_csv(inputf, detail=None):
  23. """Extract package or file sizes from CSV file into size dictionary"""
  24. sizes = {}
  25. reader = csv.reader(inputf)
  26. header = next(reader)
  27. if header[0] != 'File name' or header[1] != 'Package name' or \
  28. header[2] != 'File size' or header[3] != 'Package size':
  29. print(("Input file %s does not contain the expected header. Are you "
  30. "sure this file corresponds to the file-size-stats.csv "
  31. "file created by 'make graph-size'?") % inputf.name)
  32. sys.exit(1)
  33. for row in reader:
  34. if detail:
  35. sizes[(row[0], row[1])] = int(row[2])
  36. else:
  37. sizes[(None, row[1])] = int(row[3])
  38. return sizes
  39. def compare_sizes(old, new):
  40. """Return delta/added/removed dictionaries based on two input size
  41. dictionaries"""
  42. delta = {}
  43. oldkeys = set(old.keys())
  44. newkeys = set(new.keys())
  45. # packages/files in both
  46. for entry in newkeys.intersection(oldkeys):
  47. delta[entry] = ('', new[entry] - old[entry])
  48. # packages/files only in new
  49. for entry in newkeys.difference(oldkeys):
  50. delta[entry] = ('added', new[entry])
  51. # packages/files only in old
  52. for entry in oldkeys.difference(newkeys):
  53. delta[entry] = ('removed', -old[entry])
  54. return delta
  55. def print_results(result, threshold):
  56. """Print the given result dictionary sorted by size, ignoring any entries
  57. below or equal to threshold"""
  58. from six import iteritems
  59. list_result = list(iteritems(result))
  60. # result is a dictionary: (filename, pkgname) -> (flag, size difference)
  61. # list_result is a list of tuples: ((filename, pkgname), (flag, size difference))
  62. # filename may be None if no detail is requested.
  63. maxpkgname = max(len(pkgname) for filename, pkgname in result)
  64. for entry in sorted(list_result, key=lambda entry: entry[1][1]):
  65. data = dict(
  66. filename=entry[0][0],
  67. pkgname=entry[0][1],
  68. action=entry[1][0],
  69. size=entry[1][1],
  70. maxpkgname=maxpkgname,
  71. )
  72. if threshold is not None and abs(data['size']) <= threshold:
  73. continue
  74. if data['filename']:
  75. print('{size:12d} {action:7s} {pkgname:{maxpkgname}s} {filename}'.format(**data))
  76. else:
  77. print('{size:12d} {action:7s} {pkgname}'.format(**data))
  78. # main #########################################################################
  79. description = """
  80. Compare rootfs size between Buildroot compilations, for example after changing
  81. configuration options or after switching to another Buildroot release.
  82. This script compares the file-size-stats.csv file generated by 'make graph-size'
  83. with the corresponding file from another Buildroot compilation.
  84. The size differences can be reported per package or per file.
  85. Size differences smaller or equal than a given threshold can be ignored.
  86. """
  87. parser = argparse.ArgumentParser(description=description,
  88. formatter_class=argparse.RawDescriptionHelpFormatter)
  89. parser.add_argument('-d', '--detail', action='store_true',
  90. help='''report differences for individual files rather than
  91. packages''')
  92. parser.add_argument('-t', '--threshold', type=int,
  93. help='''ignore size differences smaller or equal than this
  94. value (bytes)''')
  95. parser.add_argument('old_file_size_csv', type=argparse.FileType('r'),
  96. metavar='old-file-size-stats.csv',
  97. help="""old CSV file with file and package size statistics,
  98. generated by 'make graph-size'""")
  99. parser.add_argument('new_file_size_csv', type=argparse.FileType('r'),
  100. metavar='new-file-size-stats.csv',
  101. help='new CSV file with file and package size statistics')
  102. args = parser.parse_args()
  103. if args.detail:
  104. keyword = 'file'
  105. else:
  106. keyword = 'package'
  107. old_sizes = read_file_size_csv(args.old_file_size_csv, args.detail)
  108. new_sizes = read_file_size_csv(args.new_file_size_csv, args.detail)
  109. delta = compare_sizes(old_sizes, new_sizes)
  110. print('Size difference per %s (bytes), threshold = %s' % (keyword, args.threshold))
  111. print(80*'-')
  112. print_results(delta, args.threshold)
  113. print(80*'-')
  114. print_results({(None, 'TOTAL'): ('', sum(new_sizes.values()) - sum(old_sizes.values()))},
  115. threshold=None)