blob: 2f6498ab67430ac716fe8f2c6fb2d66bb9d50d05 [file] [log] [blame]
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001#!/usr/bin/python3
2#
3# Script for comparing buildstats from two different builds
4#
5# Copyright (c) 2016, Intel Corporation.
6#
Brad Bishopc342db32019-05-15 21:57:59 -04007# SPDX-License-Identifier: GPL-2.0-only
Patrick Williamsc0f7c042017-02-23 20:41:17 -06008#
Brad Bishopc342db32019-05-15 21:57:59 -04009
Patrick Williamsc0f7c042017-02-23 20:41:17 -060010import argparse
11import glob
Patrick Williamsc0f7c042017-02-23 20:41:17 -060012import logging
13import math
14import os
Patrick Williamsc0f7c042017-02-23 20:41:17 -060015import sys
Patrick Williamsc0f7c042017-02-23 20:41:17 -060016from operator import attrgetter
17
Brad Bishopd7bf8c12018-02-25 22:55:05 -050018# Import oe libs
19scripts_path = os.path.dirname(os.path.realpath(__file__))
20sys.path.append(os.path.join(scripts_path, 'lib'))
21from buildstats import BuildStats, diff_buildstats, taskdiff_fields, BSVerDiff
22
23
Patrick Williamsc0f7c042017-02-23 20:41:17 -060024# Setup logging
25logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
26log = logging.getLogger()
27
28
29class ScriptError(Exception):
30 """Exception for internal error handling of this script"""
31 pass
32
33
Patrick Williamsc0f7c042017-02-23 20:41:17 -060034def read_buildstats(path, multi):
35 """Read buildstats"""
36 if not os.path.exists(path):
37 raise ScriptError("No such file or directory: {}".format(path))
38
39 if os.path.isfile(path):
Brad Bishopd7bf8c12018-02-25 22:55:05 -050040 return BuildStats.from_file_json(path)
Patrick Williamsc0f7c042017-02-23 20:41:17 -060041
42 if os.path.isfile(os.path.join(path, 'build_stats')):
Brad Bishopd7bf8c12018-02-25 22:55:05 -050043 return BuildStats.from_dir(path)
Patrick Williamsc0f7c042017-02-23 20:41:17 -060044
45 # Handle a non-buildstat directory
46 subpaths = sorted(glob.glob(path + '/*'))
47 if len(subpaths) > 1:
48 if multi:
49 log.info("Averaging over {} buildstats from {}".format(
50 len(subpaths), path))
51 else:
52 raise ScriptError("Multiple buildstats found in '{}'. Please give "
53 "a single buildstat directory of use the --multi "
54 "option".format(path))
55 bs = None
56 for subpath in subpaths:
57 if os.path.isfile(subpath):
Brad Bishopd7bf8c12018-02-25 22:55:05 -050058 _bs = BuildStats.from_file_json(subpath)
Patrick Williamsc0f7c042017-02-23 20:41:17 -060059 else:
Brad Bishopd7bf8c12018-02-25 22:55:05 -050060 _bs = BuildStats.from_dir(subpath)
61 if bs is None:
62 bs = _bs
Patrick Williamsc0f7c042017-02-23 20:41:17 -060063 else:
Brad Bishopd7bf8c12018-02-25 22:55:05 -050064 bs.aggregate(_bs)
Patrick Williamsc0f7c042017-02-23 20:41:17 -060065 if not bs:
66 raise ScriptError("No buildstats found under {}".format(path))
Brad Bishopd7bf8c12018-02-25 22:55:05 -050067
Patrick Williamsc0f7c042017-02-23 20:41:17 -060068 return bs
69
70
71def print_ver_diff(bs1, bs2):
72 """Print package version differences"""
Patrick Williamsc0f7c042017-02-23 20:41:17 -060073
Brad Bishopd7bf8c12018-02-25 22:55:05 -050074 diff = BSVerDiff(bs1, bs2)
Patrick Williamsc0f7c042017-02-23 20:41:17 -060075
Brad Bishopd7bf8c12018-02-25 22:55:05 -050076 maxlen = max([len(r) for r in set(bs1.keys()).union(set(bs2.keys()))])
Patrick Williamsc0f7c042017-02-23 20:41:17 -060077 fmt_str = " {:{maxlen}} ({})"
Patrick Williamsc0f7c042017-02-23 20:41:17 -060078
Brad Bishopd7bf8c12018-02-25 22:55:05 -050079 if diff.new:
80 print("\nNEW RECIPES:")
81 print("------------")
82 for name, val in sorted(diff.new.items()):
83 print(fmt_str.format(name, val.nevr, maxlen=maxlen))
Patrick Williamsc0f7c042017-02-23 20:41:17 -060084
Brad Bishopd7bf8c12018-02-25 22:55:05 -050085 if diff.dropped:
86 print("\nDROPPED RECIPES:")
87 print("----------------")
88 for name, val in sorted(diff.dropped.items()):
89 print(fmt_str.format(name, val.nevr, maxlen=maxlen))
Patrick Williamsc0f7c042017-02-23 20:41:17 -060090
91 fmt_str = " {0:{maxlen}} {1:<20} ({2})"
Brad Bishopd7bf8c12018-02-25 22:55:05 -050092 if diff.rchanged:
Patrick Williamsc0f7c042017-02-23 20:41:17 -060093 print("\nREVISION CHANGED:")
94 print("-----------------")
Brad Bishopd7bf8c12018-02-25 22:55:05 -050095 for name, val in sorted(diff.rchanged.items()):
96 field1 = "{} -> {}".format(val.left.revision, val.right.revision)
97 field2 = "{} -> {}".format(val.left.nevr, val.right.nevr)
98 print(fmt_str.format(name, field1, field2, maxlen=maxlen))
Patrick Williamsc0f7c042017-02-23 20:41:17 -060099
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500100 if diff.vchanged:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600101 print("\nVERSION CHANGED:")
102 print("----------------")
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500103 for name, val in sorted(diff.vchanged.items()):
104 field1 = "{} -> {}".format(val.left.version, val.right.version)
105 field2 = "{} -> {}".format(val.left.nevr, val.right.nevr)
106 print(fmt_str.format(name, field1, field2, maxlen=maxlen))
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600107
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500108 if diff.echanged:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600109 print("\nEPOCH CHANGED:")
110 print("--------------")
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500111 for name, val in sorted(diff.echanged.items()):
112 field1 = "{} -> {}".format(val.left.epoch, val.right.epoch)
113 field2 = "{} -> {}".format(val.left.nevr, val.right.nevr)
114 print(fmt_str.format(name, field1, field2, maxlen=maxlen))
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600115
116
Brad Bishop96ff1982019-08-19 13:50:42 -0400117def print_task_diff(bs1, bs2, val_type, min_val=0, min_absdiff=0, sort_by=('absdiff',), only_tasks=[]):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600118 """Diff task execution times"""
119 def val_to_str(val, human_readable=False):
120 """Convert raw value to printable string"""
121 def hms_time(secs):
122 """Get time in human-readable HH:MM:SS format"""
123 h = int(secs / 3600)
124 m = int((secs % 3600) / 60)
125 s = secs % 60
126 if h == 0:
127 return "{:02d}:{:04.1f}".format(m, s)
128 else:
129 return "{:d}:{:02d}:{:04.1f}".format(h, m, s)
130
131 if 'time' in val_type:
132 if human_readable:
133 return hms_time(val)
134 else:
135 return "{:.1f}s".format(val)
136 elif 'bytes' in val_type and human_readable:
137 prefix = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi']
138 dec = int(math.log(val, 2) / 10)
139 prec = 1 if dec > 0 else 0
140 return "{:.{prec}f}{}B".format(val / (2 ** (10 * dec)),
141 prefix[dec], prec=prec)
142 elif 'ops' in val_type and human_readable:
143 prefix = ['', 'k', 'M', 'G', 'T', 'P']
144 dec = int(math.log(val, 1000))
145 prec = 1 if dec > 0 else 0
146 return "{:.{prec}f}{}ops".format(val / (1000 ** dec),
147 prefix[dec], prec=prec)
148 return str(int(val))
149
150 def sum_vals(buildstats):
151 """Get cumulative sum of all tasks"""
152 total = 0.0
153 for recipe_data in buildstats.values():
Brad Bishop96ff1982019-08-19 13:50:42 -0400154 for name, bs_task in recipe_data.tasks.items():
155 if not only_tasks or name in only_tasks:
156 total += getattr(bs_task, val_type)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600157 return total
158
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600159 if min_val:
160 print("Ignoring tasks less than {} ({})".format(
161 val_to_str(min_val, True), val_to_str(min_val)))
162 if min_absdiff:
163 print("Ignoring differences less than {} ({})".format(
164 val_to_str(min_absdiff, True), val_to_str(min_absdiff)))
165
166 # Prepare the data
Brad Bishop96ff1982019-08-19 13:50:42 -0400167 tasks_diff = diff_buildstats(bs1, bs2, val_type, min_val, min_absdiff, only_tasks)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600168
169 # Sort our list
170 for field in reversed(sort_by):
171 if field.startswith('-'):
172 field = field[1:]
173 reverse = True
174 else:
175 reverse = False
176 tasks_diff = sorted(tasks_diff, key=attrgetter(field), reverse=reverse)
177
178 linedata = [(' ', 'PKG', ' ', 'TASK', 'ABSDIFF', 'RELDIFF',
179 val_type.upper() + '1', val_type.upper() + '2')]
180 field_lens = dict([('len_{}'.format(i), len(f)) for i, f in enumerate(linedata[0])])
181
182 # Prepare fields in string format and measure field lengths
183 for diff in tasks_diff:
184 task_prefix = diff.task_op if diff.pkg_op == ' ' else ' '
185 linedata.append((diff.pkg_op, diff.pkg, task_prefix, diff.task,
186 val_to_str(diff.absdiff),
187 '{:+.1f}%'.format(diff.reldiff),
188 val_to_str(diff.value1),
189 val_to_str(diff.value2)))
190 for i, field in enumerate(linedata[-1]):
191 key = 'len_{}'.format(i)
192 if len(field) > field_lens[key]:
193 field_lens[key] = len(field)
194
195 # Print data
196 print()
197 for fields in linedata:
198 print("{:{len_0}}{:{len_1}} {:{len_2}}{:{len_3}} {:>{len_4}} {:>{len_5}} {:>{len_6}} -> {:{len_7}}".format(
199 *fields, **field_lens))
200
201 # Print summary of the diffs
202 total1 = sum_vals(bs1)
203 total2 = sum_vals(bs2)
204 print("\nCumulative {}:".format(val_type))
205 print (" {} {:+.1f}% {} ({}) -> {} ({})".format(
206 val_to_str(total2 - total1), 100 * (total2-total1) / total1,
207 val_to_str(total1, True), val_to_str(total1),
208 val_to_str(total2, True), val_to_str(total2)))
209
210
211def parse_args(argv):
212 """Parse cmdline arguments"""
213 description="""
214Script for comparing buildstats of two separate builds."""
215 parser = argparse.ArgumentParser(
216 formatter_class=argparse.ArgumentDefaultsHelpFormatter,
217 description=description)
218
219 min_val_defaults = {'cputime': 3.0,
220 'read_bytes': 524288,
221 'write_bytes': 524288,
222 'read_ops': 500,
223 'write_ops': 500,
224 'walltime': 5}
225 min_absdiff_defaults = {'cputime': 1.0,
226 'read_bytes': 131072,
227 'write_bytes': 131072,
228 'read_ops': 50,
229 'write_ops': 50,
230 'walltime': 2}
231
232 parser.add_argument('--debug', '-d', action='store_true',
233 help="Verbose logging")
234 parser.add_argument('--ver-diff', action='store_true',
235 help="Show package version differences and exit")
236 parser.add_argument('--diff-attr', default='cputime',
237 choices=min_val_defaults.keys(),
238 help="Buildstat attribute which to compare")
239 parser.add_argument('--min-val', default=min_val_defaults, type=float,
240 help="Filter out tasks less than MIN_VAL. "
241 "Default depends on --diff-attr.")
242 parser.add_argument('--min-absdiff', default=min_absdiff_defaults, type=float,
243 help="Filter out tasks whose difference is less than "
244 "MIN_ABSDIFF, Default depends on --diff-attr.")
245 parser.add_argument('--sort-by', default='absdiff',
246 help="Comma-separated list of field sort order. "
247 "Prepend the field name with '-' for reversed sort. "
248 "Available fields are: {}".format(', '.join(taskdiff_fields)))
249 parser.add_argument('--multi', action='store_true',
250 help="Read all buildstats from the given paths and "
251 "average over them")
Brad Bishop96ff1982019-08-19 13:50:42 -0400252 parser.add_argument('--only-task', dest='only_tasks', metavar='TASK', action='append', default=[],
253 help="Only include TASK in report. May be specified multiple times")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600254 parser.add_argument('buildstats1', metavar='BUILDSTATS1', help="'Left' buildstat")
255 parser.add_argument('buildstats2', metavar='BUILDSTATS2', help="'Right' buildstat")
256
257 args = parser.parse_args(argv)
258
259 # We do not nedd/want to read all buildstats if we just want to look at the
260 # package versions
261 if args.ver_diff:
262 args.multi = False
263
264 # Handle defaults for the filter arguments
265 if args.min_val is min_val_defaults:
266 args.min_val = min_val_defaults[args.diff_attr]
267 if args.min_absdiff is min_absdiff_defaults:
268 args.min_absdiff = min_absdiff_defaults[args.diff_attr]
269
270 return args
271
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600272def main(argv=None):
273 """Script entry point"""
274 args = parse_args(argv)
275 if args.debug:
276 log.setLevel(logging.DEBUG)
277
278 # Validate sort fields
279 sort_by = []
280 for field in args.sort_by.split(','):
281 if field.lstrip('-') not in taskdiff_fields:
282 log.error("Invalid sort field '%s' (must be one of: %s)" %
283 (field, ', '.join(taskdiff_fields)))
284 sys.exit(1)
285 sort_by.append(field)
286
287 try:
288 bs1 = read_buildstats(args.buildstats1, args.multi)
289 bs2 = read_buildstats(args.buildstats2, args.multi)
290
291 if args.ver_diff:
292 print_ver_diff(bs1, bs2)
293 else:
294 print_task_diff(bs1, bs2, args.diff_attr, args.min_val,
Brad Bishop96ff1982019-08-19 13:50:42 -0400295 args.min_absdiff, sort_by, args.only_tasks)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600296 except ScriptError as err:
297 log.error(str(err))
298 return 1
299 return 0
300
301if __name__ == "__main__":
302 sys.exit(main())