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.
30 import xml.etree.ElementTree as ET
33 class CallTreeNode(object):
35 def __init__(self, name):
39 def add_child(self, child):
40 self.children.append(child)
43 return 'CallTreeNode:\n' + '\n'.join(self._dump(1))
45 def _dump(self, indent):
46 indent_str = ' ' * indent
47 strs = [indent_str + self.name]
48 for child in self.children:
49 strs.extend(child._dump(indent + 1))
55 def __init__(self, name, comm, overhead, children_overhead):
58 self.overhead = overhead
59 # children_overhead is the overhead sum of this symbol and functions
60 # called by this symbol.
61 self.children_overhead = children_overhead
64 def set_call_tree(self, call_tree):
65 self.call_tree = call_tree
69 strs.append('Symbol name=%s comm=%s overhead=%f children_overhead=%f' % (
70 self.name, self.comm, self.overhead, self.children_overhead))
72 strs.append('\t%s' % self.call_tree)
73 return '\n'.join(strs)
76 class SymbolOverheadRequirement(object):
78 def __init__(self, symbol_name, comm=None, min_overhead=None,
80 self.symbol_name = symbol_name
82 self.min_overhead = min_overhead
83 self.max_overhead = max_overhead
87 strs.append('SymbolOverheadRequirement symbol_name=%s' %
89 if self.comm is not None:
90 strs.append('comm=%s' % self.comm)
91 if self.min_overhead is not None:
92 strs.append('min_overhead=%f' % self.min_overhead)
93 if self.max_overhead is not None:
94 strs.append('max_overhead=%f' % self.max_overhead)
97 def is_match(self, symbol):
98 if symbol.name != self.symbol_name:
100 if self.comm is not None:
101 if self.comm != symbol.comm:
105 def check_overhead(self, overhead):
106 if self.min_overhead is not None:
107 if self.min_overhead > overhead:
109 if self.max_overhead is not None:
110 if self.max_overhead < overhead:
115 class SymbolRelationRequirement(object):
117 def __init__(self, symbol_name, comm=None):
118 self.symbol_name = symbol_name
122 def add_child(self, child):
123 self.children.append(child)
126 return 'SymbolRelationRequirement:\n' + '\n'.join(self._dump(1))
128 def _dump(self, indent):
129 indent_str = ' ' * indent
130 strs = [indent_str + self.symbol_name +
131 (' ' + self.comm if self.comm else '')]
132 for child in self.children:
133 strs.extend(child._dump(indent + 1))
136 def is_match(self, symbol):
137 if symbol.name != self.symbol_name:
139 if self.comm is not None:
140 if symbol.comm != self.comm:
144 def check_relation(self, call_tree):
147 if self.symbol_name != call_tree.name:
149 for child in self.children:
150 child_matched = False
151 for node in call_tree.children:
152 if child.check_relation(node):
155 if not child_matched:
166 symbol_overhead_requirements,
167 symbol_children_overhead_requirements,
168 symbol_relation_requirements):
169 self.test_name = test_name
170 self.executable_name = executable_name
171 self.symbol_overhead_requirements = symbol_overhead_requirements
172 self.symbol_children_overhead_requirements = (
173 symbol_children_overhead_requirements)
174 self.symbol_relation_requirements = symbol_relation_requirements
178 strs.append('Test test_name=%s' % self.test_name)
179 strs.append('\texecutable_name=%s' % self.executable_name)
180 strs.append('\tsymbol_overhead_requirements:')
181 for req in self.symbol_overhead_requirements:
182 strs.append('\t\t%s' % req)
183 strs.append('\tsymbol_children_overhead_requirements:')
184 for req in self.symbol_children_overhead_requirements:
185 strs.append('\t\t%s' % req)
186 strs.append('\tsymbol_relation_requirements:')
187 for req in self.symbol_relation_requirements:
188 strs.append('\t\t%s' % req)
189 return '\n'.join(strs)
192 def load_config_file(config_file):
194 tree = ET.parse(config_file)
195 root = tree.getroot()
196 assert root.tag == 'runtests'
198 assert test.tag == 'test'
199 test_name = test.attrib['name']
200 executable_name = None
201 symbol_overhead_requirements = []
202 symbol_children_overhead_requirements = []
203 symbol_relation_requirements = []
204 for test_item in test:
205 if test_item.tag == 'executable':
206 executable_name = test_item.attrib['name']
207 elif (test_item.tag == 'symbol_overhead' or
208 test_item.tag == 'symbol_children_overhead'):
209 for symbol_item in test_item:
210 assert symbol_item.tag == 'symbol'
211 symbol_name = symbol_item.attrib['name']
213 if 'comm' in symbol_item.attrib:
214 comm = symbol_item.attrib['comm']
216 if 'min' in symbol_item.attrib:
217 overhead_min = float(symbol_item.attrib['min'])
219 if 'max' in symbol_item.attrib:
220 overhead_max = float(symbol_item.attrib['max'])
222 if test_item.tag == 'symbol_overhead':
223 symbol_overhead_requirements.append(
224 SymbolOverheadRequirement(
231 symbol_children_overhead_requirements.append(
232 SymbolOverheadRequirement(
237 elif test_item.tag == 'symbol_callgraph_relation':
238 for symbol_item in test_item:
239 req = load_symbol_relation_requirement(symbol_item)
240 symbol_relation_requirements.append(req)
246 symbol_overhead_requirements,
247 symbol_children_overhead_requirements,
248 symbol_relation_requirements))
252 def load_symbol_relation_requirement(symbol_item):
253 symbol_name = symbol_item.attrib['name']
255 if 'comm' in symbol_item.attrib:
256 comm = symbol_item.attrib['comm']
257 req = SymbolRelationRequirement(symbol_name, comm)
258 for item in symbol_item:
259 child_req = load_symbol_relation_requirement(item)
260 req.add_child(child_req)
264 class Runner(object):
266 def __init__(self, perf_path):
267 self.perf_path = perf_path
269 def record(self, test_executable_name, record_file, additional_options=[]):
270 call_args = [self.perf_path,
271 'record'] + additional_options + ['-e',
275 test_executable_name]
276 self._call(call_args)
278 def report(self, record_file, report_file, additional_options=[]):
279 call_args = [self.perf_path,
280 'report'] + additional_options + ['-i',
282 self._call(call_args, report_file)
284 def _call(self, args, output_file=None):
288 class HostRunner(Runner):
290 """Run perf test on host."""
292 def _call(self, args, output_file=None):
294 if output_file is not None:
295 output_fh = open(output_file, 'w')
296 subprocess.check_call(args, stdout=output_fh)
297 if output_fh is not None:
301 class DeviceRunner(Runner):
303 """Run perf test on device."""
305 def _call(self, args, output_file=None):
307 if output_file is not None:
308 output_fh = open(output_file, 'w')
309 args_with_adb = ['adb', 'shell']
310 args_with_adb.extend(args)
311 subprocess.check_call(args_with_adb, stdout=output_fh)
312 if output_fh is not None:
316 class ReportAnalyzer(object):
318 """Check if perf.report matches expectation in Configuration."""
320 def _read_report_file(self, report_file, has_callgraph):
321 fh = open(report_file, 'r')
322 lines = fh.readlines()
325 lines = [x.rstrip() for x in lines]
326 blank_line_index = -1
327 for i in range(len(lines)):
330 assert blank_line_index != -1
331 assert blank_line_index + 1 < len(lines)
332 title_line = lines[blank_line_index + 1]
333 report_item_lines = lines[blank_line_index + 2:]
336 assert re.search(r'^Children\s+Self\s+Command.+Symbol$', title_line)
338 assert re.search(r'^Overhead\s+Command.+Symbol$', title_line)
340 return self._parse_report_items(report_item_lines, has_callgraph)
342 def _parse_report_items(self, lines, has_callgraph):
346 vertical_columns = []
353 if not line[0].isspace():
355 m = re.search(r'^([\d\.]+)%\s+([\d\.]+)%\s+(\S+).*\s+(\S+)$', line)
356 children_overhead = float(m.group(1))
357 overhead = float(m.group(2))
359 symbol_name = m.group(4)
360 cur_symbol = Symbol(symbol_name, comm, overhead, children_overhead)
361 symbols.append(cur_symbol)
363 m = re.search(r'^([\d\.]+)%\s+(\S+).*\s+(\S+)$', line)
364 overhead = float(m.group(1))
366 symbol_name = m.group(3)
367 cur_symbol = Symbol(symbol_name, comm, overhead, 0)
368 symbols.append(cur_symbol)
369 # Each report item can have different column depths.
370 vertical_columns = []
372 for i in range(len(line)):
374 if not vertical_columns or vertical_columns[-1] < i:
375 vertical_columns.append(i)
377 if not line.strip('| \t'):
379 if line.find('-') == -1:
380 function_name = line.strip('| \t')
381 node = CallTreeNode(function_name)
382 last_node.add_child(node)
384 call_tree_stack[last_depth] = node
388 for i in range(len(vertical_columns)):
389 if pos >= vertical_columns[i]:
393 line = line.strip('|- \t')
394 m = re.search(r'^[\d\.]+%[-\s]+(.+)$', line)
396 function_name = m.group(1)
400 node = CallTreeNode(function_name)
402 cur_symbol.set_call_tree(node)
405 call_tree_stack[depth - 1].add_child(node)
406 call_tree_stack[depth] = node
412 def check_report_file(self, test, report_file, has_callgraph):
413 symbols = self._read_report_file(report_file, has_callgraph)
414 if not self._check_symbol_overhead_requirements(test, symbols):
417 if not self._check_symbol_children_overhead_requirements(test, symbols):
419 if not self._check_symbol_relation_requirements(test, symbols):
423 def _check_symbol_overhead_requirements(self, test, symbols):
425 matched = [False] * len(test.symbol_overhead_requirements)
426 for symbol in symbols:
427 for i in range(len(test.symbol_overhead_requirements)):
428 req = test.symbol_overhead_requirements[i]
429 if req.is_match(symbol):
431 fulfilled = req.check_overhead(symbol.overhead)
433 print "Symbol (%s) doesn't match requirement (%s) in test %s" % (
436 for i in range(len(matched)):
438 print 'requirement (%s) has no matched symbol in test %s' % (
439 test.symbol_overhead_requirements[i], test)
443 def _check_symbol_children_overhead_requirements(self, test, symbols):
445 matched = [False] * len(test.symbol_children_overhead_requirements)
446 for symbol in symbols:
447 for i in range(len(test.symbol_children_overhead_requirements)):
448 req = test.symbol_children_overhead_requirements[i]
449 if req.is_match(symbol):
451 fulfilled = req.check_overhead(symbol.children_overhead)
453 print "Symbol (%s) doesn't match requirement (%s) in test %s" % (
456 for i in range(len(matched)):
458 print 'requirement (%s) has no matched symbol in test %s' % (
459 test.symbol_children_overhead_requirements[i], test)
463 def _check_symbol_relation_requirements(self, test, symbols):
465 matched = [False] * len(test.symbol_relation_requirements)
466 for symbol in symbols:
467 for i in range(len(test.symbol_relation_requirements)):
468 req = test.symbol_relation_requirements[i]
469 if req.is_match(symbol):
471 fulfilled = req.check_relation(symbol.call_tree)
473 print "Symbol (%s) doesn't match requirement (%s) in test %s" % (
476 for i in range(len(matched)):
478 print 'requirement (%s) has no matched symbol in test %s' % (
479 test.symbol_relation_requirements[i], test)
485 tests = load_config_file('runtest.conf')
486 host_runner = HostRunner('simpleperf')
487 device_runner = DeviceRunner('simpleperf')
488 report_analyzer = ReportAnalyzer()
490 host_runner.record(test.executable_name, 'perf.data')
491 host_runner.report('perf.data', 'perf.report')
492 result = report_analyzer.check_report_file(
493 test, 'perf.report', False)
494 print 'test %s on host %s' % (
495 test.test_name, 'Succeeded' if result else 'Failed')
498 device_runner.record(test.executable_name, '/data/perf.data')
499 device_runner.report('/data/perf.data', 'perf.report')
500 result = report_analyzer.check_report_file(test, 'perf.report', False)
501 print 'test %s on device %s' % (
502 test.test_name, 'Succeeded' if result else 'Failed')
507 test.executable_name,
509 additional_options=['-g'])
513 additional_options=['-g'])
514 result = report_analyzer.check_report_file(test, 'perf_g.report', True)
515 print 'call-graph test %s on host %s' % (
516 test.test_name, 'Succeeded' if result else 'Failed')
520 device_runner.record(
521 test.executable_name,
523 additional_options=['-g'])
524 device_runner.report(
527 additional_options=['-g'])
528 result = report_analyzer.check_report_file(test, 'perf_g.report', True)
529 print 'call-graph test %s on device %s' % (
530 test.test_name, 'Succeeded' if result else 'Failed')
535 if __name__ == '__main__':