xref: /OK3568_Linux_fs/yocto/poky/scripts/oe-build-perf-report (revision 4882a59341e53eb6f0b4789bf948001014eff981)
1*4882a593Smuzhiyun#!/usr/bin/env python3
2*4882a593Smuzhiyun#
3*4882a593Smuzhiyun# Examine build performance test results
4*4882a593Smuzhiyun#
5*4882a593Smuzhiyun# Copyright (c) 2017, Intel Corporation.
6*4882a593Smuzhiyun#
7*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only
8*4882a593Smuzhiyun#
9*4882a593Smuzhiyun
10*4882a593Smuzhiyunimport argparse
11*4882a593Smuzhiyunimport json
12*4882a593Smuzhiyunimport logging
13*4882a593Smuzhiyunimport os
14*4882a593Smuzhiyunimport re
15*4882a593Smuzhiyunimport sys
16*4882a593Smuzhiyunfrom collections import namedtuple, OrderedDict
17*4882a593Smuzhiyunfrom operator import attrgetter
18*4882a593Smuzhiyunfrom xml.etree import ElementTree as ET
19*4882a593Smuzhiyun
20*4882a593Smuzhiyun# Import oe libs
21*4882a593Smuzhiyunscripts_path = os.path.dirname(os.path.realpath(__file__))
22*4882a593Smuzhiyunsys.path.append(os.path.join(scripts_path, 'lib'))
23*4882a593Smuzhiyunimport scriptpath
24*4882a593Smuzhiyunfrom build_perf import print_table
25*4882a593Smuzhiyunfrom build_perf.report import (metadata_xml_to_json, results_xml_to_json,
26*4882a593Smuzhiyun                               aggregate_data, aggregate_metadata, measurement_stats,
27*4882a593Smuzhiyun                               AggregateTestData)
28*4882a593Smuzhiyunfrom build_perf import html
29*4882a593Smuzhiyunfrom buildstats import BuildStats, diff_buildstats, BSVerDiff
30*4882a593Smuzhiyun
31*4882a593Smuzhiyunscriptpath.add_oe_lib_path()
32*4882a593Smuzhiyun
33*4882a593Smuzhiyunfrom oeqa.utils.git import GitRepo, GitError
34*4882a593Smuzhiyunimport oeqa.utils.gitarchive as gitarchive
35*4882a593Smuzhiyun
36*4882a593Smuzhiyun
37*4882a593Smuzhiyun# Setup logging
38*4882a593Smuzhiyunlogging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
39*4882a593Smuzhiyunlog = logging.getLogger('oe-build-perf-report')
40*4882a593Smuzhiyun
41*4882a593Smuzhiyundef list_test_revs(repo, tag_name, verbosity, **kwargs):
42*4882a593Smuzhiyun    """Get list of all tested revisions"""
43*4882a593Smuzhiyun    valid_kwargs = dict([(k, v) for k, v in kwargs.items() if v is not None])
44*4882a593Smuzhiyun
45*4882a593Smuzhiyun    fields, revs = gitarchive.get_test_runs(log, repo, tag_name, **valid_kwargs)
46*4882a593Smuzhiyun    ignore_fields = ['tag_number']
47*4882a593Smuzhiyun    if verbosity < 2:
48*4882a593Smuzhiyun        extra_fields = ['COMMITS', 'TEST RUNS']
49*4882a593Smuzhiyun        ignore_fields.extend(['commit_number', 'commit'])
50*4882a593Smuzhiyun    else:
51*4882a593Smuzhiyun        extra_fields = ['TEST RUNS']
52*4882a593Smuzhiyun
53*4882a593Smuzhiyun    print_fields = [i for i, f in enumerate(fields) if f not in ignore_fields]
54*4882a593Smuzhiyun
55*4882a593Smuzhiyun    # Sort revs
56*4882a593Smuzhiyun    rows = [[fields[i].upper() for i in print_fields] + extra_fields]
57*4882a593Smuzhiyun
58*4882a593Smuzhiyun    prev = [''] * len(print_fields)
59*4882a593Smuzhiyun    prev_commit = None
60*4882a593Smuzhiyun    commit_cnt = 0
61*4882a593Smuzhiyun    commit_field = fields.index('commit')
62*4882a593Smuzhiyun    for rev in revs:
63*4882a593Smuzhiyun        # Only use fields that we want to print
64*4882a593Smuzhiyun        cols = [rev[i] for i in print_fields]
65*4882a593Smuzhiyun
66*4882a593Smuzhiyun
67*4882a593Smuzhiyun        if cols != prev:
68*4882a593Smuzhiyun            commit_cnt = 1
69*4882a593Smuzhiyun            test_run_cnt = 1
70*4882a593Smuzhiyun            new_row = [''] * (len(print_fields) + len(extra_fields))
71*4882a593Smuzhiyun
72*4882a593Smuzhiyun            for i in print_fields:
73*4882a593Smuzhiyun                if cols[i] != prev[i]:
74*4882a593Smuzhiyun                    break
75*4882a593Smuzhiyun            new_row[i:-len(extra_fields)] = cols[i:]
76*4882a593Smuzhiyun            rows.append(new_row)
77*4882a593Smuzhiyun        else:
78*4882a593Smuzhiyun            if rev[commit_field] != prev_commit:
79*4882a593Smuzhiyun                commit_cnt += 1
80*4882a593Smuzhiyun            test_run_cnt += 1
81*4882a593Smuzhiyun
82*4882a593Smuzhiyun        if verbosity < 2:
83*4882a593Smuzhiyun            new_row[-2] = commit_cnt
84*4882a593Smuzhiyun        new_row[-1] = test_run_cnt
85*4882a593Smuzhiyun        prev = cols
86*4882a593Smuzhiyun        prev_commit = rev[commit_field]
87*4882a593Smuzhiyun
88*4882a593Smuzhiyun    print_table(rows)
89*4882a593Smuzhiyun
90*4882a593Smuzhiyundef is_xml_format(repo, commit):
91*4882a593Smuzhiyun    """Check if the commit contains xml (or json) data"""
92*4882a593Smuzhiyun    if repo.rev_parse(commit + ':results.xml'):
93*4882a593Smuzhiyun        log.debug("Detected report in xml format in %s", commit)
94*4882a593Smuzhiyun        return True
95*4882a593Smuzhiyun    else:
96*4882a593Smuzhiyun        log.debug("No xml report in %s, assuming json formatted results", commit)
97*4882a593Smuzhiyun        return False
98*4882a593Smuzhiyun
99*4882a593Smuzhiyundef read_results(repo, tags, xml=True):
100*4882a593Smuzhiyun    """Read result files from repo"""
101*4882a593Smuzhiyun
102*4882a593Smuzhiyun    def parse_xml_stream(data):
103*4882a593Smuzhiyun        """Parse multiple concatenated XML objects"""
104*4882a593Smuzhiyun        objs = []
105*4882a593Smuzhiyun        xml_d = ""
106*4882a593Smuzhiyun        for line in data.splitlines():
107*4882a593Smuzhiyun            if xml_d and line.startswith('<?xml version='):
108*4882a593Smuzhiyun                objs.append(ET.fromstring(xml_d))
109*4882a593Smuzhiyun                xml_d = line
110*4882a593Smuzhiyun            else:
111*4882a593Smuzhiyun                xml_d += line
112*4882a593Smuzhiyun        objs.append(ET.fromstring(xml_d))
113*4882a593Smuzhiyun        return objs
114*4882a593Smuzhiyun
115*4882a593Smuzhiyun    def parse_json_stream(data):
116*4882a593Smuzhiyun        """Parse multiple concatenated JSON objects"""
117*4882a593Smuzhiyun        objs = []
118*4882a593Smuzhiyun        json_d = ""
119*4882a593Smuzhiyun        for line in data.splitlines():
120*4882a593Smuzhiyun            if line == '}{':
121*4882a593Smuzhiyun                json_d += '}'
122*4882a593Smuzhiyun                objs.append(json.loads(json_d, object_pairs_hook=OrderedDict))
123*4882a593Smuzhiyun                json_d = '{'
124*4882a593Smuzhiyun            else:
125*4882a593Smuzhiyun                json_d += line
126*4882a593Smuzhiyun        objs.append(json.loads(json_d, object_pairs_hook=OrderedDict))
127*4882a593Smuzhiyun        return objs
128*4882a593Smuzhiyun
129*4882a593Smuzhiyun    num_revs = len(tags)
130*4882a593Smuzhiyun
131*4882a593Smuzhiyun    # Optimize by reading all data with one git command
132*4882a593Smuzhiyun    log.debug("Loading raw result data from %d tags, %s...", num_revs, tags[0])
133*4882a593Smuzhiyun    if xml:
134*4882a593Smuzhiyun        git_objs = [tag + ':metadata.xml' for tag in tags] + [tag + ':results.xml' for tag in tags]
135*4882a593Smuzhiyun        data = parse_xml_stream(repo.run_cmd(['show'] + git_objs + ['--']))
136*4882a593Smuzhiyun        return ([metadata_xml_to_json(e) for e in data[0:num_revs]],
137*4882a593Smuzhiyun                [results_xml_to_json(e) for e in data[num_revs:]])
138*4882a593Smuzhiyun    else:
139*4882a593Smuzhiyun        git_objs = [tag + ':metadata.json' for tag in tags] + [tag + ':results.json' for tag in tags]
140*4882a593Smuzhiyun        data = parse_json_stream(repo.run_cmd(['show'] + git_objs + ['--']))
141*4882a593Smuzhiyun        return data[0:num_revs], data[num_revs:]
142*4882a593Smuzhiyun
143*4882a593Smuzhiyun
144*4882a593Smuzhiyundef get_data_item(data, key):
145*4882a593Smuzhiyun    """Nested getitem lookup"""
146*4882a593Smuzhiyun    for k in key.split('.'):
147*4882a593Smuzhiyun        data = data[k]
148*4882a593Smuzhiyun    return data
149*4882a593Smuzhiyun
150*4882a593Smuzhiyun
151*4882a593Smuzhiyundef metadata_diff(metadata_l, metadata_r):
152*4882a593Smuzhiyun    """Prepare a metadata diff for printing"""
153*4882a593Smuzhiyun    keys = [('Hostname', 'hostname', 'hostname'),
154*4882a593Smuzhiyun            ('Branch', 'branch', 'layers.meta.branch'),
155*4882a593Smuzhiyun            ('Commit number', 'commit_num', 'layers.meta.commit_count'),
156*4882a593Smuzhiyun            ('Commit', 'commit', 'layers.meta.commit'),
157*4882a593Smuzhiyun            ('Number of test runs', 'testrun_count', 'testrun_count')
158*4882a593Smuzhiyun           ]
159*4882a593Smuzhiyun
160*4882a593Smuzhiyun    def _metadata_diff(key):
161*4882a593Smuzhiyun        """Diff metadata from two test reports"""
162*4882a593Smuzhiyun        try:
163*4882a593Smuzhiyun            val1 = get_data_item(metadata_l, key)
164*4882a593Smuzhiyun        except KeyError:
165*4882a593Smuzhiyun            val1 = '(N/A)'
166*4882a593Smuzhiyun        try:
167*4882a593Smuzhiyun            val2 = get_data_item(metadata_r, key)
168*4882a593Smuzhiyun        except KeyError:
169*4882a593Smuzhiyun            val2 = '(N/A)'
170*4882a593Smuzhiyun        return val1, val2
171*4882a593Smuzhiyun
172*4882a593Smuzhiyun    metadata = OrderedDict()
173*4882a593Smuzhiyun    for title, key, key_json in keys:
174*4882a593Smuzhiyun        value_l, value_r = _metadata_diff(key_json)
175*4882a593Smuzhiyun        metadata[key] = {'title': title,
176*4882a593Smuzhiyun                         'value_old': value_l,
177*4882a593Smuzhiyun                         'value': value_r}
178*4882a593Smuzhiyun    return metadata
179*4882a593Smuzhiyun
180*4882a593Smuzhiyun
181*4882a593Smuzhiyundef print_diff_report(metadata_l, data_l, metadata_r, data_r):
182*4882a593Smuzhiyun    """Print differences between two data sets"""
183*4882a593Smuzhiyun
184*4882a593Smuzhiyun    # First, print general metadata
185*4882a593Smuzhiyun    print("\nTEST METADATA:\n==============")
186*4882a593Smuzhiyun    meta_diff = metadata_diff(metadata_l, metadata_r)
187*4882a593Smuzhiyun    rows = []
188*4882a593Smuzhiyun    row_fmt = ['{:{wid}} ', '{:<{wid}}   ', '{:<{wid}}']
189*4882a593Smuzhiyun    rows = [['', 'CURRENT COMMIT', 'COMPARING WITH']]
190*4882a593Smuzhiyun    for key, val in meta_diff.items():
191*4882a593Smuzhiyun        # Shorten commit hashes
192*4882a593Smuzhiyun        if key == 'commit':
193*4882a593Smuzhiyun            rows.append([val['title'] + ':', val['value'][:20], val['value_old'][:20]])
194*4882a593Smuzhiyun        else:
195*4882a593Smuzhiyun            rows.append([val['title'] + ':', val['value'], val['value_old']])
196*4882a593Smuzhiyun    print_table(rows, row_fmt)
197*4882a593Smuzhiyun
198*4882a593Smuzhiyun
199*4882a593Smuzhiyun    # Print test results
200*4882a593Smuzhiyun    print("\nTEST RESULTS:\n=============")
201*4882a593Smuzhiyun
202*4882a593Smuzhiyun    tests = list(data_l['tests'].keys())
203*4882a593Smuzhiyun    # Append tests that are only present in 'right' set
204*4882a593Smuzhiyun    tests += [t for t in list(data_r['tests'].keys()) if t not in tests]
205*4882a593Smuzhiyun
206*4882a593Smuzhiyun    # Prepare data to be printed
207*4882a593Smuzhiyun    rows = []
208*4882a593Smuzhiyun    row_fmt = ['{:8}', '{:{wid}}', '{:{wid}}', '  {:>{wid}}', ' {:{wid}} ', '{:{wid}}',
209*4882a593Smuzhiyun               '  {:>{wid}}', '  {:>{wid}}']
210*4882a593Smuzhiyun    num_cols = len(row_fmt)
211*4882a593Smuzhiyun    for test in tests:
212*4882a593Smuzhiyun        test_l = data_l['tests'][test] if test in data_l['tests'] else None
213*4882a593Smuzhiyun        test_r = data_r['tests'][test] if test in data_r['tests'] else None
214*4882a593Smuzhiyun        pref = ' '
215*4882a593Smuzhiyun        if test_l is None:
216*4882a593Smuzhiyun            pref = '+'
217*4882a593Smuzhiyun        elif test_r is None:
218*4882a593Smuzhiyun            pref = '-'
219*4882a593Smuzhiyun        descr = test_l['description'] if test_l else test_r['description']
220*4882a593Smuzhiyun        heading = "{} {}: {}".format(pref, test, descr)
221*4882a593Smuzhiyun
222*4882a593Smuzhiyun        rows.append([heading])
223*4882a593Smuzhiyun
224*4882a593Smuzhiyun        # Generate the list of measurements
225*4882a593Smuzhiyun        meas_l = test_l['measurements'] if test_l else {}
226*4882a593Smuzhiyun        meas_r = test_r['measurements'] if test_r else {}
227*4882a593Smuzhiyun        measurements = list(meas_l.keys())
228*4882a593Smuzhiyun        measurements += [m for m in list(meas_r.keys()) if m not in measurements]
229*4882a593Smuzhiyun
230*4882a593Smuzhiyun        for meas in measurements:
231*4882a593Smuzhiyun            m_pref = ' '
232*4882a593Smuzhiyun            if meas in meas_l:
233*4882a593Smuzhiyun                stats_l = measurement_stats(meas_l[meas], 'l.')
234*4882a593Smuzhiyun            else:
235*4882a593Smuzhiyun                stats_l = measurement_stats(None, 'l.')
236*4882a593Smuzhiyun                m_pref = '+'
237*4882a593Smuzhiyun            if meas in meas_r:
238*4882a593Smuzhiyun                stats_r = measurement_stats(meas_r[meas], 'r.')
239*4882a593Smuzhiyun            else:
240*4882a593Smuzhiyun                stats_r = measurement_stats(None, 'r.')
241*4882a593Smuzhiyun                m_pref = '-'
242*4882a593Smuzhiyun            stats = stats_l.copy()
243*4882a593Smuzhiyun            stats.update(stats_r)
244*4882a593Smuzhiyun
245*4882a593Smuzhiyun            absdiff = stats['val_cls'](stats['r.mean'] - stats['l.mean'])
246*4882a593Smuzhiyun            reldiff = "{:+.1f} %".format(absdiff * 100 / stats['l.mean'])
247*4882a593Smuzhiyun            if stats['r.mean'] > stats['l.mean']:
248*4882a593Smuzhiyun                absdiff = '+' + str(absdiff)
249*4882a593Smuzhiyun            else:
250*4882a593Smuzhiyun                absdiff = str(absdiff)
251*4882a593Smuzhiyun            rows.append(['', m_pref, stats['name'] + ' ' + stats['quantity'],
252*4882a593Smuzhiyun                         str(stats['l.mean']), '->', str(stats['r.mean']),
253*4882a593Smuzhiyun                         absdiff, reldiff])
254*4882a593Smuzhiyun        rows.append([''] * num_cols)
255*4882a593Smuzhiyun
256*4882a593Smuzhiyun    print_table(rows, row_fmt)
257*4882a593Smuzhiyun
258*4882a593Smuzhiyun    print()
259*4882a593Smuzhiyun
260*4882a593Smuzhiyun
261*4882a593Smuzhiyunclass BSSummary(object):
262*4882a593Smuzhiyun    def __init__(self, bs1, bs2):
263*4882a593Smuzhiyun        self.tasks = {'count': bs2.num_tasks,
264*4882a593Smuzhiyun                      'change': '{:+d}'.format(bs2.num_tasks - bs1.num_tasks)}
265*4882a593Smuzhiyun        self.top_consumer = None
266*4882a593Smuzhiyun        self.top_decrease = None
267*4882a593Smuzhiyun        self.top_increase = None
268*4882a593Smuzhiyun        self.ver_diff = OrderedDict()
269*4882a593Smuzhiyun
270*4882a593Smuzhiyun        tasks_diff = diff_buildstats(bs1, bs2, 'cputime')
271*4882a593Smuzhiyun
272*4882a593Smuzhiyun        # Get top consumers of resources
273*4882a593Smuzhiyun        tasks_diff = sorted(tasks_diff, key=attrgetter('value2'))
274*4882a593Smuzhiyun        self.top_consumer = tasks_diff[-5:]
275*4882a593Smuzhiyun
276*4882a593Smuzhiyun        # Get biggest increase and decrease in resource usage
277*4882a593Smuzhiyun        tasks_diff = sorted(tasks_diff, key=attrgetter('absdiff'))
278*4882a593Smuzhiyun        self.top_decrease = tasks_diff[0:5]
279*4882a593Smuzhiyun        self.top_increase = tasks_diff[-5:]
280*4882a593Smuzhiyun
281*4882a593Smuzhiyun        # Compare recipe versions and prepare data for display
282*4882a593Smuzhiyun        ver_diff = BSVerDiff(bs1, bs2)
283*4882a593Smuzhiyun        if ver_diff:
284*4882a593Smuzhiyun            if ver_diff.new:
285*4882a593Smuzhiyun                self.ver_diff['New recipes'] = [(n, r.evr) for n, r in ver_diff.new.items()]
286*4882a593Smuzhiyun            if ver_diff.dropped:
287*4882a593Smuzhiyun                self.ver_diff['Dropped recipes'] = [(n, r.evr) for n, r in ver_diff.dropped.items()]
288*4882a593Smuzhiyun            if ver_diff.echanged:
289*4882a593Smuzhiyun                self.ver_diff['Epoch changed'] = [(n, "{} &rarr; {}".format(r.left.evr, r.right.evr)) for n, r in ver_diff.echanged.items()]
290*4882a593Smuzhiyun            if ver_diff.vchanged:
291*4882a593Smuzhiyun                self.ver_diff['Version changed'] = [(n, "{} &rarr; {}".format(r.left.version, r.right.version)) for n, r in ver_diff.vchanged.items()]
292*4882a593Smuzhiyun            if ver_diff.rchanged:
293*4882a593Smuzhiyun                self.ver_diff['Revision changed'] = [(n, "{} &rarr; {}".format(r.left.evr, r.right.evr)) for n, r in ver_diff.rchanged.items()]
294*4882a593Smuzhiyun
295*4882a593Smuzhiyun
296*4882a593Smuzhiyundef print_html_report(data, id_comp, buildstats):
297*4882a593Smuzhiyun    """Print report in html format"""
298*4882a593Smuzhiyun    # Handle metadata
299*4882a593Smuzhiyun    metadata = metadata_diff(data[id_comp].metadata, data[-1].metadata)
300*4882a593Smuzhiyun
301*4882a593Smuzhiyun    # Generate list of tests
302*4882a593Smuzhiyun    tests = []
303*4882a593Smuzhiyun    for test in data[-1].results['tests'].keys():
304*4882a593Smuzhiyun        test_r = data[-1].results['tests'][test]
305*4882a593Smuzhiyun        new_test = {'name': test_r['name'],
306*4882a593Smuzhiyun                    'description': test_r['description'],
307*4882a593Smuzhiyun                    'status': test_r['status'],
308*4882a593Smuzhiyun                    'measurements': [],
309*4882a593Smuzhiyun                    'err_type': test_r.get('err_type'),
310*4882a593Smuzhiyun                   }
311*4882a593Smuzhiyun        # Limit length of err output shown
312*4882a593Smuzhiyun        if 'message' in test_r:
313*4882a593Smuzhiyun            lines = test_r['message'].splitlines()
314*4882a593Smuzhiyun            if len(lines) > 20:
315*4882a593Smuzhiyun                new_test['message'] = '...\n' + '\n'.join(lines[-20:])
316*4882a593Smuzhiyun            else:
317*4882a593Smuzhiyun                new_test['message'] = test_r['message']
318*4882a593Smuzhiyun
319*4882a593Smuzhiyun
320*4882a593Smuzhiyun        # Generate the list of measurements
321*4882a593Smuzhiyun        for meas in test_r['measurements'].keys():
322*4882a593Smuzhiyun            meas_r = test_r['measurements'][meas]
323*4882a593Smuzhiyun            meas_type = 'time' if meas_r['type'] == 'sysres' else 'size'
324*4882a593Smuzhiyun            new_meas = {'name': meas_r['name'],
325*4882a593Smuzhiyun                        'legend': meas_r['legend'],
326*4882a593Smuzhiyun                        'description': meas_r['name'] + ' ' + meas_type,
327*4882a593Smuzhiyun                       }
328*4882a593Smuzhiyun            samples = []
329*4882a593Smuzhiyun
330*4882a593Smuzhiyun            # Run through all revisions in our data
331*4882a593Smuzhiyun            for meta, test_data in data:
332*4882a593Smuzhiyun                if (not test in test_data['tests'] or
333*4882a593Smuzhiyun                        not meas in test_data['tests'][test]['measurements']):
334*4882a593Smuzhiyun                    samples.append(measurement_stats(None))
335*4882a593Smuzhiyun                    continue
336*4882a593Smuzhiyun                test_i = test_data['tests'][test]
337*4882a593Smuzhiyun                meas_i = test_i['measurements'][meas]
338*4882a593Smuzhiyun                commit_num = get_data_item(meta, 'layers.meta.commit_count')
339*4882a593Smuzhiyun                samples.append(measurement_stats(meas_i))
340*4882a593Smuzhiyun                samples[-1]['commit_num'] = commit_num
341*4882a593Smuzhiyun
342*4882a593Smuzhiyun            absdiff = samples[-1]['val_cls'](samples[-1]['mean'] - samples[id_comp]['mean'])
343*4882a593Smuzhiyun            reldiff = absdiff * 100 / samples[id_comp]['mean']
344*4882a593Smuzhiyun            new_meas['absdiff'] = absdiff
345*4882a593Smuzhiyun            new_meas['absdiff_str'] = str(absdiff) if absdiff < 0 else '+' + str(absdiff)
346*4882a593Smuzhiyun            new_meas['reldiff'] = reldiff
347*4882a593Smuzhiyun            new_meas['reldiff_str'] = "{:+.1f} %".format(reldiff)
348*4882a593Smuzhiyun            new_meas['samples'] = samples
349*4882a593Smuzhiyun            new_meas['value'] = samples[-1]
350*4882a593Smuzhiyun            new_meas['value_type'] = samples[-1]['val_cls']
351*4882a593Smuzhiyun
352*4882a593Smuzhiyun            # Compare buildstats
353*4882a593Smuzhiyun            bs_key = test + '.' + meas
354*4882a593Smuzhiyun            rev = str(metadata['commit_num']['value'])
355*4882a593Smuzhiyun            comp_rev = str(metadata['commit_num']['value_old'])
356*4882a593Smuzhiyun            if (buildstats and rev in buildstats and bs_key in buildstats[rev] and
357*4882a593Smuzhiyun                    comp_rev in buildstats and bs_key in buildstats[comp_rev]):
358*4882a593Smuzhiyun                new_meas['buildstats'] = BSSummary(buildstats[comp_rev][bs_key],
359*4882a593Smuzhiyun                                                   buildstats[rev][bs_key])
360*4882a593Smuzhiyun
361*4882a593Smuzhiyun
362*4882a593Smuzhiyun            new_test['measurements'].append(new_meas)
363*4882a593Smuzhiyun        tests.append(new_test)
364*4882a593Smuzhiyun
365*4882a593Smuzhiyun    # Chart options
366*4882a593Smuzhiyun    chart_opts = {'haxis': {'min': get_data_item(data[0][0], 'layers.meta.commit_count'),
367*4882a593Smuzhiyun                            'max': get_data_item(data[-1][0], 'layers.meta.commit_count')}
368*4882a593Smuzhiyun                 }
369*4882a593Smuzhiyun
370*4882a593Smuzhiyun    print(html.template.render(title="Build Perf Test Report",
371*4882a593Smuzhiyun                               metadata=metadata, test_data=tests,
372*4882a593Smuzhiyun                               chart_opts=chart_opts))
373*4882a593Smuzhiyun
374*4882a593Smuzhiyun
375*4882a593Smuzhiyundef get_buildstats(repo, notes_ref, notes_ref2, revs, outdir=None):
376*4882a593Smuzhiyun    """Get the buildstats from git notes"""
377*4882a593Smuzhiyun    full_ref = 'refs/notes/' + notes_ref
378*4882a593Smuzhiyun    if not repo.rev_parse(full_ref):
379*4882a593Smuzhiyun        log.error("No buildstats found, please try running "
380*4882a593Smuzhiyun                  "'git fetch origin %s:%s' to fetch them from the remote",
381*4882a593Smuzhiyun                  full_ref, full_ref)
382*4882a593Smuzhiyun        return
383*4882a593Smuzhiyun
384*4882a593Smuzhiyun    missing = False
385*4882a593Smuzhiyun    buildstats = {}
386*4882a593Smuzhiyun    log.info("Parsing buildstats from 'refs/notes/%s'", notes_ref)
387*4882a593Smuzhiyun    for rev in revs:
388*4882a593Smuzhiyun        buildstats[rev.commit_number] = {}
389*4882a593Smuzhiyun        log.debug('Dumping buildstats for %s (%s)', rev.commit_number,
390*4882a593Smuzhiyun                  rev.commit)
391*4882a593Smuzhiyun        for tag in rev.tags:
392*4882a593Smuzhiyun            log.debug('    %s', tag)
393*4882a593Smuzhiyun            try:
394*4882a593Smuzhiyun                try:
395*4882a593Smuzhiyun                    bs_all = json.loads(repo.run_cmd(['notes', '--ref', notes_ref, 'show', tag + '^0']))
396*4882a593Smuzhiyun                except GitError:
397*4882a593Smuzhiyun                    if notes_ref2:
398*4882a593Smuzhiyun                        bs_all = json.loads(repo.run_cmd(['notes', '--ref', notes_ref2, 'show', tag + '^0']))
399*4882a593Smuzhiyun                    else:
400*4882a593Smuzhiyun                        raise
401*4882a593Smuzhiyun            except GitError:
402*4882a593Smuzhiyun                log.warning("Buildstats not found for %s", tag)
403*4882a593Smuzhiyun                bs_all = {}
404*4882a593Smuzhiyun                missing = True
405*4882a593Smuzhiyun
406*4882a593Smuzhiyun            for measurement, bs in bs_all.items():
407*4882a593Smuzhiyun                # Write out onto disk
408*4882a593Smuzhiyun                if outdir:
409*4882a593Smuzhiyun                    tag_base, run_id = tag.rsplit('/', 1)
410*4882a593Smuzhiyun                    tag_base = tag_base.replace('/', '_')
411*4882a593Smuzhiyun                    bs_dir = os.path.join(outdir, measurement, tag_base)
412*4882a593Smuzhiyun                    if not os.path.exists(bs_dir):
413*4882a593Smuzhiyun                        os.makedirs(bs_dir)
414*4882a593Smuzhiyun                    with open(os.path.join(bs_dir, run_id + '.json'), 'w') as f:
415*4882a593Smuzhiyun                        json.dump(bs, f, indent=2)
416*4882a593Smuzhiyun
417*4882a593Smuzhiyun                # Read buildstats into a dict
418*4882a593Smuzhiyun                _bs = BuildStats.from_json(bs)
419*4882a593Smuzhiyun                if measurement not in buildstats[rev.commit_number]:
420*4882a593Smuzhiyun                    buildstats[rev.commit_number][measurement] = _bs
421*4882a593Smuzhiyun                else:
422*4882a593Smuzhiyun                    buildstats[rev.commit_number][measurement].aggregate(_bs)
423*4882a593Smuzhiyun
424*4882a593Smuzhiyun    if missing:
425*4882a593Smuzhiyun        log.info("Buildstats were missing for some test runs, please "
426*4882a593Smuzhiyun                 "run 'git fetch origin %s:%s' and try again",
427*4882a593Smuzhiyun                 full_ref, full_ref)
428*4882a593Smuzhiyun
429*4882a593Smuzhiyun    return buildstats
430*4882a593Smuzhiyun
431*4882a593Smuzhiyun
432*4882a593Smuzhiyundef auto_args(repo, args):
433*4882a593Smuzhiyun    """Guess arguments, if not defined by the user"""
434*4882a593Smuzhiyun    # Get the latest commit in the repo
435*4882a593Smuzhiyun    log.debug("Guessing arguments from the latest commit")
436*4882a593Smuzhiyun    msg = repo.run_cmd(['log', '-1', '--branches', '--remotes', '--format=%b'])
437*4882a593Smuzhiyun    for line in msg.splitlines():
438*4882a593Smuzhiyun        split = line.split(':', 1)
439*4882a593Smuzhiyun        if len(split) != 2:
440*4882a593Smuzhiyun            continue
441*4882a593Smuzhiyun
442*4882a593Smuzhiyun        key = split[0]
443*4882a593Smuzhiyun        val = split[1].strip()
444*4882a593Smuzhiyun        if key == 'hostname' and not args.hostname:
445*4882a593Smuzhiyun            log.debug("Using hostname %s", val)
446*4882a593Smuzhiyun            args.hostname = val
447*4882a593Smuzhiyun        elif key == 'branch' and not args.branch:
448*4882a593Smuzhiyun            log.debug("Using branch %s", val)
449*4882a593Smuzhiyun            args.branch = val
450*4882a593Smuzhiyun
451*4882a593Smuzhiyun
452*4882a593Smuzhiyundef parse_args(argv):
453*4882a593Smuzhiyun    """Parse command line arguments"""
454*4882a593Smuzhiyun    description = """
455*4882a593SmuzhiyunExamine build performance test results from a Git repository"""
456*4882a593Smuzhiyun    parser = argparse.ArgumentParser(
457*4882a593Smuzhiyun        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
458*4882a593Smuzhiyun        description=description)
459*4882a593Smuzhiyun
460*4882a593Smuzhiyun    parser.add_argument('--debug', '-d', action='store_true',
461*4882a593Smuzhiyun                        help="Verbose logging")
462*4882a593Smuzhiyun    parser.add_argument('--repo', '-r', required=True,
463*4882a593Smuzhiyun                        help="Results repository (local git clone)")
464*4882a593Smuzhiyun    parser.add_argument('--list', '-l', action='count',
465*4882a593Smuzhiyun                        help="List available test runs")
466*4882a593Smuzhiyun    parser.add_argument('--html', action='store_true',
467*4882a593Smuzhiyun                        help="Generate report in html format")
468*4882a593Smuzhiyun    group = parser.add_argument_group('Tag and revision')
469*4882a593Smuzhiyun    group.add_argument('--tag-name', '-t',
470*4882a593Smuzhiyun                       default='{hostname}/{branch}/{machine}/{commit_number}-g{commit}/{tag_number}',
471*4882a593Smuzhiyun                       help="Tag name (pattern) for finding results")
472*4882a593Smuzhiyun    group.add_argument('--hostname', '-H')
473*4882a593Smuzhiyun    group.add_argument('--branch', '-B', default='master', help="Branch to find commit in")
474*4882a593Smuzhiyun    group.add_argument('--branch2', help="Branch to find comparision revisions in")
475*4882a593Smuzhiyun    group.add_argument('--machine', default='qemux86')
476*4882a593Smuzhiyun    group.add_argument('--history-length', default=25, type=int,
477*4882a593Smuzhiyun                       help="Number of tested revisions to plot in html report")
478*4882a593Smuzhiyun    group.add_argument('--commit',
479*4882a593Smuzhiyun                       help="Revision to search for")
480*4882a593Smuzhiyun    group.add_argument('--commit-number',
481*4882a593Smuzhiyun                       help="Revision number to search for, redundant if "
482*4882a593Smuzhiyun                            "--commit is specified")
483*4882a593Smuzhiyun    group.add_argument('--commit2',
484*4882a593Smuzhiyun                       help="Revision to compare with")
485*4882a593Smuzhiyun    group.add_argument('--commit-number2',
486*4882a593Smuzhiyun                       help="Revision number to compare with, redundant if "
487*4882a593Smuzhiyun                            "--commit2 is specified")
488*4882a593Smuzhiyun    parser.add_argument('--dump-buildstats', nargs='?', const='.',
489*4882a593Smuzhiyun                        help="Dump buildstats of the tests")
490*4882a593Smuzhiyun
491*4882a593Smuzhiyun    return parser.parse_args(argv)
492*4882a593Smuzhiyun
493*4882a593Smuzhiyun
494*4882a593Smuzhiyundef main(argv=None):
495*4882a593Smuzhiyun    """Script entry point"""
496*4882a593Smuzhiyun    args = parse_args(argv)
497*4882a593Smuzhiyun    if args.debug:
498*4882a593Smuzhiyun        log.setLevel(logging.DEBUG)
499*4882a593Smuzhiyun
500*4882a593Smuzhiyun    repo = GitRepo(args.repo)
501*4882a593Smuzhiyun
502*4882a593Smuzhiyun    if args.list:
503*4882a593Smuzhiyun        list_test_revs(repo, args.tag_name, args.list, hostname=args.hostname)
504*4882a593Smuzhiyun        return 0
505*4882a593Smuzhiyun
506*4882a593Smuzhiyun    # Determine hostname which to use
507*4882a593Smuzhiyun    if not args.hostname:
508*4882a593Smuzhiyun        auto_args(repo, args)
509*4882a593Smuzhiyun
510*4882a593Smuzhiyun    revs = gitarchive.get_test_revs(log, repo, args.tag_name, hostname=args.hostname,
511*4882a593Smuzhiyun                                    branch=args.branch, machine=args.machine)
512*4882a593Smuzhiyun    if args.branch2 and args.branch2 != args.branch:
513*4882a593Smuzhiyun        revs2 = gitarchive.get_test_revs(log, repo, args.tag_name, hostname=args.hostname,
514*4882a593Smuzhiyun                                         branch=args.branch2, machine=args.machine)
515*4882a593Smuzhiyun        if not len(revs2):
516*4882a593Smuzhiyun            log.error("No revisions found to compare against")
517*4882a593Smuzhiyun            return 1
518*4882a593Smuzhiyun        if not len(revs):
519*4882a593Smuzhiyun            log.error("No revision to report on found")
520*4882a593Smuzhiyun            return 1
521*4882a593Smuzhiyun    else:
522*4882a593Smuzhiyun        if len(revs) < 2:
523*4882a593Smuzhiyun            log.error("Only %d tester revisions found, unable to generate report" % len(revs))
524*4882a593Smuzhiyun            return 1
525*4882a593Smuzhiyun
526*4882a593Smuzhiyun    # Pick revisions
527*4882a593Smuzhiyun    if args.commit:
528*4882a593Smuzhiyun        if args.commit_number:
529*4882a593Smuzhiyun            log.warning("Ignoring --commit-number as --commit was specified")
530*4882a593Smuzhiyun        index1 = gitarchive.rev_find(revs, 'commit', args.commit)
531*4882a593Smuzhiyun    elif args.commit_number:
532*4882a593Smuzhiyun        index1 = gitarchive.rev_find(revs, 'commit_number', args.commit_number)
533*4882a593Smuzhiyun    else:
534*4882a593Smuzhiyun        index1 = len(revs) - 1
535*4882a593Smuzhiyun
536*4882a593Smuzhiyun    if args.branch2 and args.branch2 != args.branch:
537*4882a593Smuzhiyun        revs2.append(revs[index1])
538*4882a593Smuzhiyun        index1 = len(revs2) - 1
539*4882a593Smuzhiyun        revs = revs2
540*4882a593Smuzhiyun
541*4882a593Smuzhiyun    if args.commit2:
542*4882a593Smuzhiyun        if args.commit_number2:
543*4882a593Smuzhiyun            log.warning("Ignoring --commit-number2 as --commit2 was specified")
544*4882a593Smuzhiyun        index2 = gitarchive.rev_find(revs, 'commit', args.commit2)
545*4882a593Smuzhiyun    elif args.commit_number2:
546*4882a593Smuzhiyun        index2 = gitarchive.rev_find(revs, 'commit_number', args.commit_number2)
547*4882a593Smuzhiyun    else:
548*4882a593Smuzhiyun        if index1 > 0:
549*4882a593Smuzhiyun            index2 = index1 - 1
550*4882a593Smuzhiyun            # Find the closest matching commit number for comparision
551*4882a593Smuzhiyun            # In future we could check the commit is a common ancestor and
552*4882a593Smuzhiyun            # continue back if not but this good enough for now
553*4882a593Smuzhiyun            while index2 > 0 and revs[index2].commit_number > revs[index1].commit_number:
554*4882a593Smuzhiyun                index2 = index2 - 1
555*4882a593Smuzhiyun        else:
556*4882a593Smuzhiyun            log.error("Unable to determine the other commit, use "
557*4882a593Smuzhiyun                      "--commit2 or --commit-number2 to specify it")
558*4882a593Smuzhiyun            return 1
559*4882a593Smuzhiyun
560*4882a593Smuzhiyun    index_l = min(index1, index2)
561*4882a593Smuzhiyun    index_r = max(index1, index2)
562*4882a593Smuzhiyun
563*4882a593Smuzhiyun    rev_l = revs[index_l]
564*4882a593Smuzhiyun    rev_r = revs[index_r]
565*4882a593Smuzhiyun    log.debug("Using 'left' revision %s (%s), %s test runs:\n    %s",
566*4882a593Smuzhiyun              rev_l.commit_number, rev_l.commit, len(rev_l.tags),
567*4882a593Smuzhiyun              '\n    '.join(rev_l.tags))
568*4882a593Smuzhiyun    log.debug("Using 'right' revision %s (%s), %s test runs:\n    %s",
569*4882a593Smuzhiyun              rev_r.commit_number, rev_r.commit, len(rev_r.tags),
570*4882a593Smuzhiyun              '\n    '.join(rev_r.tags))
571*4882a593Smuzhiyun
572*4882a593Smuzhiyun    # Check report format used in the repo (assume all reports in the same fmt)
573*4882a593Smuzhiyun    xml = is_xml_format(repo, revs[index_r].tags[-1])
574*4882a593Smuzhiyun
575*4882a593Smuzhiyun    if args.html:
576*4882a593Smuzhiyun        index_0 = max(0, min(index_l, index_r - args.history_length))
577*4882a593Smuzhiyun        rev_range = range(index_0, index_r + 1)
578*4882a593Smuzhiyun    else:
579*4882a593Smuzhiyun        # We do not need range of commits for text report (no graphs)
580*4882a593Smuzhiyun        index_0 = index_l
581*4882a593Smuzhiyun        rev_range = (index_l, index_r)
582*4882a593Smuzhiyun
583*4882a593Smuzhiyun    # Read raw data
584*4882a593Smuzhiyun    log.debug("Reading %d revisions, starting from %s (%s)",
585*4882a593Smuzhiyun              len(rev_range), revs[index_0].commit_number, revs[index_0].commit)
586*4882a593Smuzhiyun    raw_data = [read_results(repo, revs[i].tags, xml) for i in rev_range]
587*4882a593Smuzhiyun
588*4882a593Smuzhiyun    data = []
589*4882a593Smuzhiyun    for raw_m, raw_d in raw_data:
590*4882a593Smuzhiyun        data.append(AggregateTestData(aggregate_metadata(raw_m),
591*4882a593Smuzhiyun                                      aggregate_data(raw_d)))
592*4882a593Smuzhiyun
593*4882a593Smuzhiyun    # Read buildstats only when needed
594*4882a593Smuzhiyun    buildstats = None
595*4882a593Smuzhiyun    if args.dump_buildstats or args.html:
596*4882a593Smuzhiyun        outdir = 'oe-build-perf-buildstats' if args.dump_buildstats else None
597*4882a593Smuzhiyun        notes_ref = 'buildstats/{}/{}/{}'.format(args.hostname, args.branch, args.machine)
598*4882a593Smuzhiyun        notes_ref2 = None
599*4882a593Smuzhiyun        if args.branch2:
600*4882a593Smuzhiyun            notes_ref = 'buildstats/{}/{}/{}'.format(args.hostname, args.branch2, args.machine)
601*4882a593Smuzhiyun            notes_ref2 = 'buildstats/{}/{}/{}'.format(args.hostname, args.branch, args.machine)
602*4882a593Smuzhiyun        buildstats = get_buildstats(repo, notes_ref, notes_ref2, [rev_l, rev_r], outdir)
603*4882a593Smuzhiyun
604*4882a593Smuzhiyun    # Print report
605*4882a593Smuzhiyun    if not args.html:
606*4882a593Smuzhiyun        print_diff_report(data[0].metadata, data[0].results,
607*4882a593Smuzhiyun                          data[1].metadata, data[1].results)
608*4882a593Smuzhiyun    else:
609*4882a593Smuzhiyun        # Re-map 'left' list index to the data table where index_0 maps to 0
610*4882a593Smuzhiyun        print_html_report(data, index_l - index_0, buildstats)
611*4882a593Smuzhiyun
612*4882a593Smuzhiyun    return 0
613*4882a593Smuzhiyun
614*4882a593Smuzhiyunif __name__ == "__main__":
615*4882a593Smuzhiyun    sys.exit(main())
616