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