| #!/usr/bin/env python |
| # |
| # Copyright (C) 2015 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| """Simpleperf runtest runner: run simpleperf runtests on host or on device. |
| |
| For a simpleperf runtest like one_function test, it contains following steps: |
| 1. Run simpleperf record command to record simpleperf_runtest_one_function's |
| running samples, which is generated in perf.data. |
| 2. Run simpleperf report command to parse perf.data, generate perf.report. |
| 4. Parse perf.report and see if it matches expectation. |
| |
| The information of all runtests is stored in runtest.conf. |
| """ |
| |
| import os |
| import os.path |
| import re |
| import subprocess |
| import sys |
| import xml.etree.ElementTree as ET |
| |
| |
| class CallTreeNode(object): |
| |
| def __init__(self, name): |
| self.name = name |
| self.children = [] |
| |
| def add_child(self, child): |
| self.children.append(child) |
| |
| def __str__(self): |
| return 'CallTreeNode:\n' + '\n'.join(self._dump(1)) |
| |
| def _dump(self, indent): |
| indent_str = ' ' * indent |
| strs = [indent_str + self.name] |
| for child in self.children: |
| strs.extend(child._dump(indent + 1)) |
| return strs |
| |
| |
| class Symbol(object): |
| |
| def __init__(self, name, comm, overhead, children_overhead): |
| self.name = name |
| self.comm = comm |
| self.overhead = overhead |
| # children_overhead is the overhead sum of this symbol and functions |
| # called by this symbol. |
| self.children_overhead = children_overhead |
| self.call_tree = None |
| |
| def set_call_tree(self, call_tree): |
| self.call_tree = call_tree |
| |
| def __str__(self): |
| strs = [] |
| strs.append('Symbol name=%s comm=%s overhead=%f children_overhead=%f' % ( |
| self.name, self.comm, self.overhead, self.children_overhead)) |
| if self.call_tree: |
| strs.append('\t%s' % self.call_tree) |
| return '\n'.join(strs) |
| |
| |
| class SymbolOverheadRequirement(object): |
| |
| def __init__(self, symbol_name=None, comm=None, min_overhead=None, |
| max_overhead=None): |
| self.symbol_name = symbol_name |
| self.comm = comm |
| self.min_overhead = min_overhead |
| self.max_overhead = max_overhead |
| |
| def __str__(self): |
| strs = [] |
| strs.append('SymbolOverheadRequirement') |
| if self.symbol_name is not None: |
| strs.append('symbol_name=%s' % self.symbol_name) |
| if self.comm is not None: |
| strs.append('comm=%s' % self.comm) |
| if self.min_overhead is not None: |
| strs.append('min_overhead=%f' % self.min_overhead) |
| if self.max_overhead is not None: |
| strs.append('max_overhead=%f' % self.max_overhead) |
| return ' '.join(strs) |
| |
| def is_match(self, symbol): |
| if self.symbol_name is not None: |
| if self.symbol_name != symbol.name: |
| return False |
| if self.comm is not None: |
| if self.comm != symbol.comm: |
| return False |
| return True |
| |
| def check_overhead(self, overhead): |
| if self.min_overhead is not None: |
| if self.min_overhead > overhead: |
| return False |
| if self.max_overhead is not None: |
| if self.max_overhead < overhead: |
| return False |
| return True |
| |
| |
| class SymbolRelationRequirement(object): |
| |
| def __init__(self, symbol_name, comm=None): |
| self.symbol_name = symbol_name |
| self.comm = comm |
| self.children = [] |
| |
| def add_child(self, child): |
| self.children.append(child) |
| |
| def __str__(self): |
| return 'SymbolRelationRequirement:\n' + '\n'.join(self._dump(1)) |
| |
| def _dump(self, indent): |
| indent_str = ' ' * indent |
| strs = [indent_str + self.symbol_name + |
| (' ' + self.comm if self.comm else '')] |
| for child in self.children: |
| strs.extend(child._dump(indent + 1)) |
| return strs |
| |
| def is_match(self, symbol): |
| if symbol.name != self.symbol_name: |
| return False |
| if self.comm is not None: |
| if symbol.comm != self.comm: |
| return False |
| return True |
| |
| def check_relation(self, call_tree): |
| if not call_tree: |
| return False |
| if self.symbol_name != call_tree.name: |
| return False |
| for child in self.children: |
| child_matched = False |
| for node in call_tree.children: |
| if child.check_relation(node): |
| child_matched = True |
| break |
| if not child_matched: |
| return False |
| return True |
| |
| |
| class Test(object): |
| |
| def __init__( |
| self, |
| test_name, |
| executable_name, |
| disable_host, |
| record_options, |
| report_options, |
| symbol_overhead_requirements, |
| symbol_children_overhead_requirements, |
| symbol_relation_requirements): |
| self.test_name = test_name |
| self.executable_name = executable_name |
| self.disable_host = disable_host |
| self.record_options = record_options |
| self.report_options = report_options |
| self.symbol_overhead_requirements = symbol_overhead_requirements |
| self.symbol_children_overhead_requirements = ( |
| symbol_children_overhead_requirements) |
| self.symbol_relation_requirements = symbol_relation_requirements |
| |
| def __str__(self): |
| strs = [] |
| strs.append('Test test_name=%s' % self.test_name) |
| strs.append('\texecutable_name=%s' % self.executable_name) |
| strs.append('\tdisable_host=%s' % self.disable_host) |
| strs.append('\trecord_options=%s' % (' '.join(self.record_options))) |
| strs.append('\treport_options=%s' % (' '.join(self.report_options))) |
| strs.append('\tsymbol_overhead_requirements:') |
| for req in self.symbol_overhead_requirements: |
| strs.append('\t\t%s' % req) |
| strs.append('\tsymbol_children_overhead_requirements:') |
| for req in self.symbol_children_overhead_requirements: |
| strs.append('\t\t%s' % req) |
| strs.append('\tsymbol_relation_requirements:') |
| for req in self.symbol_relation_requirements: |
| strs.append('\t\t%s' % req) |
| return '\n'.join(strs) |
| |
| |
| def load_config_file(config_file): |
| tests = [] |
| tree = ET.parse(config_file) |
| root = tree.getroot() |
| assert root.tag == 'runtests' |
| for test in root: |
| assert test.tag == 'test' |
| test_name = test.attrib['name'] |
| executable_name = None |
| disable_host = False |
| record_options = [] |
| report_options = [] |
| symbol_overhead_requirements = [] |
| symbol_children_overhead_requirements = [] |
| symbol_relation_requirements = [] |
| for test_item in test: |
| if test_item.tag == 'executable': |
| executable_name = test_item.attrib['name'] |
| elif test_item.tag == 'disable_host': |
| disable_host = True |
| elif test_item.tag == 'record': |
| record_options = test_item.attrib['option'].split() |
| elif test_item.tag == 'report': |
| report_options = test_item.attrib['option'].split() |
| elif (test_item.tag == 'symbol_overhead' or |
| test_item.tag == 'symbol_children_overhead'): |
| for symbol_item in test_item: |
| assert symbol_item.tag == 'symbol' |
| symbol_name = None |
| if 'name' in symbol_item.attrib: |
| symbol_name = symbol_item.attrib['name'] |
| comm = None |
| if 'comm' in symbol_item.attrib: |
| comm = symbol_item.attrib['comm'] |
| overhead_min = None |
| if 'min' in symbol_item.attrib: |
| overhead_min = float(symbol_item.attrib['min']) |
| overhead_max = None |
| if 'max' in symbol_item.attrib: |
| overhead_max = float(symbol_item.attrib['max']) |
| |
| if test_item.tag == 'symbol_overhead': |
| symbol_overhead_requirements.append( |
| SymbolOverheadRequirement( |
| symbol_name, |
| comm, |
| overhead_min, |
| overhead_max) |
| ) |
| else: |
| symbol_children_overhead_requirements.append( |
| SymbolOverheadRequirement( |
| symbol_name, |
| comm, |
| overhead_min, |
| overhead_max)) |
| elif test_item.tag == 'symbol_callgraph_relation': |
| for symbol_item in test_item: |
| req = load_symbol_relation_requirement(symbol_item) |
| symbol_relation_requirements.append(req) |
| |
| tests.append( |
| Test( |
| test_name, |
| executable_name, |
| disable_host, |
| record_options, |
| report_options, |
| symbol_overhead_requirements, |
| symbol_children_overhead_requirements, |
| symbol_relation_requirements)) |
| return tests |
| |
| |
| def load_symbol_relation_requirement(symbol_item): |
| symbol_name = symbol_item.attrib['name'] |
| comm = None |
| if 'comm' in symbol_item.attrib: |
| comm = symbol_item.attrib['comm'] |
| req = SymbolRelationRequirement(symbol_name, comm) |
| for item in symbol_item: |
| child_req = load_symbol_relation_requirement(item) |
| req.add_child(child_req) |
| return req |
| |
| |
| class Runner(object): |
| |
| def __init__(self, target, perf_path): |
| self.target = target |
| self.is32 = target.endswith('32') |
| self.perf_path = perf_path |
| self.use_callgraph = False |
| self.sampler = 'cpu-cycles' |
| |
| def record(self, test_executable_name, record_file, additional_options=[]): |
| call_args = [self.perf_path, 'record'] |
| call_args += ['--duration', '2'] |
| call_args += ['-e', '%s:u' % self.sampler] |
| if self.use_callgraph: |
| call_args += ['-f', '1000', '-g'] |
| call_args += ['-o', record_file] |
| call_args += additional_options |
| test_executable_name += '32' if self.is32 else '64' |
| call_args += [test_executable_name] |
| self._call(call_args) |
| |
| def report(self, record_file, report_file, additional_options=[]): |
| call_args = [self.perf_path, 'report'] |
| call_args += ['-i', record_file] |
| if self.use_callgraph: |
| call_args += ['-g', 'callee'] |
| call_args += additional_options |
| self._call(call_args, report_file) |
| |
| def _call(self, args, output_file=None): |
| pass |
| |
| |
| class HostRunner(Runner): |
| |
| """Run perf test on host.""" |
| |
| def __init__(self, target): |
| perf_path = 'simpleperf32' if target.endswith('32') else 'simpleperf' |
| super(HostRunner, self).__init__(target, perf_path) |
| |
| def _call(self, args, output_file=None): |
| output_fh = None |
| if output_file is not None: |
| output_fh = open(output_file, 'w') |
| subprocess.check_call(args, stdout=output_fh) |
| if output_fh is not None: |
| output_fh.close() |
| |
| |
| class DeviceRunner(Runner): |
| |
| """Run perf test on device.""" |
| |
| def __init__(self, target): |
| self.tmpdir = '/data/local/tmp/' |
| perf_path = 'simpleperf32' if target.endswith('32') else 'simpleperf' |
| super(DeviceRunner, self).__init__(target, self.tmpdir + perf_path) |
| self._download(os.environ['OUT'] + '/system/xbin/' + perf_path, self.tmpdir) |
| |
| def _call(self, args, output_file=None): |
| output_fh = None |
| if output_file is not None: |
| output_fh = open(output_file, 'w') |
| args_with_adb = ['adb', 'shell'] |
| args_with_adb.append('export LD_LIBRARY_PATH=' + self.tmpdir + ' && ' + ' '.join(args)) |
| subprocess.check_call(args_with_adb, stdout=output_fh) |
| if output_fh is not None: |
| output_fh.close() |
| |
| def _download(self, file, to_dir): |
| args = ['adb', 'push', file, to_dir] |
| subprocess.check_call(args) |
| |
| def record(self, test_executable_name, record_file, additional_options=[]): |
| self._download(os.environ['OUT'] + '/system/bin/' + test_executable_name + |
| ('32' if self.is32 else '64'), self.tmpdir) |
| super(DeviceRunner, self).record(self.tmpdir + test_executable_name, |
| self.tmpdir + record_file, |
| additional_options) |
| |
| def report(self, record_file, report_file, additional_options=[]): |
| super(DeviceRunner, self).report(self.tmpdir + record_file, |
| report_file, |
| additional_options) |
| |
| class ReportAnalyzer(object): |
| |
| """Check if perf.report matches expectation in Configuration.""" |
| |
| def _read_report_file(self, report_file, has_callgraph): |
| fh = open(report_file, 'r') |
| lines = fh.readlines() |
| fh.close() |
| |
| lines = [x.rstrip() for x in lines] |
| blank_line_index = -1 |
| for i in range(len(lines)): |
| if not lines[i]: |
| blank_line_index = i |
| assert blank_line_index != -1 |
| assert blank_line_index + 1 < len(lines) |
| title_line = lines[blank_line_index + 1] |
| report_item_lines = lines[blank_line_index + 2:] |
| |
| if has_callgraph: |
| assert re.search(r'^Children\s+Self\s+Command.+Symbol$', title_line) |
| else: |
| assert re.search(r'^Overhead\s+Command.+Symbol$', title_line) |
| |
| return self._parse_report_items(report_item_lines, has_callgraph) |
| |
| def _parse_report_items(self, lines, has_callgraph): |
| symbols = [] |
| cur_symbol = None |
| call_tree_stack = {} |
| vertical_columns = [] |
| last_node = None |
| last_depth = -1 |
| |
| for line in lines: |
| if not line: |
| continue |
| if not line[0].isspace(): |
| if has_callgraph: |
| items = line.split(None, 6) |
| assert len(items) == 7 |
| children_overhead = float(items[0][:-1]) |
| overhead = float(items[1][:-1]) |
| comm = items[2] |
| symbol_name = items[6] |
| cur_symbol = Symbol(symbol_name, comm, overhead, children_overhead) |
| symbols.append(cur_symbol) |
| else: |
| items = line.split(None, 5) |
| assert len(items) == 6 |
| overhead = float(items[0][:-1]) |
| comm = items[1] |
| symbol_name = items[5] |
| cur_symbol = Symbol(symbol_name, comm, overhead, 0) |
| symbols.append(cur_symbol) |
| # Each report item can have different column depths. |
| vertical_columns = [] |
| else: |
| for i in range(len(line)): |
| if line[i] == '|': |
| if not vertical_columns or vertical_columns[-1] < i: |
| vertical_columns.append(i) |
| |
| if not line.strip('| \t'): |
| continue |
| if line.find('-') == -1: |
| function_name = line.strip('| \t') |
| node = CallTreeNode(function_name) |
| last_node.add_child(node) |
| last_node = node |
| call_tree_stack[last_depth] = node |
| else: |
| pos = line.find('-') |
| depth = -1 |
| for i in range(len(vertical_columns)): |
| if pos >= vertical_columns[i]: |
| depth = i |
| assert depth != -1 |
| |
| line = line.strip('|- \t') |
| m = re.search(r'^[\d\.]+%[-\s]+(.+)$', line) |
| if m: |
| function_name = m.group(1) |
| else: |
| function_name = line |
| |
| node = CallTreeNode(function_name) |
| if depth == 0: |
| cur_symbol.set_call_tree(node) |
| |
| else: |
| call_tree_stack[depth - 1].add_child(node) |
| call_tree_stack[depth] = node |
| last_node = node |
| last_depth = depth |
| |
| return symbols |
| |
| def check_report_file(self, test, report_file, has_callgraph): |
| symbols = self._read_report_file(report_file, has_callgraph) |
| if not self._check_symbol_overhead_requirements(test, symbols): |
| return False |
| if has_callgraph: |
| if not self._check_symbol_children_overhead_requirements(test, symbols): |
| return False |
| if not self._check_symbol_relation_requirements(test, symbols): |
| return False |
| return True |
| |
| def _check_symbol_overhead_requirements(self, test, symbols): |
| result = True |
| matched = [False] * len(test.symbol_overhead_requirements) |
| matched_overhead = [0] * len(test.symbol_overhead_requirements) |
| for symbol in symbols: |
| for i in range(len(test.symbol_overhead_requirements)): |
| req = test.symbol_overhead_requirements[i] |
| if req.is_match(symbol): |
| matched[i] = True |
| matched_overhead[i] += symbol.overhead |
| for i in range(len(matched)): |
| if not matched[i]: |
| print 'requirement (%s) has no matched symbol in test %s' % ( |
| test.symbol_overhead_requirements[i], test) |
| result = False |
| else: |
| fulfilled = req.check_overhead(matched_overhead[i]) |
| if not fulfilled: |
| print "Symbol (%s) doesn't match requirement (%s) in test %s" % ( |
| symbol, req, test) |
| result = False |
| return result |
| |
| def _check_symbol_children_overhead_requirements(self, test, symbols): |
| result = True |
| matched = [False] * len(test.symbol_children_overhead_requirements) |
| for symbol in symbols: |
| for i in range(len(test.symbol_children_overhead_requirements)): |
| req = test.symbol_children_overhead_requirements[i] |
| if req.is_match(symbol): |
| matched[i] = True |
| fulfilled = req.check_overhead(symbol.children_overhead) |
| if not fulfilled: |
| print "Symbol (%s) doesn't match requirement (%s) in test %s" % ( |
| symbol, req, test) |
| result = False |
| for i in range(len(matched)): |
| if not matched[i]: |
| print 'requirement (%s) has no matched symbol in test %s' % ( |
| test.symbol_children_overhead_requirements[i], test) |
| result = False |
| return result |
| |
| def _check_symbol_relation_requirements(self, test, symbols): |
| result = True |
| matched = [False] * len(test.symbol_relation_requirements) |
| for symbol in symbols: |
| for i in range(len(test.symbol_relation_requirements)): |
| req = test.symbol_relation_requirements[i] |
| if req.is_match(symbol): |
| matched[i] = True |
| fulfilled = req.check_relation(symbol.call_tree) |
| if not fulfilled: |
| print "Symbol (%s) doesn't match requirement (%s) in test %s" % ( |
| symbol, req, test) |
| result = False |
| for i in range(len(matched)): |
| if not matched[i]: |
| print 'requirement (%s) has no matched symbol in test %s' % ( |
| test.symbol_relation_requirements[i], test) |
| result = False |
| return result |
| |
| |
| def build_runner(target, use_callgraph, sampler): |
| if target == 'host32' and use_callgraph: |
| print "Current 64bit linux host doesn't support `simpleperf32 record -g`" |
| return None |
| if target.startswith('host'): |
| runner = HostRunner(target) |
| else: |
| runner = DeviceRunner(target) |
| runner.use_callgraph = use_callgraph |
| runner.sampler = sampler |
| return runner |
| |
| |
| def test_with_runner(runner, tests): |
| report_analyzer = ReportAnalyzer() |
| for test in tests: |
| if test.disable_host and runner.target.startswith('host'): |
| print('Skip test %s on %s' % (test.test_name, runner.target)) |
| continue |
| runner.record(test.executable_name, 'perf.data', additional_options = test.record_options) |
| runner.report('perf.data', 'perf.report', additional_options = test.report_options) |
| result = report_analyzer.check_report_file(test, 'perf.report', runner.use_callgraph) |
| str = 'test %s on %s ' % (test.test_name, runner.target) |
| if runner.use_callgraph: |
| str += 'with call graph ' |
| str += 'using %s ' % runner.sampler |
| str += ' Succeeded' if result else 'Failed' |
| print str |
| if not result: |
| exit(1) |
| |
| |
| def runtest(target_options, use_callgraph_options, sampler_options, selected_tests): |
| tests = load_config_file(os.path.dirname(os.path.realpath(__file__)) + \ |
| '/runtest.conf') |
| if selected_tests is not None: |
| new_tests = [] |
| for test in tests: |
| if test.test_name in selected_tests: |
| new_tests.append(test) |
| tests = new_tests |
| for target in target_options: |
| for use_callgraph in use_callgraph_options: |
| for sampler in sampler_options: |
| runner = build_runner(target, use_callgraph, sampler) |
| if runner is not None: |
| test_with_runner(runner, tests) |
| |
| |
| def main(): |
| target_options = ['host64', 'host32', 'device64', 'device32'] |
| use_callgraph_options = [False, True] |
| sampler_options = ['cpu-cycles'] |
| selected_tests = None |
| i = 1 |
| while i < len(sys.argv): |
| if sys.argv[i] == '--host': |
| target_options = ['host64', 'host32'] |
| elif sys.argv[i] == '--device': |
| target_options = ['device64', 'device32'] |
| elif sys.argv[i] == '--normal': |
| use_callgraph_options = [False] |
| elif sys.argv[i] == '--callgraph': |
| use_callgraph_options = [True] |
| elif sys.argv[i] == '--test': |
| if i < len(sys.argv): |
| i += 1 |
| for test in sys.argv[i].split(','): |
| if selected_tests is None: |
| selected_tests = {} |
| selected_tests[test] = True |
| i += 1 |
| runtest(target_options, use_callgraph_options, sampler_options, selected_tests) |
| |
| if __name__ == '__main__': |
| main() |