1#!/usr/bin/env python3 2# Copyright 2014 the V8 project authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6""" 7Performance runner for d8. 8 9Call e.g. with tools/run-perf.py --arch ia32 some_suite.json 10 11The suite json format is expected to be: 12{ 13 "path": <relative path chunks to perf resources and main file>, 14 "owners": [<list of email addresses of benchmark owners (required)>], 15 "name": <optional suite name, file name is default>, 16 "archs": [<architecture name for which this suite is run>, ...], 17 "binary": <name of binary to run, default "d8">, 18 "flags": [<flag to d8>, ...], 19 "test_flags": [<flag to the test file>, ...], 20 "run_count": <how often will this suite run (optional)>, 21 "run_count_XXX": <how often will this suite run for arch XXX (optional)>, 22 "timeout": <how long test is allowed to run>, 23 "timeout_XXX": <how long test is allowed run run for arch XXX>, 24 "retry_count": <how many times to retry failures (in addition to first try)", 25 "retry_count_XXX": <how many times to retry failures for arch XXX> 26 "resources": [<js file to be moved to android device>, ...] 27 "main": <main js perf runner file>, 28 "results_regexp": <optional regexp>, 29 "results_processor": <optional python results processor script>, 30 "units": <the unit specification for the performance dashboard>, 31 "process_size": <flag - collect maximum memory used by the process>, 32 "tests": [ 33 { 34 "name": <name of the trace>, 35 "results_regexp": <optional more specific regexp>, 36 "results_processor": <optional python results processor script>, 37 "units": <the unit specification for the performance dashboard>, 38 "process_size": <flag - collect maximum memory used by the process>, 39 }, ... 40 ] 41} 42 43The tests field can also nest other suites in arbitrary depth. A suite 44with a "main" file is a leaf suite that can contain one more level of 45tests. 46 47A suite's results_regexp is expected to have one string place holder 48"%s" for the trace name. A trace's results_regexp overwrites suite 49defaults. 50 51A suite's results_processor may point to an optional python script. If 52specified, it is called after running the tests (with a path relative to the 53suite level's path). It is expected to read the measurement's output text 54on stdin and print the processed output to stdout. 55 56The results_regexp will be applied to the processed output. 57 58A suite without "tests" is considered a performance test itself. 59 60Full example (suite with one runner): 61{ 62 "path": ["."], 63 "owners": ["username@chromium.org"], 64 "flags": ["--expose-gc"], 65 "test_flags": ["5"], 66 "archs": ["ia32", "x64"], 67 "run_count": 5, 68 "run_count_ia32": 3, 69 "main": "run.js", 70 "results_regexp": "^%s: (.+)$", 71 "units": "score", 72 "tests": [ 73 {"name": "Richards"}, 74 {"name": "DeltaBlue"}, 75 {"name": "NavierStokes", 76 "results_regexp": "^NavierStokes: (.+)$"} 77 ] 78} 79 80Full example (suite with several runners): 81{ 82 "path": ["."], 83 "owners": ["username@chromium.org", "otherowner@google.com"], 84 "flags": ["--expose-gc"], 85 "archs": ["ia32", "x64"], 86 "run_count": 5, 87 "units": "score", 88 "tests": [ 89 {"name": "Richards", 90 "path": ["richards"], 91 "main": "run.js", 92 "run_count": 3, 93 "results_regexp": "^Richards: (.+)$"}, 94 {"name": "NavierStokes", 95 "path": ["navier_stokes"], 96 "main": "run.js", 97 "results_regexp": "^NavierStokes: (.+)$"} 98 ] 99} 100 101Path pieces are concatenated. D8 is always run with the suite's path as cwd. 102 103The test flags are passed to the js test file after '--'. 104""" 105 106from collections import OrderedDict 107from math import sqrt 108from statistics import mean, stdev 109import copy 110import json 111import logging 112import math 113import argparse 114import os 115import re 116import subprocess 117import sys 118import time 119import traceback 120 121from testrunner.local import android 122from testrunner.local import command 123from testrunner.local import utils 124from testrunner.objects.output import Output, NULL_OUTPUT 125 126 127SUPPORTED_ARCHS = ['arm', 128 'ia32', 129 'mips', 130 'mipsel', 131 'x64', 132 'arm64', 133 'riscv64'] 134 135GENERIC_RESULTS_RE = re.compile(r'^RESULT ([^:]+): ([^=]+)= ([^ ]+) ([^ ]*)$') 136RESULT_STDDEV_RE = re.compile(r'^\{([^\}]+)\}$') 137RESULT_LIST_RE = re.compile(r'^\[([^\]]+)\]$') 138TOOLS_BASE = os.path.abspath(os.path.dirname(__file__)) 139INFRA_FAILURE_RETCODE = 87 140MIN_RUNS_FOR_CONFIDENCE = 10 141 142 143def GeometricMean(values): 144 """Returns the geometric mean of a list of values. 145 146 The mean is calculated using log to avoid overflow. 147 """ 148 values = list(map(float, values)) 149 return math.exp(sum(map(math.log, values)) / len(values)) 150 151 152class ResultTracker(object): 153 """Class that tracks trace/runnable results and produces script output. 154 155 The output is structured like this: 156 { 157 "traces": [ 158 { 159 "graphs": ["path", "to", "trace", "config"], 160 "units": <string describing units, e.g. "ms" or "KB">, 161 "results": [<list of values measured over several runs>], 162 "stddev": <stddev of the value if measure by script or ''> 163 }, 164 ... 165 ], 166 "runnables": [ 167 { 168 "graphs": ["path", "to", "runnable", "config"], 169 "durations": [<list of durations of each runnable run in seconds>], 170 "timeout": <timeout configured for runnable in seconds>, 171 }, 172 ... 173 ], 174 "errors": [<list of strings describing errors>], 175 } 176 """ 177 def __init__(self): 178 self.traces = {} 179 self.errors = [] 180 self.runnables = {} 181 182 def AddTraceResult(self, trace, result, stddev): 183 if trace.name not in self.traces: 184 self.traces[trace.name] = { 185 'graphs': trace.graphs, 186 'units': trace.units, 187 'results': [result], 188 'stddev': stddev or '', 189 } 190 else: 191 existing_entry = self.traces[trace.name] 192 assert trace.graphs == existing_entry['graphs'] 193 assert trace.units == existing_entry['units'] 194 if stddev: 195 existing_entry['stddev'] = stddev 196 existing_entry['results'].append(result) 197 198 def TraceHasStdDev(self, trace): 199 return trace.name in self.traces and self.traces[trace.name]['stddev'] != '' 200 201 def AddError(self, error): 202 self.errors.append(error) 203 204 def AddRunnableDuration(self, runnable, duration): 205 """Records a duration of a specific run of the runnable.""" 206 if runnable.name not in self.runnables: 207 self.runnables[runnable.name] = { 208 'graphs': runnable.graphs, 209 'durations': [duration], 210 'timeout': runnable.timeout, 211 } 212 else: 213 existing_entry = self.runnables[runnable.name] 214 assert runnable.timeout == existing_entry['timeout'] 215 assert runnable.graphs == existing_entry['graphs'] 216 existing_entry['durations'].append(duration) 217 218 def ToDict(self): 219 return { 220 'traces': list(self.traces.values()), 221 'errors': self.errors, 222 'runnables': list(self.runnables.values()), 223 } 224 225 def WriteToFile(self, file_name): 226 with open(file_name, 'w') as f: 227 f.write(json.dumps(self.ToDict())) 228 229 def HasEnoughRuns(self, graph_config, confidence_level): 230 """Checks if the mean of the results for a given trace config is within 231 0.1% of the true value with the specified confidence level. 232 233 This assumes Gaussian distribution of the noise and based on 234 https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule. 235 236 Args: 237 graph_config: An instance of GraphConfig. 238 confidence_level: Number of standard deviations from the mean that all 239 values must lie within. Typical values are 1, 2 and 3 and correspond 240 to 68%, 95% and 99.7% probability that the measured value is within 241 0.1% of the true value. 242 243 Returns: 244 True if specified confidence level have been achieved. 245 """ 246 if not isinstance(graph_config, TraceConfig): 247 return all(self.HasEnoughRuns(child, confidence_level) 248 for child in graph_config.children) 249 250 trace = self.traces.get(graph_config.name, {}) 251 results = trace.get('results', []) 252 logging.debug('HasEnoughRuns for %s', graph_config.name) 253 254 if len(results) < MIN_RUNS_FOR_CONFIDENCE: 255 logging.debug(' Ran %d times, need at least %d', 256 len(results), MIN_RUNS_FOR_CONFIDENCE) 257 return False 258 259 logging.debug(' Results: %d entries', len(results)) 260 avg = mean(results) 261 avg_stderr = stdev(results) / sqrt(len(results)) 262 logging.debug(' Mean: %.2f, mean_stderr: %.2f', avg, avg_stderr) 263 logging.info('>>> Confidence level is %.2f', 264 avg / max(1000.0 * avg_stderr, .1)) 265 return confidence_level * avg_stderr < avg / 1000.0 266 267 def __str__(self): # pragma: no cover 268 return json.dumps(self.ToDict(), indent=2, separators=(',', ': ')) 269 270 271def RunResultsProcessor(results_processor, output, count): 272 # Dummy pass through for null-runs. 273 if output.stdout is None: 274 return output 275 276 # We assume the results processor is relative to the suite. 277 assert os.path.exists(results_processor) 278 p = subprocess.Popen( 279 [sys.executable, results_processor], 280 stdin=subprocess.PIPE, 281 stdout=subprocess.PIPE, 282 stderr=subprocess.PIPE, 283 ) 284 new_output = copy.copy(output) 285 new_output.stdout = p.communicate( 286 input=output.stdout.encode('utf-8'))[0].decode('utf-8') 287 logging.info('>>> Processed stdout (#%d):\n%s', count, output.stdout) 288 return new_output 289 290 291class Node(object): 292 """Represents a node in the suite tree structure.""" 293 def __init__(self, *args): 294 self._children = [] 295 296 def AppendChild(self, child): 297 self._children.append(child) 298 299 @property 300 def children(self): 301 return self._children 302 303 304class DefaultSentinel(Node): 305 """Fake parent node with all default values.""" 306 def __init__(self, binary = 'd8'): 307 super(DefaultSentinel, self).__init__() 308 self.binary = binary 309 self.run_count = 10 310 self.timeout = 60 311 self.retry_count = 4 312 self.path = [] 313 self.graphs = [] 314 self.flags = [] 315 self.test_flags = [] 316 self.process_size = False 317 self.resources = [] 318 self.results_processor = None 319 self.results_regexp = None 320 self.stddev_regexp = None 321 self.units = 'score' 322 self.total = False 323 self.owners = [] 324 325 326class GraphConfig(Node): 327 """Represents a suite definition. 328 329 Can either be a leaf or an inner node that provides default values. 330 """ 331 def __init__(self, suite, parent, arch): 332 super(GraphConfig, self).__init__() 333 self._suite = suite 334 335 assert isinstance(suite.get('path', []), list) 336 assert isinstance(suite.get('owners', []), list) 337 assert isinstance(suite['name'], str) 338 assert isinstance(suite.get('flags', []), list) 339 assert isinstance(suite.get('test_flags', []), list) 340 assert isinstance(suite.get('resources', []), list) 341 342 # Accumulated values. 343 self.path = parent.path[:] + suite.get('path', []) 344 self.graphs = parent.graphs[:] + [suite['name']] 345 self.flags = parent.flags[:] + suite.get('flags', []) 346 self.test_flags = parent.test_flags[:] + suite.get('test_flags', []) 347 self.owners = parent.owners[:] + suite.get('owners', []) 348 349 # Values independent of parent node. 350 self.resources = suite.get('resources', []) 351 352 # Descrete values (with parent defaults). 353 self.binary = suite.get('binary', parent.binary) 354 self.run_count = suite.get('run_count', parent.run_count) 355 self.run_count = suite.get('run_count_%s' % arch, self.run_count) 356 self.retry_count = suite.get('retry_count', parent.retry_count) 357 self.retry_count = suite.get('retry_count_%s' % arch, self.retry_count) 358 self.timeout = suite.get('timeout', parent.timeout) 359 self.timeout = suite.get('timeout_%s' % arch, self.timeout) 360 self.units = suite.get('units', parent.units) 361 self.total = suite.get('total', parent.total) 362 self.results_processor = suite.get( 363 'results_processor', parent.results_processor) 364 self.process_size = suite.get('process_size', parent.process_size) 365 366 # A regular expression for results. If the parent graph provides a 367 # regexp and the current suite has none, a string place holder for the 368 # suite name is expected. 369 # TODO(machenbach): Currently that makes only sense for the leaf level. 370 # Multiple place holders for multiple levels are not supported. 371 if parent.results_regexp: 372 regexp_default = parent.results_regexp % re.escape(suite['name']) 373 else: 374 regexp_default = None 375 self.results_regexp = suite.get('results_regexp', regexp_default) 376 377 # A similar regular expression for the standard deviation (optional). 378 if parent.stddev_regexp: 379 stddev_default = parent.stddev_regexp % re.escape(suite['name']) 380 else: 381 stddev_default = None 382 self.stddev_regexp = suite.get('stddev_regexp', stddev_default) 383 384 @property 385 def name(self): 386 return '/'.join(self.graphs) 387 388 389class TraceConfig(GraphConfig): 390 """Represents a leaf in the suite tree structure.""" 391 def __init__(self, suite, parent, arch): 392 super(TraceConfig, self).__init__(suite, parent, arch) 393 assert self.results_regexp 394 assert self.owners 395 396 def ConsumeOutput(self, output, result_tracker): 397 """Extracts trace results from the output. 398 399 Args: 400 output: Output object from the test run. 401 result_tracker: Result tracker to be updated. 402 403 Returns: 404 The raw extracted result value or None if an error occurred. 405 """ 406 result = None 407 stddev = None 408 409 try: 410 result = float( 411 re.search(self.results_regexp, output.stdout, re.M).group(1)) 412 except ValueError: 413 result_tracker.AddError( 414 'Regexp "%s" returned a non-numeric for test %s.' % 415 (self.results_regexp, self.name)) 416 except: 417 result_tracker.AddError( 418 'Regexp "%s" did not match for test %s.' % 419 (self.results_regexp, self.name)) 420 421 try: 422 if self.stddev_regexp: 423 if result_tracker.TraceHasStdDev(self): 424 result_tracker.AddError( 425 'Test %s should only run once since a stddev is provided by the ' 426 'test.' % self.name) 427 stddev = re.search(self.stddev_regexp, output.stdout, re.M).group(1) 428 except: 429 result_tracker.AddError( 430 'Regexp "%s" did not match for test %s.' % 431 (self.stddev_regexp, self.name)) 432 433 if result: 434 result_tracker.AddTraceResult(self, result, stddev) 435 return result 436 437 438class RunnableConfig(GraphConfig): 439 """Represents a runnable suite definition (i.e. has a main file). 440 """ 441 def __init__(self, suite, parent, arch): 442 super(RunnableConfig, self).__init__(suite, parent, arch) 443 self.arch = arch 444 445 @property 446 def main(self): 447 return self._suite.get('main', '') 448 449 def ChangeCWD(self, suite_path): 450 """Changes the cwd to to path defined in the current graph. 451 452 The tests are supposed to be relative to the suite configuration. 453 """ 454 suite_dir = os.path.abspath(os.path.dirname(suite_path)) 455 bench_dir = os.path.normpath(os.path.join(*self.path)) 456 cwd = os.path.join(suite_dir, bench_dir) 457 logging.debug('Changing CWD to: %s' % cwd) 458 os.chdir(cwd) 459 460 def GetCommandFlags(self, extra_flags=None): 461 suffix = ['--'] + self.test_flags if self.test_flags else [] 462 return self.flags + (extra_flags or []) + [self.main] + suffix 463 464 def GetCommand(self, cmd_prefix, shell_dir, extra_flags=None): 465 # TODO(machenbach): This requires +.exe if run on windows. 466 extra_flags = extra_flags or [] 467 if self.binary != 'd8' and '--prof' in extra_flags: 468 logging.info('Profiler supported only on a benchmark run with d8') 469 470 if self.process_size: 471 cmd_prefix = ['/usr/bin/time', '--format=MaxMemory: %MKB'] + cmd_prefix 472 if self.binary.endswith('.py'): 473 # Copy cmd_prefix instead of update (+=). 474 cmd_prefix = cmd_prefix + [sys.executable] 475 476 return command.Command( 477 cmd_prefix=cmd_prefix, 478 shell=os.path.join(shell_dir, self.binary), 479 args=self.GetCommandFlags(extra_flags=extra_flags), 480 timeout=self.timeout or 60, 481 handle_sigterm=True) 482 483 def ProcessOutput(self, output, result_tracker, count): 484 """Processes test run output and updates result tracker. 485 486 Args: 487 output: Output object from the test run. 488 result_tracker: ResultTracker object to be updated. 489 count: Index of the test run (used for better logging). 490 """ 491 if self.results_processor: 492 output = RunResultsProcessor(self.results_processor, output, count) 493 494 results_for_total = [] 495 for trace in self.children: 496 result = trace.ConsumeOutput(output, result_tracker) 497 if result: 498 results_for_total.append(result) 499 500 if self.total: 501 # Produce total metric only when all traces have produced results. 502 if len(self.children) != len(results_for_total): 503 result_tracker.AddError( 504 'Not all traces have produced results. Can not compute total for ' 505 '%s.' % self.name) 506 return 507 508 # Calculate total as a the geometric mean for results from all traces. 509 total_trace = TraceConfig( 510 {'name': 'Total', 'units': self.children[0].units}, self, self.arch) 511 result_tracker.AddTraceResult( 512 total_trace, GeometricMean(results_for_total), '') 513 514 515class RunnableTraceConfig(TraceConfig, RunnableConfig): 516 """Represents a runnable suite definition that is a leaf.""" 517 def __init__(self, suite, parent, arch): 518 super(RunnableTraceConfig, self).__init__(suite, parent, arch) 519 520 def ProcessOutput(self, output, result_tracker, count): 521 result_tracker.AddRunnableDuration(self, output.duration) 522 self.ConsumeOutput(output, result_tracker) 523 524 525def MakeGraphConfig(suite, arch, parent): 526 """Factory method for making graph configuration objects.""" 527 if isinstance(parent, RunnableConfig): 528 # Below a runnable can only be traces. 529 return TraceConfig(suite, parent, arch) 530 elif suite.get('main') is not None: 531 # A main file makes this graph runnable. Empty strings are accepted. 532 if suite.get('tests'): 533 # This graph has subgraphs (traces). 534 return RunnableConfig(suite, parent, arch) 535 else: 536 # This graph has no subgraphs, it's a leaf. 537 return RunnableTraceConfig(suite, parent, arch) 538 elif suite.get('tests'): 539 # This is neither a leaf nor a runnable. 540 return GraphConfig(suite, parent, arch) 541 else: # pragma: no cover 542 raise Exception('Invalid suite configuration.') 543 544 545def BuildGraphConfigs(suite, arch, parent): 546 """Builds a tree structure of graph objects that corresponds to the suite 547 configuration. 548 """ 549 550 # TODO(machenbach): Implement notion of cpu type? 551 if arch not in suite.get('archs', SUPPORTED_ARCHS): 552 return None 553 554 graph = MakeGraphConfig(suite, arch, parent) 555 for subsuite in suite.get('tests', []): 556 BuildGraphConfigs(subsuite, arch, graph) 557 parent.AppendChild(graph) 558 return graph 559 560 561def FlattenRunnables(node, node_cb): 562 """Generator that traverses the tree structure and iterates over all 563 runnables. 564 """ 565 node_cb(node) 566 if isinstance(node, RunnableConfig): 567 yield node 568 elif isinstance(node, Node): 569 for child in node._children: 570 for result in FlattenRunnables(child, node_cb): 571 yield result 572 else: # pragma: no cover 573 raise Exception('Invalid suite configuration.') 574 575 576def find_build_directory(base_path, arch): 577 """Returns the location of d8 or node in the build output directory. 578 579 This supports a seamless transition between legacy build location 580 (out/Release) and new build location (out/build). 581 """ 582 def is_build(path): 583 # We support d8 or node as executables. We don't support testing on 584 # Windows. 585 return (os.path.isfile(os.path.join(path, 'd8')) or 586 os.path.isfile(os.path.join(path, 'node'))) 587 possible_paths = [ 588 # Location developer wrapper scripts is using. 589 '%s.release' % arch, 590 # Current build location on bots. 591 'build', 592 # Legacy build location on bots. 593 'Release', 594 ] 595 possible_paths = [os.path.join(base_path, p) for p in possible_paths] 596 actual_paths = list(filter(is_build, possible_paths)) 597 assert actual_paths, 'No build directory found.' 598 assert len( 599 actual_paths 600 ) == 1, 'Found ambiguous build directories use --binary-override-path.' 601 return actual_paths[0] 602 603 604class Platform(object): 605 def __init__(self, args): 606 self.shell_dir = args.shell_dir 607 self.shell_dir_secondary = args.shell_dir_secondary 608 self.extra_flags = args.extra_flags.split() 609 self.args = args 610 611 @staticmethod 612 def ReadBuildConfig(args): 613 config_path = os.path.join(args.shell_dir, 'v8_build_config.json') 614 if not os.path.isfile(config_path): 615 return {} 616 with open(config_path) as f: 617 return json.load(f) 618 619 @staticmethod 620 def GetPlatform(args): 621 if Platform.ReadBuildConfig(args).get('is_android', False): 622 return AndroidPlatform(args) 623 else: 624 return DesktopPlatform(args) 625 626 def _Run(self, runnable, count, secondary=False): 627 raise NotImplementedError() # pragma: no cover 628 629 def _LoggedRun(self, runnable, count, secondary=False): 630 suffix = ' - secondary' if secondary else '' 631 title = '>>> %%s (#%d)%s:' % ((count + 1), suffix) 632 try: 633 output = self._Run(runnable, count, secondary) 634 except OSError: 635 logging.exception(title % 'OSError') 636 raise 637 if output.stdout: 638 logging.info(title % 'Stdout' + '\n%s', output.stdout) 639 if output.stderr: # pragma: no cover 640 # Print stderr for debugging. 641 logging.info(title % 'Stderr' + '\n%s', output.stderr) 642 logging.warning('>>> Test timed out after %ss.', runnable.timeout) 643 if output.exit_code != 0: 644 logging.warning('>>> Test crashed with exit code %d.', output.exit_code) 645 return output 646 647 def Run(self, runnable, count, secondary): 648 """Execute the benchmark's main file. 649 650 Args: 651 runnable: A Runnable benchmark instance. 652 count: The number of this (repeated) run. 653 secondary: True if secondary run should be executed. 654 655 Returns: 656 A tuple with the two benchmark outputs. The latter will be NULL_OUTPUT if 657 secondary is False. 658 """ 659 output = self._LoggedRun(runnable, count, secondary=False) 660 if secondary: 661 return output, self._LoggedRun(runnable, count, secondary=True) 662 else: 663 return output, NULL_OUTPUT 664 665 666class DesktopPlatform(Platform): 667 def __init__(self, args): 668 super(DesktopPlatform, self).__init__(args) 669 self.command_prefix = [] 670 671 # Setup command class to OS specific version. 672 command.setup(utils.GuessOS(), args.device) 673 674 if args.prioritize or args.affinitize != None: 675 self.command_prefix = ['schedtool'] 676 if args.prioritize: 677 self.command_prefix += ['-n', '-20'] 678 if args.affinitize != None: 679 # schedtool expects a bit pattern when setting affinity, where each 680 # bit set to '1' corresponds to a core where the process may run on. 681 # First bit corresponds to CPU 0. Since the 'affinitize' parameter is 682 # a core number, we need to map to said bit pattern. 683 cpu = int(args.affinitize) 684 core = 1 << cpu 685 self.command_prefix += ['-a', ('0x%x' % core)] 686 self.command_prefix += ['-e'] 687 688 def PreExecution(self): 689 pass 690 691 def PostExecution(self): 692 pass 693 694 def PreTests(self, node, path): 695 if isinstance(node, RunnableConfig): 696 node.ChangeCWD(path) 697 698 def _Run(self, runnable, count, secondary=False): 699 shell_dir = self.shell_dir_secondary if secondary else self.shell_dir 700 cmd = runnable.GetCommand(self.command_prefix, shell_dir, self.extra_flags) 701 logging.debug('Running command: %s' % cmd) 702 output = cmd.execute() 703 704 if output.IsSuccess() and '--prof' in self.extra_flags: 705 os_prefix = {'linux': 'linux', 'macos': 'mac'}.get(utils.GuessOS()) 706 if os_prefix: 707 tick_tools = os.path.join(TOOLS_BASE, '%s-tick-processor' % os_prefix) 708 subprocess.check_call(tick_tools + ' --only-summary', shell=True) 709 else: # pragma: no cover 710 logging.warning( 711 'Profiler option currently supported on Linux and Mac OS.') 712 713 # /usr/bin/time outputs to stderr 714 if runnable.process_size: 715 output.stdout += output.stderr 716 return output 717 718 719class AndroidPlatform(Platform): # pragma: no cover 720 721 def __init__(self, args): 722 super(AndroidPlatform, self).__init__(args) 723 self.driver = android.android_driver(args.device) 724 725 def PreExecution(self): 726 self.driver.set_high_perf_mode() 727 728 def PostExecution(self): 729 self.driver.set_default_perf_mode() 730 self.driver.tear_down() 731 732 def PreTests(self, node, path): 733 if isinstance(node, RunnableConfig): 734 node.ChangeCWD(path) 735 suite_dir = os.path.abspath(os.path.dirname(path)) 736 if node.path: 737 bench_rel = os.path.normpath(os.path.join(*node.path)) 738 bench_abs = os.path.join(suite_dir, bench_rel) 739 else: 740 bench_rel = '.' 741 bench_abs = suite_dir 742 743 self.driver.push_executable(self.shell_dir, 'bin', node.binary) 744 if self.shell_dir_secondary: 745 self.driver.push_executable( 746 self.shell_dir_secondary, 'bin_secondary', node.binary) 747 748 if isinstance(node, RunnableConfig): 749 self.driver.push_file(bench_abs, node.main, bench_rel) 750 for resource in node.resources: 751 self.driver.push_file(bench_abs, resource, bench_rel) 752 753 def _Run(self, runnable, count, secondary=False): 754 target_dir = 'bin_secondary' if secondary else 'bin' 755 self.driver.drop_ram_caches() 756 757 # Relative path to benchmark directory. 758 if runnable.path: 759 bench_rel = os.path.normpath(os.path.join(*runnable.path)) 760 else: 761 bench_rel = '.' 762 763 logcat_file = None 764 if self.args.dump_logcats_to: 765 runnable_name = '-'.join(runnable.graphs) 766 logcat_file = os.path.join( 767 self.args.dump_logcats_to, 'logcat-%s-#%d%s.log' % ( 768 runnable_name, count + 1, '-secondary' if secondary else '')) 769 logging.debug('Dumping logcat into %s', logcat_file) 770 771 output = Output() 772 start = time.time() 773 try: 774 output.stdout = self.driver.run( 775 target_dir=target_dir, 776 binary=runnable.binary, 777 args=runnable.GetCommandFlags(self.extra_flags), 778 rel_path=bench_rel, 779 timeout=runnable.timeout, 780 logcat_file=logcat_file, 781 ) 782 except android.CommandFailedException as e: 783 output.stdout = e.output 784 output.exit_code = e.status 785 except android.TimeoutException as e: 786 output.stdout = e.output 787 output.timed_out = True 788 if runnable.process_size: 789 output.stdout += 'MaxMemory: Unsupported' 790 output.duration = time.time() - start 791 return output 792 793 794class CustomMachineConfiguration: 795 def __init__(self, disable_aslr = False, governor = None): 796 self.aslr_backup = None 797 self.governor_backup = None 798 self.disable_aslr = disable_aslr 799 self.governor = governor 800 801 def __enter__(self): 802 if self.disable_aslr: 803 self.aslr_backup = CustomMachineConfiguration.GetASLR() 804 CustomMachineConfiguration.SetASLR(0) 805 if self.governor != None: 806 self.governor_backup = CustomMachineConfiguration.GetCPUGovernor() 807 CustomMachineConfiguration.SetCPUGovernor(self.governor) 808 return self 809 810 def __exit__(self, type, value, traceback): 811 if self.aslr_backup != None: 812 CustomMachineConfiguration.SetASLR(self.aslr_backup) 813 if self.governor_backup != None: 814 CustomMachineConfiguration.SetCPUGovernor(self.governor_backup) 815 816 @staticmethod 817 def GetASLR(): 818 try: 819 with open('/proc/sys/kernel/randomize_va_space', 'r') as f: 820 return int(f.readline().strip()) 821 except Exception: 822 logging.exception('Failed to get current ASLR settings.') 823 raise 824 825 @staticmethod 826 def SetASLR(value): 827 try: 828 with open('/proc/sys/kernel/randomize_va_space', 'w') as f: 829 f.write(str(value)) 830 except Exception: 831 logging.exception( 832 'Failed to update ASLR to %s. Are we running under sudo?', value) 833 raise 834 835 new_value = CustomMachineConfiguration.GetASLR() 836 if value != new_value: 837 raise Exception('Present value is %s' % new_value) 838 839 @staticmethod 840 def GetCPUCoresRange(): 841 try: 842 with open('/sys/devices/system/cpu/present', 'r') as f: 843 indexes = f.readline() 844 r = list(map(int, indexes.split('-'))) 845 if len(r) == 1: 846 return list(range(r[0], r[0] + 1)) 847 return list(range(r[0], r[1] + 1)) 848 except Exception: 849 logging.exception('Failed to retrieve number of CPUs.') 850 raise 851 852 @staticmethod 853 def GetCPUPathForId(cpu_index): 854 ret = '/sys/devices/system/cpu/cpu' 855 ret += str(cpu_index) 856 ret += '/cpufreq/scaling_governor' 857 return ret 858 859 @staticmethod 860 def GetCPUGovernor(): 861 try: 862 cpu_indices = CustomMachineConfiguration.GetCPUCoresRange() 863 ret = None 864 for cpu_index in cpu_indices: 865 cpu_device = CustomMachineConfiguration.GetCPUPathForId(cpu_index) 866 with open(cpu_device, 'r') as f: 867 # We assume the governors of all CPUs are set to the same value 868 val = f.readline().strip() 869 if ret == None: 870 ret = val 871 elif ret != val: 872 raise Exception('CPU cores have differing governor settings') 873 return ret 874 except Exception: 875 logging.exception('Failed to get the current CPU governor. Is the CPU ' 876 'governor disabled? Check BIOS.') 877 raise 878 879 @staticmethod 880 def SetCPUGovernor(value): 881 try: 882 cpu_indices = CustomMachineConfiguration.GetCPUCoresRange() 883 for cpu_index in cpu_indices: 884 cpu_device = CustomMachineConfiguration.GetCPUPathForId(cpu_index) 885 with open(cpu_device, 'w') as f: 886 f.write(value) 887 888 except Exception: 889 logging.exception('Failed to change CPU governor to %s. Are we ' 890 'running under sudo?', value) 891 raise 892 893 cur_value = CustomMachineConfiguration.GetCPUGovernor() 894 if cur_value != value: 895 raise Exception('Could not set CPU governor. Present value is %s' 896 % cur_value ) 897 898 899class MaxTotalDurationReachedError(Exception): 900 """Exception used to stop running tests when max total duration is reached.""" 901 pass 902 903 904def Main(argv): 905 parser = argparse.ArgumentParser() 906 parser.add_argument('--arch', 907 help='The architecture to run tests for. Pass "auto" ' 908 'to auto-detect.', default='x64', 909 choices=SUPPORTED_ARCHS + ['auto']) 910 parser.add_argument('--buildbot', 911 help='Deprecated', 912 default=False, action='store_true') 913 parser.add_argument('-d', '--device', 914 help='The device ID to run Android tests on. If not ' 915 'given it will be autodetected.') 916 parser.add_argument('--extra-flags', 917 help='Additional flags to pass to the test executable', 918 default='') 919 parser.add_argument('--json-test-results', 920 help='Path to a file for storing json results.') 921 parser.add_argument('--json-test-results-secondary', 922 help='Path to a file for storing json results from run ' 923 'without patch or for reference build run.') 924 parser.add_argument('--outdir', help='Base directory with compile output', 925 default='out') 926 parser.add_argument('--outdir-secondary', 927 help='Base directory with compile output without patch ' 928 'or for reference build') 929 parser.add_argument('--binary-override-path', 930 help='JavaScript engine binary. By default, d8 under ' 931 'architecture-specific build dir. ' 932 'Not supported in conjunction with outdir-secondary.') 933 parser.add_argument('--prioritize', 934 help='Raise the priority to nice -20 for the ' 935 'benchmarking process.Requires Linux, schedtool, and ' 936 'sudo privileges.', default=False, action='store_true') 937 parser.add_argument('--affinitize', 938 help='Run benchmarking process on the specified core. ' 939 'For example: --affinitize=0 will run the benchmark ' 940 'process on core 0. --affinitize=3 will run the ' 941 'benchmark process on core 3. Requires Linux, schedtool, ' 942 'and sudo privileges.', default=None) 943 parser.add_argument('--noaslr', 944 help='Disable ASLR for the duration of the benchmarked ' 945 'process. Requires Linux and sudo privileges.', 946 default=False, action='store_true') 947 parser.add_argument('--cpu-governor', 948 help='Set cpu governor to specified policy for the ' 949 'duration of the benchmarked process. Typical options: ' 950 '"powersave" for more stable results, or "performance" ' 951 'for shorter completion time of suite, with potentially ' 952 'more noise in results.') 953 parser.add_argument('--filter', 954 help='Only run the benchmarks beginning with this ' 955 'string. For example: ' 956 '--filter=JSTests/TypedArrays/ will run only TypedArray ' 957 'benchmarks from the JSTests suite.', 958 default='') 959 parser.add_argument('--confidence-level', type=float, 960 help='Repeatedly runs each benchmark until specified ' 961 'confidence level is reached. The value is interpreted ' 962 'as the number of standard deviations from the mean that ' 963 'all values must lie within. Typical values are 1, 2 and ' 964 '3 and correspond to 68%%, 95%% and 99.7%% probability ' 965 'that the measured value is within 0.1%% of the true ' 966 'value. Larger values result in more retries and thus ' 967 'longer runtime, but also provide more reliable results. ' 968 'Also see --max-total-duration flag.') 969 parser.add_argument('--max-total-duration', type=int, default=7140, # 1h 59m 970 help='Max total duration in seconds allowed for retries ' 971 'across all tests. This is especially useful in ' 972 'combination with the --confidence-level flag.') 973 parser.add_argument('--dump-logcats-to', 974 help='Writes logcat output from each test into specified ' 975 'directory. Only supported for android targets.') 976 parser.add_argument('--run-count', type=int, default=0, 977 help='Override the run count specified by the test ' 978 'suite. The default 0 uses the suite\'s config.') 979 parser.add_argument('-v', '--verbose', default=False, action='store_true', 980 help='Be verbose and print debug output.') 981 parser.add_argument('suite', nargs='+', help='Path to the suite config file.') 982 983 try: 984 args = parser.parse_args(argv) 985 except SystemExit: 986 return INFRA_FAILURE_RETCODE 987 988 logging.basicConfig( 989 level=logging.DEBUG if args.verbose else logging.INFO, 990 format='%(asctime)s %(levelname)-8s %(message)s') 991 992 if args.arch == 'auto': # pragma: no cover 993 args.arch = utils.DefaultArch() 994 if args.arch not in SUPPORTED_ARCHS: 995 logging.error( 996 'Auto-detected architecture "%s" is not supported.', args.arch) 997 return INFRA_FAILURE_RETCODE 998 999 if (args.json_test_results_secondary and 1000 not args.outdir_secondary): # pragma: no cover 1001 logging.error('For writing secondary json test results, a secondary outdir ' 1002 'patch must be specified.') 1003 return INFRA_FAILURE_RETCODE 1004 1005 workspace = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 1006 1007 if args.binary_override_path == None: 1008 args.shell_dir = find_build_directory( 1009 os.path.join(workspace, args.outdir), args.arch) 1010 default_binary_name = 'd8' 1011 else: 1012 if not os.path.isfile(args.binary_override_path): 1013 logging.error('binary-override-path must be a file name') 1014 return INFRA_FAILURE_RETCODE 1015 if args.outdir_secondary: 1016 logging.error('specify either binary-override-path or outdir-secondary') 1017 return INFRA_FAILURE_RETCODE 1018 args.shell_dir = os.path.abspath( 1019 os.path.dirname(args.binary_override_path)) 1020 default_binary_name = os.path.basename(args.binary_override_path) 1021 1022 if args.outdir_secondary: 1023 args.shell_dir_secondary = find_build_directory( 1024 os.path.join(workspace, args.outdir_secondary), args.arch) 1025 else: 1026 args.shell_dir_secondary = None 1027 1028 if args.json_test_results: 1029 args.json_test_results = os.path.abspath(args.json_test_results) 1030 1031 if args.json_test_results_secondary: 1032 args.json_test_results_secondary = os.path.abspath( 1033 args.json_test_results_secondary) 1034 1035 # Ensure all arguments have absolute path before we start changing current 1036 # directory. 1037 args.suite = list(map(os.path.abspath, args.suite)) 1038 1039 prev_aslr = None 1040 prev_cpu_gov = None 1041 platform = Platform.GetPlatform(args) 1042 1043 result_tracker = ResultTracker() 1044 result_tracker_secondary = ResultTracker() 1045 have_failed_tests = False 1046 with CustomMachineConfiguration(governor = args.cpu_governor, 1047 disable_aslr = args.noaslr) as conf: 1048 for path in args.suite: 1049 if not os.path.exists(path): # pragma: no cover 1050 result_tracker.AddError('Configuration file %s does not exist.' % path) 1051 continue 1052 1053 with open(path) as f: 1054 suite = json.loads(f.read()) 1055 1056 # If no name is given, default to the file name without .json. 1057 suite.setdefault('name', os.path.splitext(os.path.basename(path))[0]) 1058 1059 # Setup things common to one test suite. 1060 platform.PreExecution() 1061 1062 # Build the graph/trace tree structure. 1063 default_parent = DefaultSentinel(default_binary_name) 1064 root = BuildGraphConfigs(suite, args.arch, default_parent) 1065 1066 # Callback to be called on each node on traversal. 1067 def NodeCB(node): 1068 platform.PreTests(node, path) 1069 1070 # Traverse graph/trace tree and iterate over all runnables. 1071 start = time.time() 1072 try: 1073 for runnable in FlattenRunnables(root, NodeCB): 1074 runnable_name = '/'.join(runnable.graphs) 1075 if (not runnable_name.startswith(args.filter) and 1076 runnable_name + '/' != args.filter): 1077 continue 1078 logging.info('>>> Running suite: %s', runnable_name) 1079 1080 def RunGenerator(runnable): 1081 if args.confidence_level: 1082 counter = 0 1083 while not result_tracker.HasEnoughRuns( 1084 runnable, args.confidence_level): 1085 yield counter 1086 counter += 1 1087 else: 1088 for i in range(0, max(1, args.run_count or runnable.run_count)): 1089 yield i 1090 1091 for i in RunGenerator(runnable): 1092 attempts_left = runnable.retry_count + 1 1093 while attempts_left: 1094 total_duration = time.time() - start 1095 if total_duration > args.max_total_duration: 1096 logging.info( 1097 '>>> Stopping now since running for too long (%ds > %ds)', 1098 total_duration, args.max_total_duration) 1099 raise MaxTotalDurationReachedError() 1100 1101 output, output_secondary = platform.Run( 1102 runnable, i, secondary=args.shell_dir_secondary) 1103 result_tracker.AddRunnableDuration(runnable, output.duration) 1104 result_tracker_secondary.AddRunnableDuration( 1105 runnable, output_secondary.duration) 1106 1107 if output.IsSuccess() and output_secondary.IsSuccess(): 1108 runnable.ProcessOutput(output, result_tracker, i) 1109 if output_secondary is not NULL_OUTPUT: 1110 runnable.ProcessOutput( 1111 output_secondary, result_tracker_secondary, i) 1112 break 1113 1114 attempts_left -= 1 1115 if not attempts_left: 1116 logging.info('>>> Suite %s failed after %d retries', 1117 runnable_name, runnable.retry_count + 1) 1118 have_failed_tests = True 1119 else: 1120 logging.info('>>> Retrying suite: %s', runnable_name) 1121 except MaxTotalDurationReachedError: 1122 have_failed_tests = True 1123 1124 platform.PostExecution() 1125 1126 if args.json_test_results: 1127 result_tracker.WriteToFile(args.json_test_results) 1128 else: # pragma: no cover 1129 print('Primary results:', result_tracker) 1130 1131 if args.shell_dir_secondary: 1132 if args.json_test_results_secondary: 1133 result_tracker_secondary.WriteToFile(args.json_test_results_secondary) 1134 else: # pragma: no cover 1135 print('Secondary results:', result_tracker_secondary) 1136 1137 if (result_tracker.errors or result_tracker_secondary.errors or 1138 have_failed_tests): 1139 return 1 1140 1141 return 0 1142 1143 1144def MainWrapper(): 1145 try: 1146 return Main(sys.argv[1:]) 1147 except: 1148 # Log uncaptured exceptions and report infra failure to the caller. 1149 traceback.print_exc() 1150 return INFRA_FAILURE_RETCODE 1151 1152 1153if __name__ == '__main__': # pragma: no cover 1154 sys.exit(MainWrapper()) 1155