Home | History | Annotate | Download | only in runtest
      1 #!/usr/bin/env python
      2 #
      3 # Copyright (C) 2015 The Android Open Source Project
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License");
      6 # you may not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 #      http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS IS" BASIS,
     13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 #
     17 """Simpleperf runtest runner: run simpleperf runtests on host or on device.
     18 
     19 For a simpleperf runtest like one_function test, it contains following steps:
     20 1. Run simpleperf record command to record simpleperf_runtest_one_function's
     21    running samples, which is generated in perf.data.
     22 2. Run simpleperf report command to parse perf.data, generate perf.report.
     23 4. Parse perf.report and see if it matches expectation.
     24 
     25 The information of all runtests is stored in runtest.conf.
     26 """
     27 
     28 import os
     29 import os.path
     30 import re
     31 import subprocess
     32 import sys
     33 import xml.etree.ElementTree as ET
     34 
     35 
     36 class CallTreeNode(object):
     37 
     38   def __init__(self, name):
     39     self.name = name
     40     self.children = []
     41 
     42   def add_child(self, child):
     43     self.children.append(child)
     44 
     45   def __str__(self):
     46     return 'CallTreeNode:\n' + '\n'.join(self._dump(1))
     47 
     48   def _dump(self, indent):
     49     indent_str = '  ' * indent
     50     strs = [indent_str + self.name]
     51     for child in self.children:
     52       strs.extend(child._dump(indent + 1))
     53     return strs
     54 
     55 
     56 class Symbol(object):
     57 
     58   def __init__(self, name, comm, overhead, children_overhead):
     59     self.name = name
     60     self.comm = comm
     61     self.overhead = overhead
     62     # children_overhead is the overhead sum of this symbol and functions
     63     # called by this symbol.
     64     self.children_overhead = children_overhead
     65     self.call_tree = None
     66 
     67   def set_call_tree(self, call_tree):
     68     self.call_tree = call_tree
     69 
     70   def __str__(self):
     71     strs = []
     72     strs.append('Symbol name=%s comm=%s overhead=%f children_overhead=%f' % (
     73         self.name, self.comm, self.overhead, self.children_overhead))
     74     if self.call_tree:
     75       strs.append('\t%s' % self.call_tree)
     76     return '\n'.join(strs)
     77 
     78 
     79 class SymbolOverheadRequirement(object):
     80 
     81   def __init__(self, symbol_name=None, comm=None, min_overhead=None,
     82                max_overhead=None):
     83     self.symbol_name = symbol_name
     84     self.comm = comm
     85     self.min_overhead = min_overhead
     86     self.max_overhead = max_overhead
     87 
     88   def __str__(self):
     89     strs = []
     90     strs.append('SymbolOverheadRequirement')
     91     if self.symbol_name is not None:
     92       strs.append('symbol_name=%s' % self.symbol_name)
     93     if self.comm is not None:
     94       strs.append('comm=%s' % self.comm)
     95     if self.min_overhead is not None:
     96       strs.append('min_overhead=%f' % self.min_overhead)
     97     if self.max_overhead is not None:
     98       strs.append('max_overhead=%f' % self.max_overhead)
     99     return ' '.join(strs)
    100 
    101   def is_match(self, symbol):
    102     if self.symbol_name is not None:
    103       if self.symbol_name != symbol.name:
    104         return False
    105     if self.comm is not None:
    106       if self.comm != symbol.comm:
    107         return False
    108     return True
    109 
    110   def check_overhead(self, overhead):
    111     if self.min_overhead is not None:
    112       if self.min_overhead > overhead:
    113         return False
    114     if self.max_overhead is not None:
    115       if self.max_overhead < overhead:
    116         return False
    117     return True
    118 
    119 
    120 class SymbolRelationRequirement(object):
    121 
    122   def __init__(self, symbol_name, comm=None):
    123     self.symbol_name = symbol_name
    124     self.comm = comm
    125     self.children = []
    126 
    127   def add_child(self, child):
    128     self.children.append(child)
    129 
    130   def __str__(self):
    131     return 'SymbolRelationRequirement:\n' + '\n'.join(self._dump(1))
    132 
    133   def _dump(self, indent):
    134     indent_str = '  ' * indent
    135     strs = [indent_str + self.symbol_name +
    136             (' ' + self.comm if self.comm else '')]
    137     for child in self.children:
    138       strs.extend(child._dump(indent + 1))
    139     return strs
    140 
    141   def is_match(self, symbol):
    142     if symbol.name != self.symbol_name:
    143       return False
    144     if self.comm is not None:
    145       if symbol.comm != self.comm:
    146         return False
    147     return True
    148 
    149   def check_relation(self, call_tree):
    150     if not call_tree:
    151       return False
    152     if self.symbol_name != call_tree.name:
    153       return False
    154     for child in self.children:
    155       child_matched = False
    156       for node in call_tree.children:
    157         if child.check_relation(node):
    158           child_matched = True
    159           break
    160       if not child_matched:
    161         return False
    162     return True
    163 
    164 
    165 class Test(object):
    166 
    167   def __init__(
    168           self,
    169           test_name,
    170           executable_name,
    171           report_options,
    172           symbol_overhead_requirements,
    173           symbol_children_overhead_requirements,
    174           symbol_relation_requirements):
    175     self.test_name = test_name
    176     self.executable_name = executable_name
    177     self.report_options = report_options
    178     self.symbol_overhead_requirements = symbol_overhead_requirements
    179     self.symbol_children_overhead_requirements = (
    180         symbol_children_overhead_requirements)
    181     self.symbol_relation_requirements = symbol_relation_requirements
    182 
    183   def __str__(self):
    184     strs = []
    185     strs.append('Test test_name=%s' % self.test_name)
    186     strs.append('\texecutable_name=%s' % self.executable_name)
    187     strs.append('\treport_options=%s' % (' '.join(self.report_options)))
    188     strs.append('\tsymbol_overhead_requirements:')
    189     for req in self.symbol_overhead_requirements:
    190       strs.append('\t\t%s' % req)
    191     strs.append('\tsymbol_children_overhead_requirements:')
    192     for req in self.symbol_children_overhead_requirements:
    193       strs.append('\t\t%s' % req)
    194     strs.append('\tsymbol_relation_requirements:')
    195     for req in self.symbol_relation_requirements:
    196       strs.append('\t\t%s' % req)
    197     return '\n'.join(strs)
    198 
    199 
    200 def load_config_file(config_file):
    201   tests = []
    202   tree = ET.parse(config_file)
    203   root = tree.getroot()
    204   assert root.tag == 'runtests'
    205   for test in root:
    206     assert test.tag == 'test'
    207     test_name = test.attrib['name']
    208     executable_name = None
    209     report_options = []
    210     symbol_overhead_requirements = []
    211     symbol_children_overhead_requirements = []
    212     symbol_relation_requirements = []
    213     for test_item in test:
    214       if test_item.tag == 'executable':
    215         executable_name = test_item.attrib['name']
    216       elif test_item.tag == 'report':
    217         report_options = test_item.attrib['option'].split()
    218       elif (test_item.tag == 'symbol_overhead' or
    219               test_item.tag == 'symbol_children_overhead'):
    220         for symbol_item in test_item:
    221           assert symbol_item.tag == 'symbol'
    222           symbol_name = None
    223           if 'name' in symbol_item.attrib:
    224             symbol_name = symbol_item.attrib['name']
    225           comm = None
    226           if 'comm' in symbol_item.attrib:
    227             comm = symbol_item.attrib['comm']
    228           overhead_min = None
    229           if 'min' in symbol_item.attrib:
    230             overhead_min = float(symbol_item.attrib['min'])
    231           overhead_max = None
    232           if 'max' in symbol_item.attrib:
    233             overhead_max = float(symbol_item.attrib['max'])
    234 
    235           if test_item.tag == 'symbol_overhead':
    236             symbol_overhead_requirements.append(
    237                 SymbolOverheadRequirement(
    238                     symbol_name,
    239                     comm,
    240                     overhead_min,
    241                     overhead_max)
    242             )
    243           else:
    244             symbol_children_overhead_requirements.append(
    245                 SymbolOverheadRequirement(
    246                     symbol_name,
    247                     comm,
    248                     overhead_min,
    249                     overhead_max))
    250       elif test_item.tag == 'symbol_callgraph_relation':
    251         for symbol_item in test_item:
    252           req = load_symbol_relation_requirement(symbol_item)
    253           symbol_relation_requirements.append(req)
    254 
    255     tests.append(
    256         Test(
    257             test_name,
    258             executable_name,
    259             report_options,
    260             symbol_overhead_requirements,
    261             symbol_children_overhead_requirements,
    262             symbol_relation_requirements))
    263   return tests
    264 
    265 
    266 def load_symbol_relation_requirement(symbol_item):
    267   symbol_name = symbol_item.attrib['name']
    268   comm = None
    269   if 'comm' in symbol_item.attrib:
    270     comm = symbol_item.attrib['comm']
    271   req = SymbolRelationRequirement(symbol_name, comm)
    272   for item in symbol_item:
    273     child_req = load_symbol_relation_requirement(item)
    274     req.add_child(child_req)
    275   return req
    276 
    277 
    278 class Runner(object):
    279 
    280   def __init__(self, target, perf_path):
    281     self.target = target
    282     self.is32 = target.endswith('32')
    283     self.perf_path = perf_path
    284     self.use_callgraph = False
    285     self.sampler = 'cpu-cycles'
    286 
    287   def record(self, test_executable_name, record_file, additional_options=[]):
    288     call_args = [self.perf_path, 'record']
    289     call_args += ['--duration', '2']
    290     call_args += ['-e', '%s:u' % self.sampler]
    291     if self.use_callgraph:
    292       call_args += ['-f', '1000', '-g']
    293     call_args += ['-o', record_file]
    294     call_args += additional_options
    295     test_executable_name += '32' if self.is32 else '64'
    296     call_args += [test_executable_name]
    297     self._call(call_args)
    298 
    299   def report(self, record_file, report_file, additional_options=[]):
    300     call_args = [self.perf_path, 'report']
    301     call_args += ['-i', record_file]
    302     if self.use_callgraph:
    303       call_args += ['-g', 'callee']
    304     call_args += additional_options
    305     self._call(call_args, report_file)
    306 
    307   def _call(self, args, output_file=None):
    308     pass
    309 
    310 
    311 class HostRunner(Runner):
    312 
    313   """Run perf test on host."""
    314 
    315   def __init__(self, target):
    316     perf_path = 'simpleperf32' if target.endswith('32') else 'simpleperf'
    317     super(HostRunner, self).__init__(target, perf_path)
    318 
    319   def _call(self, args, output_file=None):
    320     output_fh = None
    321     if output_file is not None:
    322       output_fh = open(output_file, 'w')
    323     subprocess.check_call(args, stdout=output_fh)
    324     if output_fh is not None:
    325       output_fh.close()
    326 
    327 
    328 class DeviceRunner(Runner):
    329 
    330   """Run perf test on device."""
    331 
    332   def __init__(self, target):
    333     self.tmpdir = '/data/local/tmp/'
    334     perf_path = 'simpleperf32' if target.endswith('32') else 'simpleperf'
    335     super(DeviceRunner, self).__init__(target, self.tmpdir + perf_path)
    336     self._download(os.environ['OUT'] + '/system/xbin/' + perf_path, self.tmpdir)
    337     lib = 'lib' if self.is32 else 'lib64'
    338     self._download(os.environ['OUT'] + '/system/' + lib + '/libsimpleperf_inplace_sampler.so',
    339                    self.tmpdir)
    340 
    341   def _call(self, args, output_file=None):
    342     output_fh = None
    343     if output_file is not None:
    344       output_fh = open(output_file, 'w')
    345     args_with_adb = ['adb', 'shell']
    346     args_with_adb.append('export LD_LIBRARY_PATH=' + self.tmpdir + ' && ' + ' '.join(args))
    347     subprocess.check_call(args_with_adb, stdout=output_fh)
    348     if output_fh is not None:
    349       output_fh.close()
    350 
    351   def _download(self, file, to_dir):
    352     args = ['adb', 'push', file, to_dir]
    353     subprocess.check_call(args)
    354 
    355   def record(self, test_executable_name, record_file, additional_options=[]):
    356     self._download(os.environ['OUT'] + '/system/bin/' + test_executable_name +
    357                    ('32' if self.is32 else '64'), self.tmpdir)
    358     super(DeviceRunner, self).record(self.tmpdir + test_executable_name,
    359                                      self.tmpdir + record_file,
    360                                      additional_options)
    361 
    362   def report(self, record_file, report_file, additional_options=[]):
    363     super(DeviceRunner, self).report(self.tmpdir + record_file,
    364                                      report_file,
    365                                      additional_options)
    366 
    367 class ReportAnalyzer(object):
    368 
    369   """Check if perf.report matches expectation in Configuration."""
    370 
    371   def _read_report_file(self, report_file, has_callgraph):
    372     fh = open(report_file, 'r')
    373     lines = fh.readlines()
    374     fh.close()
    375 
    376     lines = [x.rstrip() for x in lines]
    377     blank_line_index = -1
    378     for i in range(len(lines)):
    379       if not lines[i]:
    380         blank_line_index = i
    381     assert blank_line_index != -1
    382     assert blank_line_index + 1 < len(lines)
    383     title_line = lines[blank_line_index + 1]
    384     report_item_lines = lines[blank_line_index + 2:]
    385 
    386     if has_callgraph:
    387       assert re.search(r'^Children\s+Self\s+Command.+Symbol$', title_line)
    388     else:
    389       assert re.search(r'^Overhead\s+Command.+Symbol$', title_line)
    390 
    391     return self._parse_report_items(report_item_lines, has_callgraph)
    392 
    393   def _parse_report_items(self, lines, has_callgraph):
    394     symbols = []
    395     cur_symbol = None
    396     call_tree_stack = {}
    397     vertical_columns = []
    398     last_node = None
    399     last_depth = -1
    400 
    401     for line in lines:
    402       if not line:
    403         continue
    404       if not line[0].isspace():
    405         if has_callgraph:
    406           m = re.search(r'^([\d\.]+)%\s+([\d\.]+)%\s+(\S+).*\s+(\S+)$', line)
    407           children_overhead = float(m.group(1))
    408           overhead = float(m.group(2))
    409           comm = m.group(3)
    410           symbol_name = m.group(4)
    411           cur_symbol = Symbol(symbol_name, comm, overhead, children_overhead)
    412           symbols.append(cur_symbol)
    413         else:
    414           m = re.search(r'^([\d\.]+)%\s+(\S+).*\s+(\S+)$', line)
    415           overhead = float(m.group(1))
    416           comm = m.group(2)
    417           symbol_name = m.group(3)
    418           cur_symbol = Symbol(symbol_name, comm, overhead, 0)
    419           symbols.append(cur_symbol)
    420         # Each report item can have different column depths.
    421         vertical_columns = []
    422       else:
    423         for i in range(len(line)):
    424           if line[i] == '|':
    425             if not vertical_columns or vertical_columns[-1] < i:
    426               vertical_columns.append(i)
    427 
    428         if not line.strip('| \t'):
    429           continue
    430         if line.find('-') == -1:
    431           function_name = line.strip('| \t')
    432           node = CallTreeNode(function_name)
    433           last_node.add_child(node)
    434           last_node = node
    435           call_tree_stack[last_depth] = node
    436         else:
    437           pos = line.find('-')
    438           depth = -1
    439           for i in range(len(vertical_columns)):
    440             if pos >= vertical_columns[i]:
    441               depth = i
    442           assert depth != -1
    443 
    444           line = line.strip('|- \t')
    445           m = re.search(r'^[\d\.]+%[-\s]+(.+)$', line)
    446           if m:
    447             function_name = m.group(1)
    448           else:
    449             function_name = line
    450 
    451           node = CallTreeNode(function_name)
    452           if depth == 0:
    453             cur_symbol.set_call_tree(node)
    454 
    455           else:
    456             call_tree_stack[depth - 1].add_child(node)
    457           call_tree_stack[depth] = node
    458           last_node = node
    459           last_depth = depth
    460 
    461     return symbols
    462 
    463   def check_report_file(self, test, report_file, has_callgraph):
    464     symbols = self._read_report_file(report_file, has_callgraph)
    465     if not self._check_symbol_overhead_requirements(test, symbols):
    466       return False
    467     if has_callgraph:
    468       if not self._check_symbol_children_overhead_requirements(test, symbols):
    469         return False
    470       if not self._check_symbol_relation_requirements(test, symbols):
    471         return False
    472     return True
    473 
    474   def _check_symbol_overhead_requirements(self, test, symbols):
    475     result = True
    476     matched = [False] * len(test.symbol_overhead_requirements)
    477     matched_overhead = [0] * len(test.symbol_overhead_requirements)
    478     for symbol in symbols:
    479       for i in range(len(test.symbol_overhead_requirements)):
    480         req = test.symbol_overhead_requirements[i]
    481         if req.is_match(symbol):
    482           matched[i] = True
    483           matched_overhead[i] += symbol.overhead
    484     for i in range(len(matched)):
    485       if not matched[i]:
    486         print 'requirement (%s) has no matched symbol in test %s' % (
    487             test.symbol_overhead_requirements[i], test)
    488         result = False
    489       else:
    490         fulfilled = req.check_overhead(matched_overhead[i])
    491         if not fulfilled:
    492           print "Symbol (%s) doesn't match requirement (%s) in test %s" % (
    493               symbol, req, test)
    494           result = False
    495     return result
    496 
    497   def _check_symbol_children_overhead_requirements(self, test, symbols):
    498     result = True
    499     matched = [False] * len(test.symbol_children_overhead_requirements)
    500     for symbol in symbols:
    501       for i in range(len(test.symbol_children_overhead_requirements)):
    502         req = test.symbol_children_overhead_requirements[i]
    503         if req.is_match(symbol):
    504           matched[i] = True
    505           fulfilled = req.check_overhead(symbol.children_overhead)
    506           if not fulfilled:
    507             print "Symbol (%s) doesn't match requirement (%s) in test %s" % (
    508                 symbol, req, test)
    509             result = False
    510     for i in range(len(matched)):
    511       if not matched[i]:
    512         print 'requirement (%s) has no matched symbol in test %s' % (
    513             test.symbol_children_overhead_requirements[i], test)
    514         result = False
    515     return result
    516 
    517   def _check_symbol_relation_requirements(self, test, symbols):
    518     result = True
    519     matched = [False] * len(test.symbol_relation_requirements)
    520     for symbol in symbols:
    521       for i in range(len(test.symbol_relation_requirements)):
    522         req = test.symbol_relation_requirements[i]
    523         if req.is_match(symbol):
    524           matched[i] = True
    525           fulfilled = req.check_relation(symbol.call_tree)
    526           if not fulfilled:
    527             print "Symbol (%s) doesn't match requirement (%s) in test %s" % (
    528                 symbol, req, test)
    529             result = False
    530     for i in range(len(matched)):
    531       if not matched[i]:
    532         print 'requirement (%s) has no matched symbol in test %s' % (
    533             test.symbol_relation_requirements[i], test)
    534         result = False
    535     return result
    536 
    537 
    538 def build_runner(target, use_callgraph, sampler):
    539   if target == 'host32' and use_callgraph:
    540     print "Current 64bit linux host doesn't support `simpleperf32 record -g`"
    541     return None
    542   if target.startswith('host'):
    543     runner = HostRunner(target)
    544   else:
    545     runner = DeviceRunner(target)
    546   runner.use_callgraph = use_callgraph
    547   runner.sampler = sampler
    548   return runner
    549 
    550 
    551 def test_with_runner(runner, tests):
    552   report_analyzer = ReportAnalyzer()
    553   for test in tests:
    554     runner.record(test.executable_name, 'perf.data')
    555     if runner.sampler == 'inplace-sampler':
    556       # TODO: fix this when inplace-sampler actually works.
    557       runner.report('perf.data', 'perf.report')
    558       symbols = report_analyzer._read_report_file('perf.report', runner.use_callgraph)
    559       result = False
    560       if len(symbols) == 1 and symbols[0].name.find('FakeFunction()') != -1:
    561         result = True
    562     else:
    563       runner.report('perf.data', 'perf.report', additional_options = test.report_options)
    564       result = report_analyzer.check_report_file(test, 'perf.report', runner.use_callgraph)
    565     str = 'test %s on %s ' % (test.test_name, runner.target)
    566     if runner.use_callgraph:
    567       str += 'with call graph '
    568     str += 'using %s ' % runner.sampler
    569     str += ' Succeeded' if result else 'Failed'
    570     print str
    571     if not result:
    572       exit(1)
    573 
    574 
    575 def runtest(target_options, use_callgraph_options, sampler_options, selected_tests):
    576   tests = load_config_file(os.path.dirname(os.path.realpath(__file__)) + \
    577                            '/runtest.conf')
    578   if selected_tests is not None:
    579     new_tests = []
    580     for test in tests:
    581       if test.test_name in selected_tests:
    582         new_tests.append(test)
    583     tests = new_tests
    584   for target in target_options:
    585     for use_callgraph in use_callgraph_options:
    586       for sampler in sampler_options:
    587         runner = build_runner(target, use_callgraph, sampler)
    588         if runner is not None:
    589           test_with_runner(runner, tests)
    590 
    591 
    592 def main():
    593   target_options = ['host64', 'host32', 'device64', 'device32']
    594   use_callgraph_options = [False, True]
    595   sampler_options = ['cpu-cycles', 'inplace-sampler']
    596   selected_tests = None
    597   i = 1
    598   while i < len(sys.argv):
    599     if sys.argv[i] == '--host':
    600       target_options = ['host64', 'host32']
    601     elif sys.argv[i] == '--device':
    602       target_options = ['device64', 'device32']
    603     elif sys.argv[i] == '--normal':
    604       use_callgraph_options = [False]
    605     elif sys.argv[i] == '--callgraph':
    606       use_callgraph_options = [True]
    607     elif sys.argv[i] == '--no-inplace-sampler':
    608       sampler_options = ['cpu-cycles']
    609     elif sys.argv[i] == '--inplace-sampler':
    610       sampler_options = ['inplace-sampler']
    611     elif sys.argv[i] == '--test':
    612       if i < len(sys.argv):
    613         i += 1
    614         for test in sys.argv[i].split(','):
    615           if selected_tests is None:
    616             selected_tests = {}
    617           selected_tests[test] = True
    618     i += 1
    619   runtest(target_options, use_callgraph_options, sampler_options, selected_tests)
    620 
    621 if __name__ == '__main__':
    622   main()
    623