#!/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()) 
 |