3 # Copyright (C) 2015 The Android Open Source Project
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 """Simpleperf runtest runner: run simpleperf runtests on host or on device.
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.
25 The information of all runtests is stored in runtest.conf.
31 import xml.etree.ElementTree as ET
34 class CallTreeNode(object):
36 def __init__(self, name):
40 def add_child(self, child):
41 self.children.append(child)
44 return 'CallTreeNode:\n' + '\n'.join(self._dump(1))
46 def _dump(self, indent):
47 indent_str = ' ' * indent
48 strs = [indent_str + self.name]
49 for child in self.children:
50 strs.extend(child._dump(indent + 1))
56 def __init__(self, name, comm, overhead, children_overhead):
59 self.overhead = overhead
60 # children_overhead is the overhead sum of this symbol and functions
61 # called by this symbol.
62 self.children_overhead = children_overhead
65 def set_call_tree(self, call_tree):
66 self.call_tree = call_tree
70 strs.append('Symbol name=%s comm=%s overhead=%f children_overhead=%f' % (
71 self.name, self.comm, self.overhead, self.children_overhead))
73 strs.append('\t%s' % self.call_tree)
74 return '\n'.join(strs)
77 class SymbolOverheadRequirement(object):
79 def __init__(self, symbol_name=None, comm=None, min_overhead=None,
81 self.symbol_name = symbol_name
83 self.min_overhead = min_overhead
84 self.max_overhead = max_overhead
88 strs.append('SymbolOverheadRequirement')
89 if self.symbol_name is not None:
90 strs.append('symbol_name=%s' % self.symbol_name)
91 if self.comm is not None:
92 strs.append('comm=%s' % self.comm)
93 if self.min_overhead is not None:
94 strs.append('min_overhead=%f' % self.min_overhead)
95 if self.max_overhead is not None:
96 strs.append('max_overhead=%f' % self.max_overhead)
99 def is_match(self, symbol):
100 if self.symbol_name is not None:
101 if self.symbol_name != symbol.name:
103 if self.comm is not None:
104 if self.comm != symbol.comm:
108 def check_overhead(self, overhead):
109 if self.min_overhead is not None:
110 if self.min_overhead > overhead:
112 if self.max_overhead is not None:
113 if self.max_overhead < overhead:
118 class SymbolRelationRequirement(object):
120 def __init__(self, symbol_name, comm=None):
121 self.symbol_name = symbol_name
125 def add_child(self, child):
126 self.children.append(child)
129 return 'SymbolRelationRequirement:\n' + '\n'.join(self._dump(1))
131 def _dump(self, indent):
132 indent_str = ' ' * indent
133 strs = [indent_str + self.symbol_name +
134 (' ' + self.comm if self.comm else '')]
135 for child in self.children:
136 strs.extend(child._dump(indent + 1))
139 def is_match(self, symbol):
140 if symbol.name != self.symbol_name:
142 if self.comm is not None:
143 if symbol.comm != self.comm:
147 def check_relation(self, call_tree):
150 if self.symbol_name != call_tree.name:
152 for child in self.children:
153 child_matched = False
154 for node in call_tree.children:
155 if child.check_relation(node):
158 if not child_matched:
170 symbol_overhead_requirements,
171 symbol_children_overhead_requirements,
172 symbol_relation_requirements):
173 self.test_name = test_name
174 self.executable_name = executable_name
175 self.report_options = report_options
176 self.symbol_overhead_requirements = symbol_overhead_requirements
177 self.symbol_children_overhead_requirements = (
178 symbol_children_overhead_requirements)
179 self.symbol_relation_requirements = symbol_relation_requirements
183 strs.append('Test test_name=%s' % self.test_name)
184 strs.append('\texecutable_name=%s' % self.executable_name)
185 strs.append('\treport_options=%s' % (' '.join(self.report_options)))
186 strs.append('\tsymbol_overhead_requirements:')
187 for req in self.symbol_overhead_requirements:
188 strs.append('\t\t%s' % req)
189 strs.append('\tsymbol_children_overhead_requirements:')
190 for req in self.symbol_children_overhead_requirements:
191 strs.append('\t\t%s' % req)
192 strs.append('\tsymbol_relation_requirements:')
193 for req in self.symbol_relation_requirements:
194 strs.append('\t\t%s' % req)
195 return '\n'.join(strs)
198 def load_config_file(config_file):
200 tree = ET.parse(config_file)
201 root = tree.getroot()
202 assert root.tag == 'runtests'
204 assert test.tag == 'test'
205 test_name = test.attrib['name']
206 executable_name = None
208 symbol_overhead_requirements = []
209 symbol_children_overhead_requirements = []
210 symbol_relation_requirements = []
211 for test_item in test:
212 if test_item.tag == 'executable':
213 executable_name = test_item.attrib['name']
214 elif test_item.tag == 'report':
215 report_options = test_item.attrib['option'].split()
216 elif (test_item.tag == 'symbol_overhead' or
217 test_item.tag == 'symbol_children_overhead'):
218 for symbol_item in test_item:
219 assert symbol_item.tag == 'symbol'
221 if 'name' in symbol_item.attrib:
222 symbol_name = symbol_item.attrib['name']
224 if 'comm' in symbol_item.attrib:
225 comm = symbol_item.attrib['comm']
227 if 'min' in symbol_item.attrib:
228 overhead_min = float(symbol_item.attrib['min'])
230 if 'max' in symbol_item.attrib:
231 overhead_max = float(symbol_item.attrib['max'])
233 if test_item.tag == 'symbol_overhead':
234 symbol_overhead_requirements.append(
235 SymbolOverheadRequirement(
242 symbol_children_overhead_requirements.append(
243 SymbolOverheadRequirement(
248 elif test_item.tag == 'symbol_callgraph_relation':
249 for symbol_item in test_item:
250 req = load_symbol_relation_requirement(symbol_item)
251 symbol_relation_requirements.append(req)
258 symbol_overhead_requirements,
259 symbol_children_overhead_requirements,
260 symbol_relation_requirements))
264 def load_symbol_relation_requirement(symbol_item):
265 symbol_name = symbol_item.attrib['name']
267 if 'comm' in symbol_item.attrib:
268 comm = symbol_item.attrib['comm']
269 req = SymbolRelationRequirement(symbol_name, comm)
270 for item in symbol_item:
271 child_req = load_symbol_relation_requirement(item)
272 req.add_child(child_req)
276 class Runner(object):
278 def __init__(self, perf_path):
279 self.perf_path = perf_path
281 def record(self, test_executable_name, record_file, additional_options=[]):
282 call_args = [self.perf_path,
283 'record'] + additional_options + ['-e',
287 test_executable_name]
288 self._call(call_args)
290 def report(self, record_file, report_file, additional_options=[]):
291 call_args = [self.perf_path,
292 'report'] + additional_options + ['-i',
294 self._call(call_args, report_file)
296 def _call(self, args, output_file=None):
300 class HostRunner(Runner):
302 """Run perf test on host."""
304 def _call(self, args, output_file=None):
306 if output_file is not None:
307 output_fh = open(output_file, 'w')
308 subprocess.check_call(args, stdout=output_fh)
309 if output_fh is not None:
313 class DeviceRunner(Runner):
315 """Run perf test on device."""
317 def _call(self, args, output_file=None):
319 if output_file is not None:
320 output_fh = open(output_file, 'w')
321 args_with_adb = ['adb', 'shell']
322 args_with_adb.extend(args)
323 subprocess.check_call(args_with_adb, stdout=output_fh)
324 if output_fh is not None:
328 class ReportAnalyzer(object):
330 """Check if perf.report matches expectation in Configuration."""
332 def _read_report_file(self, report_file, has_callgraph):
333 fh = open(report_file, 'r')
334 lines = fh.readlines()
337 lines = [x.rstrip() for x in lines]
338 blank_line_index = -1
339 for i in range(len(lines)):
342 assert blank_line_index != -1
343 assert blank_line_index + 1 < len(lines)
344 title_line = lines[blank_line_index + 1]
345 report_item_lines = lines[blank_line_index + 2:]
348 assert re.search(r'^Children\s+Self\s+Command.+Symbol$', title_line)
350 assert re.search(r'^Overhead\s+Command.+Symbol$', title_line)
352 return self._parse_report_items(report_item_lines, has_callgraph)
354 def _parse_report_items(self, lines, has_callgraph):
358 vertical_columns = []
365 if not line[0].isspace():
367 m = re.search(r'^([\d\.]+)%\s+([\d\.]+)%\s+(\S+).*\s+(\S+)$', line)
368 children_overhead = float(m.group(1))
369 overhead = float(m.group(2))
371 symbol_name = m.group(4)
372 cur_symbol = Symbol(symbol_name, comm, overhead, children_overhead)
373 symbols.append(cur_symbol)
375 m = re.search(r'^([\d\.]+)%\s+(\S+).*\s+(\S+)$', line)
376 overhead = float(m.group(1))
378 symbol_name = m.group(3)
379 cur_symbol = Symbol(symbol_name, comm, overhead, 0)
380 symbols.append(cur_symbol)
381 # Each report item can have different column depths.
382 vertical_columns = []
384 for i in range(len(line)):
386 if not vertical_columns or vertical_columns[-1] < i:
387 vertical_columns.append(i)
389 if not line.strip('| \t'):
391 if line.find('-') == -1:
392 function_name = line.strip('| \t')
393 node = CallTreeNode(function_name)
394 last_node.add_child(node)
396 call_tree_stack[last_depth] = node
400 for i in range(len(vertical_columns)):
401 if pos >= vertical_columns[i]:
405 line = line.strip('|- \t')
406 m = re.search(r'^[\d\.]+%[-\s]+(.+)$', line)
408 function_name = m.group(1)
412 node = CallTreeNode(function_name)
414 cur_symbol.set_call_tree(node)
417 call_tree_stack[depth - 1].add_child(node)
418 call_tree_stack[depth] = node
424 def check_report_file(self, test, report_file, has_callgraph):
425 symbols = self._read_report_file(report_file, has_callgraph)
426 if not self._check_symbol_overhead_requirements(test, symbols):
429 if not self._check_symbol_children_overhead_requirements(test, symbols):
431 if not self._check_symbol_relation_requirements(test, symbols):
435 def _check_symbol_overhead_requirements(self, test, symbols):
437 matched = [False] * len(test.symbol_overhead_requirements)
438 matched_overhead = [0] * len(test.symbol_overhead_requirements)
439 for symbol in symbols:
440 for i in range(len(test.symbol_overhead_requirements)):
441 req = test.symbol_overhead_requirements[i]
442 if req.is_match(symbol):
444 matched_overhead[i] += symbol.overhead
445 for i in range(len(matched)):
447 print 'requirement (%s) has no matched symbol in test %s' % (
448 test.symbol_overhead_requirements[i], test)
451 fulfilled = req.check_overhead(matched_overhead[i])
453 print "Symbol (%s) doesn't match requirement (%s) in test %s" % (
458 def _check_symbol_children_overhead_requirements(self, test, symbols):
460 matched = [False] * len(test.symbol_children_overhead_requirements)
461 for symbol in symbols:
462 for i in range(len(test.symbol_children_overhead_requirements)):
463 req = test.symbol_children_overhead_requirements[i]
464 if req.is_match(symbol):
466 fulfilled = req.check_overhead(symbol.children_overhead)
468 print "Symbol (%s) doesn't match requirement (%s) in test %s" % (
471 for i in range(len(matched)):
473 print 'requirement (%s) has no matched symbol in test %s' % (
474 test.symbol_children_overhead_requirements[i], test)
478 def _check_symbol_relation_requirements(self, test, symbols):
480 matched = [False] * len(test.symbol_relation_requirements)
481 for symbol in symbols:
482 for i in range(len(test.symbol_relation_requirements)):
483 req = test.symbol_relation_requirements[i]
484 if req.is_match(symbol):
486 fulfilled = req.check_relation(symbol.call_tree)
488 print "Symbol (%s) doesn't match requirement (%s) in test %s" % (
491 for i in range(len(matched)):
493 print 'requirement (%s) has no matched symbol in test %s' % (
494 test.symbol_relation_requirements[i], test)
499 def runtest(host, device, normal, callgraph, selected_tests):
500 tests = load_config_file('runtest.conf')
501 host_runner = HostRunner('simpleperf')
502 device_runner = DeviceRunner('simpleperf')
503 report_analyzer = ReportAnalyzer()
505 if selected_tests is not None:
506 if test.test_name not in selected_tests:
509 host_runner.record(test.executable_name, 'perf.data')
510 host_runner.report('perf.data', 'perf.report',
511 additional_options = test.report_options)
512 result = report_analyzer.check_report_file(
513 test, 'perf.report', False)
514 print 'test %s on host %s' % (
515 test.test_name, 'Succeeded' if result else 'Failed')
519 if device and normal:
520 device_runner.record(test.executable_name, '/data/perf.data')
521 device_runner.report('/data/perf.data', 'perf.report',
522 additional_options = test.report_options)
523 result = report_analyzer.check_report_file(test, 'perf.report', False)
524 print 'test %s on device %s' % (
525 test.test_name, 'Succeeded' if result else 'Failed')
529 if host and callgraph:
531 test.executable_name,
533 additional_options=['-g'])
537 additional_options=['-g'] + test.report_options)
538 result = report_analyzer.check_report_file(test, 'perf_g.report', True)
539 print 'call-graph test %s on host %s' % (
540 test.test_name, 'Succeeded' if result else 'Failed')
544 if device and callgraph:
545 device_runner.record(
546 test.executable_name,
548 additional_options=['-g'])
549 device_runner.report(
552 additional_options=['-g'] + test.report_options)
553 result = report_analyzer.check_report_file(test, 'perf_g.report', True)
554 print 'call-graph test %s on device %s' % (
555 test.test_name, 'Succeeded' if result else 'Failed')
564 selected_tests = None
566 while i < len(sys.argv):
567 if sys.argv[i] == '--host':
570 elif sys.argv[i] == '--device':
573 elif sys.argv[i] == '--normal':
576 elif sys.argv[i] == '--callgraph':
579 elif sys.argv[i] == '--test':
580 if i < len(sys.argv):
582 for test in sys.argv[i].split(','):
583 if selected_tests is None:
585 selected_tests[test] = True
587 runtest(host, device, normal, callgraph, selected_tests)
589 if __name__ == '__main__':