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, "{} → {}".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, "{} → {}".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, "{} → {}".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