| #!/usr/bin/python3 | |
| # | |
| # Script for comparing buildstats from two different builds | |
| # | |
| # Copyright (c) 2016, Intel Corporation. | |
| # | |
| # SPDX-License-Identifier: GPL-2.0-only | |
| # | |
| import argparse | |
| import glob | |
| import logging | |
| import math | |
| import os | |
| import sys | |
| from operator import attrgetter | |
| # Import oe libs | |
| scripts_path = os.path.dirname(os.path.realpath(__file__)) | |
| sys.path.append(os.path.join(scripts_path, 'lib')) | |
| from buildstats import BuildStats, diff_buildstats, taskdiff_fields, BSVerDiff | |
| # Setup logging | |
| logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") | |
| log = logging.getLogger() | |
| class ScriptError(Exception): | |
| """Exception for internal error handling of this script""" | |
| pass | |
| def read_buildstats(path, multi): | |
| """Read buildstats""" | |
| if not os.path.exists(path): | |
| raise ScriptError("No such file or directory: {}".format(path)) | |
| if os.path.isfile(path): | |
| return BuildStats.from_file_json(path) | |
| if os.path.isfile(os.path.join(path, 'build_stats')): | |
| return BuildStats.from_dir(path) | |
| # Handle a non-buildstat directory | |
| subpaths = sorted(glob.glob(path + '/*')) | |
| if len(subpaths) > 1: | |
| if multi: | |
| log.info("Averaging over {} buildstats from {}".format( | |
| len(subpaths), path)) | |
| else: | |
| raise ScriptError("Multiple buildstats found in '{}'. Please give " | |
| "a single buildstat directory of use the --multi " | |
| "option".format(path)) | |
| bs = None | |
| for subpath in subpaths: | |
| if os.path.isfile(subpath): | |
| _bs = BuildStats.from_file_json(subpath) | |
| else: | |
| _bs = BuildStats.from_dir(subpath) | |
| if bs is None: | |
| bs = _bs | |
| else: | |
| bs.aggregate(_bs) | |
| if not bs: | |
| raise ScriptError("No buildstats found under {}".format(path)) | |
| return bs | |
| def print_ver_diff(bs1, bs2): | |
| """Print package version differences""" | |
| diff = BSVerDiff(bs1, bs2) | |
| maxlen = max([len(r) for r in set(bs1.keys()).union(set(bs2.keys()))]) | |
| fmt_str = " {:{maxlen}} ({})" | |
| if diff.new: | |
| print("\nNEW RECIPES:") | |
| print("------------") | |
| for name, val in sorted(diff.new.items()): | |
| print(fmt_str.format(name, val.nevr, maxlen=maxlen)) | |
| if diff.dropped: | |
| print("\nDROPPED RECIPES:") | |
| print("----------------") | |
| for name, val in sorted(diff.dropped.items()): | |
| print(fmt_str.format(name, val.nevr, maxlen=maxlen)) | |
| fmt_str = " {0:{maxlen}} {1:<20} ({2})" | |
| if diff.rchanged: | |
| print("\nREVISION CHANGED:") | |
| print("-----------------") | |
| for name, val in sorted(diff.rchanged.items()): | |
| field1 = "{} -> {}".format(val.left.revision, val.right.revision) | |
| field2 = "{} -> {}".format(val.left.nevr, val.right.nevr) | |
| print(fmt_str.format(name, field1, field2, maxlen=maxlen)) | |
| if diff.vchanged: | |
| print("\nVERSION CHANGED:") | |
| print("----------------") | |
| for name, val in sorted(diff.vchanged.items()): | |
| field1 = "{} -> {}".format(val.left.version, val.right.version) | |
| field2 = "{} -> {}".format(val.left.nevr, val.right.nevr) | |
| print(fmt_str.format(name, field1, field2, maxlen=maxlen)) | |
| if diff.echanged: | |
| print("\nEPOCH CHANGED:") | |
| print("--------------") | |
| for name, val in sorted(diff.echanged.items()): | |
| field1 = "{} -> {}".format(val.left.epoch, val.right.epoch) | |
| field2 = "{} -> {}".format(val.left.nevr, val.right.nevr) | |
| print(fmt_str.format(name, field1, field2, maxlen=maxlen)) | |
| def print_task_diff(bs1, bs2, val_type, min_val=0, min_absdiff=0, sort_by=('absdiff',), only_tasks=[]): | |
| """Diff task execution times""" | |
| def val_to_str(val, human_readable=False): | |
| """Convert raw value to printable string""" | |
| def hms_time(secs): | |
| """Get time in human-readable HH:MM:SS format""" | |
| h = int(secs / 3600) | |
| m = int((secs % 3600) / 60) | |
| s = secs % 60 | |
| if h == 0: | |
| return "{:02d}:{:04.1f}".format(m, s) | |
| else: | |
| return "{:d}:{:02d}:{:04.1f}".format(h, m, s) | |
| if 'time' in val_type: | |
| if human_readable: | |
| return hms_time(val) | |
| else: | |
| return "{:.1f}s".format(val) | |
| elif 'bytes' in val_type and human_readable: | |
| prefix = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi'] | |
| dec = int(math.log(val, 2) / 10) | |
| prec = 1 if dec > 0 else 0 | |
| return "{:.{prec}f}{}B".format(val / (2 ** (10 * dec)), | |
| prefix[dec], prec=prec) | |
| elif 'ops' in val_type and human_readable: | |
| prefix = ['', 'k', 'M', 'G', 'T', 'P'] | |
| dec = int(math.log(val, 1000)) | |
| prec = 1 if dec > 0 else 0 | |
| return "{:.{prec}f}{}ops".format(val / (1000 ** dec), | |
| prefix[dec], prec=prec) | |
| return str(int(val)) | |
| def sum_vals(buildstats): | |
| """Get cumulative sum of all tasks""" | |
| total = 0.0 | |
| for recipe_data in buildstats.values(): | |
| for name, bs_task in recipe_data.tasks.items(): | |
| if not only_tasks or name in only_tasks: | |
| total += getattr(bs_task, val_type) | |
| return total | |
| if min_val: | |
| print("Ignoring tasks less than {} ({})".format( | |
| val_to_str(min_val, True), val_to_str(min_val))) | |
| if min_absdiff: | |
| print("Ignoring differences less than {} ({})".format( | |
| val_to_str(min_absdiff, True), val_to_str(min_absdiff))) | |
| # Prepare the data | |
| tasks_diff = diff_buildstats(bs1, bs2, val_type, min_val, min_absdiff, only_tasks) | |
| # Sort our list | |
| for field in reversed(sort_by): | |
| if field.startswith('-'): | |
| field = field[1:] | |
| reverse = True | |
| else: | |
| reverse = False | |
| tasks_diff = sorted(tasks_diff, key=attrgetter(field), reverse=reverse) | |
| linedata = [(' ', 'PKG', ' ', 'TASK', 'ABSDIFF', 'RELDIFF', | |
| val_type.upper() + '1', val_type.upper() + '2')] | |
| field_lens = dict([('len_{}'.format(i), len(f)) for i, f in enumerate(linedata[0])]) | |
| # Prepare fields in string format and measure field lengths | |
| for diff in tasks_diff: | |
| task_prefix = diff.task_op if diff.pkg_op == ' ' else ' ' | |
| linedata.append((diff.pkg_op, diff.pkg, task_prefix, diff.task, | |
| val_to_str(diff.absdiff), | |
| '{:+.1f}%'.format(diff.reldiff), | |
| val_to_str(diff.value1), | |
| val_to_str(diff.value2))) | |
| for i, field in enumerate(linedata[-1]): | |
| key = 'len_{}'.format(i) | |
| if len(field) > field_lens[key]: | |
| field_lens[key] = len(field) | |
| # Print data | |
| print() | |
| for fields in linedata: | |
| print("{:{len_0}}{:{len_1}} {:{len_2}}{:{len_3}} {:>{len_4}} {:>{len_5}} {:>{len_6}} -> {:{len_7}}".format( | |
| *fields, **field_lens)) | |
| # Print summary of the diffs | |
| total1 = sum_vals(bs1) | |
| total2 = sum_vals(bs2) | |
| print("\nCumulative {}:".format(val_type)) | |
| print (" {} {:+.1f}% {} ({}) -> {} ({})".format( | |
| val_to_str(total2 - total1), 100 * (total2-total1) / total1, | |
| val_to_str(total1, True), val_to_str(total1), | |
| val_to_str(total2, True), val_to_str(total2))) | |
| def parse_args(argv): | |
| """Parse cmdline arguments""" | |
| description=""" | |
| Script for comparing buildstats of two separate builds.""" | |
| parser = argparse.ArgumentParser( | |
| formatter_class=argparse.ArgumentDefaultsHelpFormatter, | |
| description=description) | |
| min_val_defaults = {'cputime': 3.0, | |
| 'read_bytes': 524288, | |
| 'write_bytes': 524288, | |
| 'read_ops': 500, | |
| 'write_ops': 500, | |
| 'walltime': 5} | |
| min_absdiff_defaults = {'cputime': 1.0, | |
| 'read_bytes': 131072, | |
| 'write_bytes': 131072, | |
| 'read_ops': 50, | |
| 'write_ops': 50, | |
| 'walltime': 2} | |
| parser.add_argument('--debug', '-d', action='store_true', | |
| help="Verbose logging") | |
| parser.add_argument('--ver-diff', action='store_true', | |
| help="Show package version differences and exit") | |
| parser.add_argument('--diff-attr', default='cputime', | |
| choices=min_val_defaults.keys(), | |
| help="Buildstat attribute which to compare") | |
| parser.add_argument('--min-val', default=min_val_defaults, type=float, | |
| help="Filter out tasks less than MIN_VAL. " | |
| "Default depends on --diff-attr.") | |
| parser.add_argument('--min-absdiff', default=min_absdiff_defaults, type=float, | |
| help="Filter out tasks whose difference is less than " | |
| "MIN_ABSDIFF, Default depends on --diff-attr.") | |
| parser.add_argument('--sort-by', default='absdiff', | |
| help="Comma-separated list of field sort order. " | |
| "Prepend the field name with '-' for reversed sort. " | |
| "Available fields are: {}".format(', '.join(taskdiff_fields))) | |
| parser.add_argument('--multi', action='store_true', | |
| help="Read all buildstats from the given paths and " | |
| "average over them") | |
| parser.add_argument('--only-task', dest='only_tasks', metavar='TASK', action='append', default=[], | |
| help="Only include TASK in report. May be specified multiple times") | |
| parser.add_argument('buildstats1', metavar='BUILDSTATS1', help="'Left' buildstat") | |
| parser.add_argument('buildstats2', metavar='BUILDSTATS2', help="'Right' buildstat") | |
| args = parser.parse_args(argv) | |
| # We do not nedd/want to read all buildstats if we just want to look at the | |
| # package versions | |
| if args.ver_diff: | |
| args.multi = False | |
| # Handle defaults for the filter arguments | |
| if args.min_val is min_val_defaults: | |
| args.min_val = min_val_defaults[args.diff_attr] | |
| if args.min_absdiff is min_absdiff_defaults: | |
| args.min_absdiff = min_absdiff_defaults[args.diff_attr] | |
| return args | |
| def main(argv=None): | |
| """Script entry point""" | |
| args = parse_args(argv) | |
| if args.debug: | |
| log.setLevel(logging.DEBUG) | |
| # Validate sort fields | |
| sort_by = [] | |
| for field in args.sort_by.split(','): | |
| if field.lstrip('-') not in taskdiff_fields: | |
| log.error("Invalid sort field '%s' (must be one of: %s)" % | |
| (field, ', '.join(taskdiff_fields))) | |
| sys.exit(1) | |
| sort_by.append(field) | |
| try: | |
| bs1 = read_buildstats(args.buildstats1, args.multi) | |
| bs2 = read_buildstats(args.buildstats2, args.multi) | |
| if args.ver_diff: | |
| print_ver_diff(bs1, bs2) | |
| else: | |
| print_task_diff(bs1, bs2, args.diff_attr, args.min_val, | |
| args.min_absdiff, sort_by, args.only_tasks) | |
| except ScriptError as err: | |
| log.error(str(err)) | |
| return 1 | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |