xref: /OK3568_Linux_fs/yocto/poky/meta/lib/oeqa/buildperf/base.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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