xref: /OK3568_Linux_fs/yocto/poky/scripts/buildstats-diff (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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