1*4882a593Smuzhiyun# Copyright (c) 2016, Intel Corporation. 2*4882a593Smuzhiyun# 3*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 4*4882a593Smuzhiyun# 5*4882a593Smuzhiyun"""Build performance test base classes and functionality""" 6*4882a593Smuzhiyunimport json 7*4882a593Smuzhiyunimport logging 8*4882a593Smuzhiyunimport os 9*4882a593Smuzhiyunimport re 10*4882a593Smuzhiyunimport resource 11*4882a593Smuzhiyunimport socket 12*4882a593Smuzhiyunimport shutil 13*4882a593Smuzhiyunimport time 14*4882a593Smuzhiyunimport unittest 15*4882a593Smuzhiyunimport xml.etree.ElementTree as ET 16*4882a593Smuzhiyunfrom collections import OrderedDict 17*4882a593Smuzhiyunfrom datetime import datetime, timedelta 18*4882a593Smuzhiyunfrom functools import partial 19*4882a593Smuzhiyunfrom multiprocessing import Process 20*4882a593Smuzhiyunfrom multiprocessing import SimpleQueue 21*4882a593Smuzhiyunfrom xml.dom import minidom 22*4882a593Smuzhiyun 23*4882a593Smuzhiyunimport oe.path 24*4882a593Smuzhiyunfrom oeqa.utils.commands import CommandError, runCmd, get_bb_vars 25*4882a593Smuzhiyunfrom oeqa.utils.git import GitError, GitRepo 26*4882a593Smuzhiyun 27*4882a593Smuzhiyun# Get logger for this module 28*4882a593Smuzhiyunlog = logging.getLogger('build-perf') 29*4882a593Smuzhiyun 30*4882a593Smuzhiyun# Our own version of runCmd which does not raise AssertErrors which would cause 31*4882a593Smuzhiyun# errors to interpreted as failures 32*4882a593SmuzhiyunrunCmd2 = partial(runCmd, assert_error=False, limit_exc_output=40) 33*4882a593Smuzhiyun 34*4882a593Smuzhiyun 35*4882a593Smuzhiyunclass KernelDropCaches(object): 36*4882a593Smuzhiyun """Container of the functions for dropping kernel caches""" 37*4882a593Smuzhiyun sudo_passwd = None 38*4882a593Smuzhiyun 39*4882a593Smuzhiyun @classmethod 40*4882a593Smuzhiyun def check(cls): 41*4882a593Smuzhiyun """Check permssions for dropping kernel caches""" 42*4882a593Smuzhiyun from getpass import getpass 43*4882a593Smuzhiyun from locale import getdefaultlocale 44*4882a593Smuzhiyun cmd = ['sudo', '-k', '-n', 'tee', '/proc/sys/vm/drop_caches'] 45*4882a593Smuzhiyun ret = runCmd2(cmd, ignore_status=True, data=b'0') 46*4882a593Smuzhiyun if ret.output.startswith('sudo:'): 47*4882a593Smuzhiyun pass_str = getpass( 48*4882a593Smuzhiyun "\nThe script requires sudo access to drop caches between " 49*4882a593Smuzhiyun "builds (echo 3 > /proc/sys/vm/drop_caches).\n" 50*4882a593Smuzhiyun "Please enter your sudo password: ") 51*4882a593Smuzhiyun cls.sudo_passwd = bytes(pass_str, getdefaultlocale()[1]) 52*4882a593Smuzhiyun 53*4882a593Smuzhiyun @classmethod 54*4882a593Smuzhiyun def drop(cls): 55*4882a593Smuzhiyun """Drop kernel caches""" 56*4882a593Smuzhiyun cmd = ['sudo', '-k'] 57*4882a593Smuzhiyun if cls.sudo_passwd: 58*4882a593Smuzhiyun cmd.append('-S') 59*4882a593Smuzhiyun input_data = cls.sudo_passwd + b'\n' 60*4882a593Smuzhiyun else: 61*4882a593Smuzhiyun cmd.append('-n') 62*4882a593Smuzhiyun input_data = b'' 63*4882a593Smuzhiyun cmd += ['tee', '/proc/sys/vm/drop_caches'] 64*4882a593Smuzhiyun input_data += b'3' 65*4882a593Smuzhiyun runCmd2(cmd, data=input_data) 66*4882a593Smuzhiyun 67*4882a593Smuzhiyun 68*4882a593Smuzhiyundef str_to_fn(string): 69*4882a593Smuzhiyun """Convert string to a sanitized filename""" 70*4882a593Smuzhiyun return re.sub(r'(\W+)', '-', string, flags=re.LOCALE) 71*4882a593Smuzhiyun 72*4882a593Smuzhiyun 73*4882a593Smuzhiyunclass ResultsJsonEncoder(json.JSONEncoder): 74*4882a593Smuzhiyun """Extended encoder for build perf test results""" 75*4882a593Smuzhiyun unix_epoch = datetime.utcfromtimestamp(0) 76*4882a593Smuzhiyun 77*4882a593Smuzhiyun def default(self, obj): 78*4882a593Smuzhiyun """Encoder for our types""" 79*4882a593Smuzhiyun if isinstance(obj, datetime): 80*4882a593Smuzhiyun # NOTE: we assume that all timestamps are in UTC time 81*4882a593Smuzhiyun return (obj - self.unix_epoch).total_seconds() 82*4882a593Smuzhiyun if isinstance(obj, timedelta): 83*4882a593Smuzhiyun return obj.total_seconds() 84*4882a593Smuzhiyun return json.JSONEncoder.default(self, obj) 85*4882a593Smuzhiyun 86*4882a593Smuzhiyun 87*4882a593Smuzhiyunclass BuildPerfTestResult(unittest.TextTestResult): 88*4882a593Smuzhiyun """Runner class for executing the individual tests""" 89*4882a593Smuzhiyun # List of test cases to run 90*4882a593Smuzhiyun test_run_queue = [] 91*4882a593Smuzhiyun 92*4882a593Smuzhiyun def __init__(self, out_dir, *args, **kwargs): 93*4882a593Smuzhiyun super(BuildPerfTestResult, self).__init__(*args, **kwargs) 94*4882a593Smuzhiyun 95*4882a593Smuzhiyun self.out_dir = out_dir 96*4882a593Smuzhiyun self.hostname = socket.gethostname() 97*4882a593Smuzhiyun self.product = os.getenv('OE_BUILDPERFTEST_PRODUCT', 'oe-core') 98*4882a593Smuzhiyun self.start_time = self.elapsed_time = None 99*4882a593Smuzhiyun self.successes = [] 100*4882a593Smuzhiyun 101*4882a593Smuzhiyun def addSuccess(self, test): 102*4882a593Smuzhiyun """Record results from successful tests""" 103*4882a593Smuzhiyun super(BuildPerfTestResult, self).addSuccess(test) 104*4882a593Smuzhiyun self.successes.append(test) 105*4882a593Smuzhiyun 106*4882a593Smuzhiyun def addError(self, test, err): 107*4882a593Smuzhiyun """Record results from crashed test""" 108*4882a593Smuzhiyun test.err = err 109*4882a593Smuzhiyun super(BuildPerfTestResult, self).addError(test, err) 110*4882a593Smuzhiyun 111*4882a593Smuzhiyun def addFailure(self, test, err): 112*4882a593Smuzhiyun """Record results from failed test""" 113*4882a593Smuzhiyun test.err = err 114*4882a593Smuzhiyun super(BuildPerfTestResult, self).addFailure(test, err) 115*4882a593Smuzhiyun 116*4882a593Smuzhiyun def addExpectedFailure(self, test, err): 117*4882a593Smuzhiyun """Record results from expectedly failed test""" 118*4882a593Smuzhiyun test.err = err 119*4882a593Smuzhiyun super(BuildPerfTestResult, self).addExpectedFailure(test, err) 120*4882a593Smuzhiyun 121*4882a593Smuzhiyun def startTest(self, test): 122*4882a593Smuzhiyun """Pre-test hook""" 123*4882a593Smuzhiyun test.base_dir = self.out_dir 124*4882a593Smuzhiyun log.info("Executing test %s: %s", test.name, test.shortDescription()) 125*4882a593Smuzhiyun self.stream.write(datetime.now().strftime("[%Y-%m-%d %H:%M:%S] ")) 126*4882a593Smuzhiyun super(BuildPerfTestResult, self).startTest(test) 127*4882a593Smuzhiyun 128*4882a593Smuzhiyun def startTestRun(self): 129*4882a593Smuzhiyun """Pre-run hook""" 130*4882a593Smuzhiyun self.start_time = datetime.utcnow() 131*4882a593Smuzhiyun 132*4882a593Smuzhiyun def stopTestRun(self): 133*4882a593Smuzhiyun """Pre-run hook""" 134*4882a593Smuzhiyun self.elapsed_time = datetime.utcnow() - self.start_time 135*4882a593Smuzhiyun 136*4882a593Smuzhiyun def all_results(self): 137*4882a593Smuzhiyun compound = [('SUCCESS', t, None) for t in self.successes] + \ 138*4882a593Smuzhiyun [('FAILURE', t, m) for t, m in self.failures] + \ 139*4882a593Smuzhiyun [('ERROR', t, m) for t, m in self.errors] + \ 140*4882a593Smuzhiyun [('EXPECTED_FAILURE', t, m) for t, m in self.expectedFailures] + \ 141*4882a593Smuzhiyun [('UNEXPECTED_SUCCESS', t, None) for t in self.unexpectedSuccesses] + \ 142*4882a593Smuzhiyun [('SKIPPED', t, m) for t, m in self.skipped] 143*4882a593Smuzhiyun return sorted(compound, key=lambda info: info[1].start_time) 144*4882a593Smuzhiyun 145*4882a593Smuzhiyun 146*4882a593Smuzhiyun def write_buildstats_json(self): 147*4882a593Smuzhiyun """Write buildstats file""" 148*4882a593Smuzhiyun buildstats = OrderedDict() 149*4882a593Smuzhiyun for _, test, _ in self.all_results(): 150*4882a593Smuzhiyun for key, val in test.buildstats.items(): 151*4882a593Smuzhiyun buildstats[test.name + '.' + key] = val 152*4882a593Smuzhiyun with open(os.path.join(self.out_dir, 'buildstats.json'), 'w') as fobj: 153*4882a593Smuzhiyun json.dump(buildstats, fobj, cls=ResultsJsonEncoder) 154*4882a593Smuzhiyun 155*4882a593Smuzhiyun 156*4882a593Smuzhiyun def write_results_json(self): 157*4882a593Smuzhiyun """Write test results into a json-formatted file""" 158*4882a593Smuzhiyun results = OrderedDict([('tester_host', self.hostname), 159*4882a593Smuzhiyun ('start_time', self.start_time), 160*4882a593Smuzhiyun ('elapsed_time', self.elapsed_time), 161*4882a593Smuzhiyun ('tests', OrderedDict())]) 162*4882a593Smuzhiyun 163*4882a593Smuzhiyun for status, test, reason in self.all_results(): 164*4882a593Smuzhiyun test_result = OrderedDict([('name', test.name), 165*4882a593Smuzhiyun ('description', test.shortDescription()), 166*4882a593Smuzhiyun ('status', status), 167*4882a593Smuzhiyun ('start_time', test.start_time), 168*4882a593Smuzhiyun ('elapsed_time', test.elapsed_time), 169*4882a593Smuzhiyun ('measurements', test.measurements)]) 170*4882a593Smuzhiyun if status in ('ERROR', 'FAILURE', 'EXPECTED_FAILURE'): 171*4882a593Smuzhiyun test_result['message'] = str(test.err[1]) 172*4882a593Smuzhiyun test_result['err_type'] = test.err[0].__name__ 173*4882a593Smuzhiyun test_result['err_output'] = reason 174*4882a593Smuzhiyun elif reason: 175*4882a593Smuzhiyun test_result['message'] = reason 176*4882a593Smuzhiyun 177*4882a593Smuzhiyun results['tests'][test.name] = test_result 178*4882a593Smuzhiyun 179*4882a593Smuzhiyun with open(os.path.join(self.out_dir, 'results.json'), 'w') as fobj: 180*4882a593Smuzhiyun json.dump(results, fobj, indent=4, 181*4882a593Smuzhiyun cls=ResultsJsonEncoder) 182*4882a593Smuzhiyun 183*4882a593Smuzhiyun def write_results_xml(self): 184*4882a593Smuzhiyun """Write test results into a JUnit XML file""" 185*4882a593Smuzhiyun top = ET.Element('testsuites') 186*4882a593Smuzhiyun suite = ET.SubElement(top, 'testsuite') 187*4882a593Smuzhiyun suite.set('name', 'oeqa.buildperf') 188*4882a593Smuzhiyun suite.set('timestamp', self.start_time.isoformat()) 189*4882a593Smuzhiyun suite.set('time', str(self.elapsed_time.total_seconds())) 190*4882a593Smuzhiyun suite.set('hostname', self.hostname) 191*4882a593Smuzhiyun suite.set('failures', str(len(self.failures) + len(self.expectedFailures))) 192*4882a593Smuzhiyun suite.set('errors', str(len(self.errors))) 193*4882a593Smuzhiyun suite.set('skipped', str(len(self.skipped))) 194*4882a593Smuzhiyun 195*4882a593Smuzhiyun test_cnt = 0 196*4882a593Smuzhiyun for status, test, reason in self.all_results(): 197*4882a593Smuzhiyun test_cnt += 1 198*4882a593Smuzhiyun testcase = ET.SubElement(suite, 'testcase') 199*4882a593Smuzhiyun testcase.set('classname', test.__module__ + '.' + test.__class__.__name__) 200*4882a593Smuzhiyun testcase.set('name', test.name) 201*4882a593Smuzhiyun testcase.set('description', test.shortDescription()) 202*4882a593Smuzhiyun testcase.set('timestamp', test.start_time.isoformat()) 203*4882a593Smuzhiyun testcase.set('time', str(test.elapsed_time.total_seconds())) 204*4882a593Smuzhiyun if status in ('ERROR', 'FAILURE', 'EXP_FAILURE'): 205*4882a593Smuzhiyun if status in ('FAILURE', 'EXP_FAILURE'): 206*4882a593Smuzhiyun result = ET.SubElement(testcase, 'failure') 207*4882a593Smuzhiyun else: 208*4882a593Smuzhiyun result = ET.SubElement(testcase, 'error') 209*4882a593Smuzhiyun result.set('message', str(test.err[1])) 210*4882a593Smuzhiyun result.set('type', test.err[0].__name__) 211*4882a593Smuzhiyun result.text = reason 212*4882a593Smuzhiyun elif status == 'SKIPPED': 213*4882a593Smuzhiyun result = ET.SubElement(testcase, 'skipped') 214*4882a593Smuzhiyun result.text = reason 215*4882a593Smuzhiyun elif status not in ('SUCCESS', 'UNEXPECTED_SUCCESS'): 216*4882a593Smuzhiyun raise TypeError("BUG: invalid test status '%s'" % status) 217*4882a593Smuzhiyun 218*4882a593Smuzhiyun for data in test.measurements.values(): 219*4882a593Smuzhiyun measurement = ET.SubElement(testcase, data['type']) 220*4882a593Smuzhiyun measurement.set('name', data['name']) 221*4882a593Smuzhiyun measurement.set('legend', data['legend']) 222*4882a593Smuzhiyun vals = data['values'] 223*4882a593Smuzhiyun if data['type'] == BuildPerfTestCase.SYSRES: 224*4882a593Smuzhiyun ET.SubElement(measurement, 'time', 225*4882a593Smuzhiyun timestamp=vals['start_time'].isoformat()).text = \ 226*4882a593Smuzhiyun str(vals['elapsed_time'].total_seconds()) 227*4882a593Smuzhiyun attrib = dict((k, str(v)) for k, v in vals['iostat'].items()) 228*4882a593Smuzhiyun ET.SubElement(measurement, 'iostat', attrib=attrib) 229*4882a593Smuzhiyun attrib = dict((k, str(v)) for k, v in vals['rusage'].items()) 230*4882a593Smuzhiyun ET.SubElement(measurement, 'rusage', attrib=attrib) 231*4882a593Smuzhiyun elif data['type'] == BuildPerfTestCase.DISKUSAGE: 232*4882a593Smuzhiyun ET.SubElement(measurement, 'size').text = str(vals['size']) 233*4882a593Smuzhiyun else: 234*4882a593Smuzhiyun raise TypeError('BUG: unsupported measurement type') 235*4882a593Smuzhiyun 236*4882a593Smuzhiyun suite.set('tests', str(test_cnt)) 237*4882a593Smuzhiyun 238*4882a593Smuzhiyun # Use minidom for pretty-printing 239*4882a593Smuzhiyun dom_doc = minidom.parseString(ET.tostring(top, 'utf-8')) 240*4882a593Smuzhiyun with open(os.path.join(self.out_dir, 'results.xml'), 'w') as fobj: 241*4882a593Smuzhiyun dom_doc.writexml(fobj, addindent=' ', newl='\n', encoding='utf-8') 242*4882a593Smuzhiyun 243*4882a593Smuzhiyun 244*4882a593Smuzhiyunclass BuildPerfTestCase(unittest.TestCase): 245*4882a593Smuzhiyun """Base class for build performance tests""" 246*4882a593Smuzhiyun SYSRES = 'sysres' 247*4882a593Smuzhiyun DISKUSAGE = 'diskusage' 248*4882a593Smuzhiyun build_target = None 249*4882a593Smuzhiyun 250*4882a593Smuzhiyun def __init__(self, *args, **kwargs): 251*4882a593Smuzhiyun super(BuildPerfTestCase, self).__init__(*args, **kwargs) 252*4882a593Smuzhiyun self.name = self._testMethodName 253*4882a593Smuzhiyun self.base_dir = None 254*4882a593Smuzhiyun self.start_time = None 255*4882a593Smuzhiyun self.elapsed_time = None 256*4882a593Smuzhiyun self.measurements = OrderedDict() 257*4882a593Smuzhiyun self.buildstats = OrderedDict() 258*4882a593Smuzhiyun # self.err is supposed to be a tuple from sys.exc_info() 259*4882a593Smuzhiyun self.err = None 260*4882a593Smuzhiyun self.bb_vars = get_bb_vars() 261*4882a593Smuzhiyun # TODO: remove 'times' and 'sizes' arrays when globalres support is 262*4882a593Smuzhiyun # removed 263*4882a593Smuzhiyun self.times = [] 264*4882a593Smuzhiyun self.sizes = [] 265*4882a593Smuzhiyun 266*4882a593Smuzhiyun @property 267*4882a593Smuzhiyun def tmp_dir(self): 268*4882a593Smuzhiyun return os.path.join(self.base_dir, self.name + '.tmp') 269*4882a593Smuzhiyun 270*4882a593Smuzhiyun def shortDescription(self): 271*4882a593Smuzhiyun return super(BuildPerfTestCase, self).shortDescription() or "" 272*4882a593Smuzhiyun 273*4882a593Smuzhiyun def setUp(self): 274*4882a593Smuzhiyun """Set-up fixture for each test""" 275*4882a593Smuzhiyun if not os.path.isdir(self.tmp_dir): 276*4882a593Smuzhiyun os.mkdir(self.tmp_dir) 277*4882a593Smuzhiyun if self.build_target: 278*4882a593Smuzhiyun self.run_cmd(['bitbake', self.build_target, '--runall=fetch']) 279*4882a593Smuzhiyun 280*4882a593Smuzhiyun def tearDown(self): 281*4882a593Smuzhiyun """Tear-down fixture for each test""" 282*4882a593Smuzhiyun if os.path.isdir(self.tmp_dir): 283*4882a593Smuzhiyun shutil.rmtree(self.tmp_dir) 284*4882a593Smuzhiyun 285*4882a593Smuzhiyun def run(self, *args, **kwargs): 286*4882a593Smuzhiyun """Run test""" 287*4882a593Smuzhiyun self.start_time = datetime.now() 288*4882a593Smuzhiyun super(BuildPerfTestCase, self).run(*args, **kwargs) 289*4882a593Smuzhiyun self.elapsed_time = datetime.now() - self.start_time 290*4882a593Smuzhiyun 291*4882a593Smuzhiyun def run_cmd(self, cmd): 292*4882a593Smuzhiyun """Convenience method for running a command""" 293*4882a593Smuzhiyun cmd_str = cmd if isinstance(cmd, str) else ' '.join(cmd) 294*4882a593Smuzhiyun log.info("Logging command: %s", cmd_str) 295*4882a593Smuzhiyun try: 296*4882a593Smuzhiyun runCmd2(cmd) 297*4882a593Smuzhiyun except CommandError as err: 298*4882a593Smuzhiyun log.error("Command failed: %s", err.retcode) 299*4882a593Smuzhiyun raise 300*4882a593Smuzhiyun 301*4882a593Smuzhiyun def _append_measurement(self, measurement): 302*4882a593Smuzhiyun """Simple helper for adding measurements results""" 303*4882a593Smuzhiyun if measurement['name'] in self.measurements: 304*4882a593Smuzhiyun raise ValueError('BUG: two measurements with the same name in {}'.format( 305*4882a593Smuzhiyun self.__class__.__name__)) 306*4882a593Smuzhiyun self.measurements[measurement['name']] = measurement 307*4882a593Smuzhiyun 308*4882a593Smuzhiyun def measure_cmd_resources(self, cmd, name, legend, save_bs=False): 309*4882a593Smuzhiyun """Measure system resource usage of a command""" 310*4882a593Smuzhiyun def _worker(data_q, cmd, **kwargs): 311*4882a593Smuzhiyun """Worker process for measuring resources""" 312*4882a593Smuzhiyun try: 313*4882a593Smuzhiyun start_time = datetime.now() 314*4882a593Smuzhiyun ret = runCmd2(cmd, **kwargs) 315*4882a593Smuzhiyun etime = datetime.now() - start_time 316*4882a593Smuzhiyun rusage_struct = resource.getrusage(resource.RUSAGE_CHILDREN) 317*4882a593Smuzhiyun iostat = OrderedDict() 318*4882a593Smuzhiyun with open('/proc/{}/io'.format(os.getpid())) as fobj: 319*4882a593Smuzhiyun for line in fobj.readlines(): 320*4882a593Smuzhiyun key, val = line.split(':') 321*4882a593Smuzhiyun iostat[key] = int(val) 322*4882a593Smuzhiyun rusage = OrderedDict() 323*4882a593Smuzhiyun # Skip unused fields, (i.e. 'ru_ixrss', 'ru_idrss', 'ru_isrss', 324*4882a593Smuzhiyun # 'ru_nswap', 'ru_msgsnd', 'ru_msgrcv' and 'ru_nsignals') 325*4882a593Smuzhiyun for key in ['ru_utime', 'ru_stime', 'ru_maxrss', 'ru_minflt', 326*4882a593Smuzhiyun 'ru_majflt', 'ru_inblock', 'ru_oublock', 327*4882a593Smuzhiyun 'ru_nvcsw', 'ru_nivcsw']: 328*4882a593Smuzhiyun rusage[key] = getattr(rusage_struct, key) 329*4882a593Smuzhiyun data_q.put({'ret': ret, 330*4882a593Smuzhiyun 'start_time': start_time, 331*4882a593Smuzhiyun 'elapsed_time': etime, 332*4882a593Smuzhiyun 'rusage': rusage, 333*4882a593Smuzhiyun 'iostat': iostat}) 334*4882a593Smuzhiyun except Exception as err: 335*4882a593Smuzhiyun data_q.put(err) 336*4882a593Smuzhiyun 337*4882a593Smuzhiyun cmd_str = cmd if isinstance(cmd, str) else ' '.join(cmd) 338*4882a593Smuzhiyun log.info("Timing command: %s", cmd_str) 339*4882a593Smuzhiyun data_q = SimpleQueue() 340*4882a593Smuzhiyun try: 341*4882a593Smuzhiyun proc = Process(target=_worker, args=(data_q, cmd,)) 342*4882a593Smuzhiyun proc.start() 343*4882a593Smuzhiyun data = data_q.get() 344*4882a593Smuzhiyun proc.join() 345*4882a593Smuzhiyun if isinstance(data, Exception): 346*4882a593Smuzhiyun raise data 347*4882a593Smuzhiyun except CommandError: 348*4882a593Smuzhiyun log.error("Command '%s' failed", cmd_str) 349*4882a593Smuzhiyun raise 350*4882a593Smuzhiyun etime = data['elapsed_time'] 351*4882a593Smuzhiyun 352*4882a593Smuzhiyun measurement = OrderedDict([('type', self.SYSRES), 353*4882a593Smuzhiyun ('name', name), 354*4882a593Smuzhiyun ('legend', legend)]) 355*4882a593Smuzhiyun measurement['values'] = OrderedDict([('start_time', data['start_time']), 356*4882a593Smuzhiyun ('elapsed_time', etime), 357*4882a593Smuzhiyun ('rusage', data['rusage']), 358*4882a593Smuzhiyun ('iostat', data['iostat'])]) 359*4882a593Smuzhiyun if save_bs: 360*4882a593Smuzhiyun self.save_buildstats(name) 361*4882a593Smuzhiyun 362*4882a593Smuzhiyun self._append_measurement(measurement) 363*4882a593Smuzhiyun 364*4882a593Smuzhiyun # Append to 'times' array for globalres log 365*4882a593Smuzhiyun e_sec = etime.total_seconds() 366*4882a593Smuzhiyun self.times.append('{:d}:{:02d}:{:05.2f}'.format(int(e_sec / 3600), 367*4882a593Smuzhiyun int((e_sec % 3600) / 60), 368*4882a593Smuzhiyun e_sec % 60)) 369*4882a593Smuzhiyun 370*4882a593Smuzhiyun def measure_disk_usage(self, path, name, legend, apparent_size=False): 371*4882a593Smuzhiyun """Estimate disk usage of a file or directory""" 372*4882a593Smuzhiyun cmd = ['du', '-s', '--block-size', '1024'] 373*4882a593Smuzhiyun if apparent_size: 374*4882a593Smuzhiyun cmd.append('--apparent-size') 375*4882a593Smuzhiyun cmd.append(path) 376*4882a593Smuzhiyun 377*4882a593Smuzhiyun ret = runCmd2(cmd) 378*4882a593Smuzhiyun size = int(ret.output.split()[0]) 379*4882a593Smuzhiyun log.debug("Size of %s path is %s", path, size) 380*4882a593Smuzhiyun measurement = OrderedDict([('type', self.DISKUSAGE), 381*4882a593Smuzhiyun ('name', name), 382*4882a593Smuzhiyun ('legend', legend)]) 383*4882a593Smuzhiyun measurement['values'] = OrderedDict([('size', size)]) 384*4882a593Smuzhiyun self._append_measurement(measurement) 385*4882a593Smuzhiyun # Append to 'sizes' array for globalres log 386*4882a593Smuzhiyun self.sizes.append(str(size)) 387*4882a593Smuzhiyun 388*4882a593Smuzhiyun def save_buildstats(self, measurement_name): 389*4882a593Smuzhiyun """Save buildstats""" 390*4882a593Smuzhiyun def split_nevr(nevr): 391*4882a593Smuzhiyun """Split name and version information from recipe "nevr" string""" 392*4882a593Smuzhiyun n_e_v, revision = nevr.rsplit('-', 1) 393*4882a593Smuzhiyun match = re.match(r'^(?P<name>\S+)-((?P<epoch>[0-9]{1,5})_)?(?P<version>[0-9]\S*)$', 394*4882a593Smuzhiyun n_e_v) 395*4882a593Smuzhiyun if not match: 396*4882a593Smuzhiyun # If we're not able to parse a version starting with a number, just 397*4882a593Smuzhiyun # take the part after last dash 398*4882a593Smuzhiyun match = re.match(r'^(?P<name>\S+)-((?P<epoch>[0-9]{1,5})_)?(?P<version>[^-]+)$', 399*4882a593Smuzhiyun n_e_v) 400*4882a593Smuzhiyun name = match.group('name') 401*4882a593Smuzhiyun version = match.group('version') 402*4882a593Smuzhiyun epoch = match.group('epoch') 403*4882a593Smuzhiyun return name, epoch, version, revision 404*4882a593Smuzhiyun 405*4882a593Smuzhiyun def bs_to_json(filename): 406*4882a593Smuzhiyun """Convert (task) buildstats file into json format""" 407*4882a593Smuzhiyun bs_json = OrderedDict() 408*4882a593Smuzhiyun iostat = OrderedDict() 409*4882a593Smuzhiyun rusage = OrderedDict() 410*4882a593Smuzhiyun with open(filename) as fobj: 411*4882a593Smuzhiyun for line in fobj.readlines(): 412*4882a593Smuzhiyun key, val = line.split(':', 1) 413*4882a593Smuzhiyun val = val.strip() 414*4882a593Smuzhiyun if key == 'Started': 415*4882a593Smuzhiyun start_time = datetime.utcfromtimestamp(float(val)) 416*4882a593Smuzhiyun bs_json['start_time'] = start_time 417*4882a593Smuzhiyun elif key == 'Ended': 418*4882a593Smuzhiyun end_time = datetime.utcfromtimestamp(float(val)) 419*4882a593Smuzhiyun elif key.startswith('IO '): 420*4882a593Smuzhiyun split = key.split() 421*4882a593Smuzhiyun iostat[split[1]] = int(val) 422*4882a593Smuzhiyun elif key.find('rusage') >= 0: 423*4882a593Smuzhiyun split = key.split() 424*4882a593Smuzhiyun ru_key = split[-1] 425*4882a593Smuzhiyun if ru_key in ('ru_stime', 'ru_utime'): 426*4882a593Smuzhiyun val = float(val) 427*4882a593Smuzhiyun else: 428*4882a593Smuzhiyun val = int(val) 429*4882a593Smuzhiyun rusage[ru_key] = rusage.get(ru_key, 0) + val 430*4882a593Smuzhiyun elif key == 'Status': 431*4882a593Smuzhiyun bs_json['status'] = val 432*4882a593Smuzhiyun bs_json['elapsed_time'] = end_time - start_time 433*4882a593Smuzhiyun bs_json['rusage'] = rusage 434*4882a593Smuzhiyun bs_json['iostat'] = iostat 435*4882a593Smuzhiyun return bs_json 436*4882a593Smuzhiyun 437*4882a593Smuzhiyun log.info('Saving buildstats in JSON format') 438*4882a593Smuzhiyun bs_dirs = sorted(os.listdir(self.bb_vars['BUILDSTATS_BASE'])) 439*4882a593Smuzhiyun if len(bs_dirs) > 1: 440*4882a593Smuzhiyun log.warning("Multiple buildstats found for test %s, only " 441*4882a593Smuzhiyun "archiving the last one", self.name) 442*4882a593Smuzhiyun bs_dir = os.path.join(self.bb_vars['BUILDSTATS_BASE'], bs_dirs[-1]) 443*4882a593Smuzhiyun 444*4882a593Smuzhiyun buildstats = [] 445*4882a593Smuzhiyun for fname in os.listdir(bs_dir): 446*4882a593Smuzhiyun recipe_dir = os.path.join(bs_dir, fname) 447*4882a593Smuzhiyun if not os.path.isdir(recipe_dir): 448*4882a593Smuzhiyun continue 449*4882a593Smuzhiyun name, epoch, version, revision = split_nevr(fname) 450*4882a593Smuzhiyun recipe_bs = OrderedDict((('name', name), 451*4882a593Smuzhiyun ('epoch', epoch), 452*4882a593Smuzhiyun ('version', version), 453*4882a593Smuzhiyun ('revision', revision), 454*4882a593Smuzhiyun ('tasks', OrderedDict()))) 455*4882a593Smuzhiyun for task in os.listdir(recipe_dir): 456*4882a593Smuzhiyun recipe_bs['tasks'][task] = bs_to_json(os.path.join(recipe_dir, 457*4882a593Smuzhiyun task)) 458*4882a593Smuzhiyun buildstats.append(recipe_bs) 459*4882a593Smuzhiyun 460*4882a593Smuzhiyun self.buildstats[measurement_name] = buildstats 461*4882a593Smuzhiyun 462*4882a593Smuzhiyun def rm_tmp(self): 463*4882a593Smuzhiyun """Cleanup temporary/intermediate files and directories""" 464*4882a593Smuzhiyun log.debug("Removing temporary and cache files") 465*4882a593Smuzhiyun for name in ['bitbake.lock', 'cache/sanity_info', 466*4882a593Smuzhiyun self.bb_vars['TMPDIR']]: 467*4882a593Smuzhiyun oe.path.remove(name, recurse=True) 468*4882a593Smuzhiyun 469*4882a593Smuzhiyun def rm_sstate(self): 470*4882a593Smuzhiyun """Remove sstate directory""" 471*4882a593Smuzhiyun log.debug("Removing sstate-cache") 472*4882a593Smuzhiyun oe.path.remove(self.bb_vars['SSTATE_DIR'], recurse=True) 473*4882a593Smuzhiyun 474*4882a593Smuzhiyun def rm_cache(self): 475*4882a593Smuzhiyun """Drop bitbake caches""" 476*4882a593Smuzhiyun oe.path.remove(self.bb_vars['PERSISTENT_DIR'], recurse=True) 477*4882a593Smuzhiyun 478*4882a593Smuzhiyun @staticmethod 479*4882a593Smuzhiyun def sync(): 480*4882a593Smuzhiyun """Sync and drop kernel caches""" 481*4882a593Smuzhiyun runCmd2('bitbake -m', ignore_status=True) 482*4882a593Smuzhiyun log.debug("Syncing and dropping kernel caches""") 483*4882a593Smuzhiyun KernelDropCaches.drop() 484*4882a593Smuzhiyun os.sync() 485*4882a593Smuzhiyun # Wait a bit for all the dirty blocks to be written onto disk 486*4882a593Smuzhiyun time.sleep(3) 487*4882a593Smuzhiyun 488*4882a593Smuzhiyun 489*4882a593Smuzhiyunclass BuildPerfTestLoader(unittest.TestLoader): 490*4882a593Smuzhiyun """Test loader for build performance tests""" 491*4882a593Smuzhiyun sortTestMethodsUsing = None 492*4882a593Smuzhiyun 493*4882a593Smuzhiyun 494*4882a593Smuzhiyunclass BuildPerfTestRunner(unittest.TextTestRunner): 495*4882a593Smuzhiyun """Test loader for build performance tests""" 496*4882a593Smuzhiyun sortTestMethodsUsing = None 497*4882a593Smuzhiyun 498*4882a593Smuzhiyun def __init__(self, out_dir, *args, **kwargs): 499*4882a593Smuzhiyun super(BuildPerfTestRunner, self).__init__(*args, **kwargs) 500*4882a593Smuzhiyun self.out_dir = out_dir 501*4882a593Smuzhiyun 502*4882a593Smuzhiyun def _makeResult(self): 503*4882a593Smuzhiyun return BuildPerfTestResult(self.out_dir, self.stream, self.descriptions, 504*4882a593Smuzhiyun self.verbosity) 505