| #!/usr/bin/env python |
| # Copyright 2014 the V8 project authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """ |
| Performance runner for d8. |
| |
| Call e.g. with tools/run-perf.py --arch ia32 some_suite.json |
| |
| The suite json format is expected to be: |
| { |
| "path": <relative path chunks to perf resources and main file>, |
| "name": <optional suite name, file name is default>, |
| "archs": [<architecture name for which this suite is run>, ...], |
| "binary": <name of binary to run, default "d8">, |
| "flags": [<flag to d8>, ...], |
| "test_flags": [<flag to the test file>, ...], |
| "run_count": <how often will this suite run (optional)>, |
| "run_count_XXX": <how often will this suite run for arch XXX (optional)>, |
| "resources": [<js file to be moved to android device>, ...] |
| "main": <main js perf runner file>, |
| "results_regexp": <optional regexp>, |
| "results_processor": <optional python results processor script>, |
| "units": <the unit specification for the performance dashboard>, |
| "process_size": <flag - collect maximum memory used by the process>, |
| "tests": [ |
| { |
| "name": <name of the trace>, |
| "results_regexp": <optional more specific regexp>, |
| "results_processor": <optional python results processor script>, |
| "units": <the unit specification for the performance dashboard>, |
| "process_size": <flag - collect maximum memory used by the process>, |
| }, ... |
| ] |
| } |
| |
| The tests field can also nest other suites in arbitrary depth. A suite |
| with a "main" file is a leaf suite that can contain one more level of |
| tests. |
| |
| A suite's results_regexp is expected to have one string place holder |
| "%s" for the trace name. A trace's results_regexp overwrites suite |
| defaults. |
| |
| A suite's results_processor may point to an optional python script. If |
| specified, it is called after running the tests (with a path relative to the |
| suite level's path). It is expected to read the measurement's output text |
| on stdin and print the processed output to stdout. |
| |
| The results_regexp will be applied to the processed output. |
| |
| A suite without "tests" is considered a performance test itself. |
| |
| Full example (suite with one runner): |
| { |
| "path": ["."], |
| "flags": ["--expose-gc"], |
| "test_flags": ["5"], |
| "archs": ["ia32", "x64"], |
| "run_count": 5, |
| "run_count_ia32": 3, |
| "main": "run.js", |
| "results_regexp": "^%s: (.+)$", |
| "units": "score", |
| "tests": [ |
| {"name": "Richards"}, |
| {"name": "DeltaBlue"}, |
| {"name": "NavierStokes", |
| "results_regexp": "^NavierStokes: (.+)$"} |
| ] |
| } |
| |
| Full example (suite with several runners): |
| { |
| "path": ["."], |
| "flags": ["--expose-gc"], |
| "archs": ["ia32", "x64"], |
| "run_count": 5, |
| "units": "score", |
| "tests": [ |
| {"name": "Richards", |
| "path": ["richards"], |
| "main": "run.js", |
| "run_count": 3, |
| "results_regexp": "^Richards: (.+)$"}, |
| {"name": "NavierStokes", |
| "path": ["navier_stokes"], |
| "main": "run.js", |
| "results_regexp": "^NavierStokes: (.+)$"} |
| ] |
| } |
| |
| Path pieces are concatenated. D8 is always run with the suite's path as cwd. |
| |
| The test flags are passed to the js test file after '--'. |
| """ |
| |
| from collections import OrderedDict |
| import json |
| import logging |
| import math |
| import optparse |
| import os |
| import re |
| import subprocess |
| import sys |
| |
| from testrunner.local import command |
| from testrunner.local import utils |
| |
| ARCH_GUESS = utils.DefaultArch() |
| SUPPORTED_ARCHS = ["arm", |
| "ia32", |
| "mips", |
| "mipsel", |
| "x64", |
| "arm64"] |
| |
| GENERIC_RESULTS_RE = re.compile(r"^RESULT ([^:]+): ([^=]+)= ([^ ]+) ([^ ]*)$") |
| RESULT_STDDEV_RE = re.compile(r"^\{([^\}]+)\}$") |
| RESULT_LIST_RE = re.compile(r"^\[([^\]]+)\]$") |
| TOOLS_BASE = os.path.abspath(os.path.dirname(__file__)) |
| |
| |
| def LoadAndroidBuildTools(path): # pragma: no cover |
| assert os.path.exists(path) |
| sys.path.insert(0, path) |
| |
| import devil_chromium |
| from devil.android import device_errors # pylint: disable=import-error |
| from devil.android import device_utils # pylint: disable=import-error |
| from devil.android.sdk import adb_wrapper # pylint: disable=import-error |
| from devil.android.perf import cache_control # pylint: disable=import-error |
| from devil.android.perf import perf_control # pylint: disable=import-error |
| global adb_wrapper |
| global cache_control |
| global device_errors |
| global device_utils |
| global perf_control |
| |
| devil_chromium.Initialize() |
| |
| |
| def GeometricMean(values): |
| """Returns the geometric mean of a list of values. |
| |
| The mean is calculated using log to avoid overflow. |
| """ |
| values = map(float, values) |
| return str(math.exp(sum(map(math.log, values)) / len(values))) |
| |
| |
| class Results(object): |
| """Place holder for result traces.""" |
| def __init__(self, traces=None, errors=None): |
| self.traces = traces or [] |
| self.errors = errors or [] |
| |
| def ToDict(self): |
| return {"traces": self.traces, "errors": self.errors} |
| |
| def WriteToFile(self, file_name): |
| with open(file_name, "w") as f: |
| f.write(json.dumps(self.ToDict())) |
| |
| def __add__(self, other): |
| self.traces += other.traces |
| self.errors += other.errors |
| return self |
| |
| def __str__(self): # pragma: no cover |
| return str(self.ToDict()) |
| |
| |
| class Measurement(object): |
| """Represents a series of results of one trace. |
| |
| The results are from repetitive runs of the same executable. They are |
| gathered by repeated calls to ConsumeOutput. |
| """ |
| def __init__(self, graphs, units, results_regexp, stddev_regexp): |
| self.name = '/'.join(graphs) |
| self.graphs = graphs |
| self.units = units |
| self.results_regexp = results_regexp |
| self.stddev_regexp = stddev_regexp |
| self.results = [] |
| self.errors = [] |
| self.stddev = "" |
| self.process_size = False |
| |
| def ConsumeOutput(self, stdout): |
| try: |
| result = re.search(self.results_regexp, stdout, re.M).group(1) |
| self.results.append(str(float(result))) |
| except ValueError: |
| self.errors.append("Regexp \"%s\" returned a non-numeric for test %s." |
| % (self.results_regexp, self.name)) |
| except: |
| self.errors.append("Regexp \"%s\" didn't match for test %s." |
| % (self.results_regexp, self.name)) |
| |
| try: |
| if self.stddev_regexp and self.stddev: |
| self.errors.append("Test %s should only run once since a stddev " |
| "is provided by the test." % self.name) |
| if self.stddev_regexp: |
| self.stddev = re.search(self.stddev_regexp, stdout, re.M).group(1) |
| except: |
| self.errors.append("Regexp \"%s\" didn't match for test %s." |
| % (self.stddev_regexp, self.name)) |
| |
| def GetResults(self): |
| return Results([{ |
| "graphs": self.graphs, |
| "units": self.units, |
| "results": self.results, |
| "stddev": self.stddev, |
| }], self.errors) |
| |
| |
| class NullMeasurement(object): |
| """Null object to avoid having extra logic for configurations that don't |
| require secondary run, e.g. CI bots. |
| """ |
| def ConsumeOutput(self, stdout): |
| pass |
| |
| def GetResults(self): |
| return Results() |
| |
| |
| def Unzip(iterable): |
| left = [] |
| right = [] |
| for l, r in iterable: |
| left.append(l) |
| right.append(r) |
| return lambda: iter(left), lambda: iter(right) |
| |
| |
| def RunResultsProcessor(results_processor, stdout, count): |
| # Dummy pass through for null-runs. |
| if stdout is None: |
| return None |
| |
| # We assume the results processor is relative to the suite. |
| assert os.path.exists(results_processor) |
| p = subprocess.Popen( |
| [sys.executable, results_processor], |
| stdin=subprocess.PIPE, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, |
| ) |
| result, _ = p.communicate(input=stdout) |
| print ">>> Processed stdout (#%d):" % count |
| print result |
| return result |
| |
| |
| def AccumulateResults( |
| graph_names, trace_configs, iter_output, perform_measurement, calc_total): |
| """Iterates over the output of multiple benchmark reruns and accumulates |
| results for a configured list of traces. |
| |
| Args: |
| graph_names: List of names that configure the base path of the traces. E.g. |
| ['v8', 'Octane']. |
| trace_configs: List of "TraceConfig" instances. Each trace config defines |
| how to perform a measurement. |
| iter_output: Iterator over the standard output of each test run. |
| perform_measurement: Whether to actually run tests and perform measurements. |
| This is needed so that we reuse this script for both CI |
| and trybot, but want to ignore second run on CI without |
| having to spread this logic throughout the script. |
| calc_total: Boolean flag to speficy the calculation of a summary trace. |
| Returns: A "Results" object. |
| """ |
| measurements = [ |
| trace.CreateMeasurement(perform_measurement) for trace in trace_configs] |
| for stdout in iter_output(): |
| for measurement in measurements: |
| measurement.ConsumeOutput(stdout) |
| |
| res = reduce(lambda r, m: r + m.GetResults(), measurements, Results()) |
| |
| if not res.traces or not calc_total: |
| return res |
| |
| # Assume all traces have the same structure. |
| if len(set(map(lambda t: len(t["results"]), res.traces))) != 1: |
| res.errors.append("Not all traces have the same number of results.") |
| return res |
| |
| # Calculate the geometric means for all traces. Above we made sure that |
| # there is at least one trace and that the number of results is the same |
| # for each trace. |
| n_results = len(res.traces[0]["results"]) |
| total_results = [GeometricMean(t["results"][i] for t in res.traces) |
| for i in range(0, n_results)] |
| res.traces.append({ |
| "graphs": graph_names + ["Total"], |
| "units": res.traces[0]["units"], |
| "results": total_results, |
| "stddev": "", |
| }) |
| return res |
| |
| |
| def AccumulateGenericResults(graph_names, suite_units, iter_output): |
| """Iterates over the output of multiple benchmark reruns and accumulates |
| generic results. |
| |
| Args: |
| graph_names: List of names that configure the base path of the traces. E.g. |
| ['v8', 'Octane']. |
| suite_units: Measurement default units as defined by the benchmark suite. |
| iter_output: Iterator over the standard output of each test run. |
| Returns: A "Results" object. |
| """ |
| traces = OrderedDict() |
| for stdout in iter_output(): |
| if stdout is None: |
| # The None value is used as a null object to simplify logic. |
| continue |
| for line in stdout.strip().splitlines(): |
| match = GENERIC_RESULTS_RE.match(line) |
| if match: |
| stddev = "" |
| graph = match.group(1) |
| trace = match.group(2) |
| body = match.group(3) |
| units = match.group(4) |
| match_stddev = RESULT_STDDEV_RE.match(body) |
| match_list = RESULT_LIST_RE.match(body) |
| errors = [] |
| if match_stddev: |
| result, stddev = map(str.strip, match_stddev.group(1).split(",")) |
| results = [result] |
| elif match_list: |
| results = map(str.strip, match_list.group(1).split(",")) |
| else: |
| results = [body.strip()] |
| |
| try: |
| results = map(lambda r: str(float(r)), results) |
| except ValueError: |
| results = [] |
| errors = ["Found non-numeric in %s" % |
| "/".join(graph_names + [graph, trace])] |
| |
| trace_result = traces.setdefault(trace, Results([{ |
| "graphs": graph_names + [graph, trace], |
| "units": (units or suite_units).strip(), |
| "results": [], |
| "stddev": "", |
| }], errors)) |
| trace_result.traces[0]["results"].extend(results) |
| trace_result.traces[0]["stddev"] = stddev |
| |
| return reduce(lambda r, t: r + t, traces.itervalues(), Results()) |
| |
| |
| class Node(object): |
| """Represents a node in the suite tree structure.""" |
| def __init__(self, *args): |
| self._children = [] |
| |
| def AppendChild(self, child): |
| self._children.append(child) |
| |
| |
| class DefaultSentinel(Node): |
| """Fake parent node with all default values.""" |
| def __init__(self, binary = "d8"): |
| super(DefaultSentinel, self).__init__() |
| self.binary = binary |
| self.run_count = 10 |
| self.timeout = 60 |
| self.path = [] |
| self.graphs = [] |
| self.flags = [] |
| self.test_flags = [] |
| self.process_size = False |
| self.resources = [] |
| self.results_processor = None |
| self.results_regexp = None |
| self.stddev_regexp = None |
| self.units = "score" |
| self.total = False |
| |
| |
| class GraphConfig(Node): |
| """Represents a suite definition. |
| |
| Can either be a leaf or an inner node that provides default values. |
| """ |
| def __init__(self, suite, parent, arch): |
| super(GraphConfig, self).__init__() |
| self._suite = suite |
| |
| assert isinstance(suite.get("path", []), list) |
| assert isinstance(suite["name"], basestring) |
| assert isinstance(suite.get("flags", []), list) |
| assert isinstance(suite.get("test_flags", []), list) |
| assert isinstance(suite.get("resources", []), list) |
| |
| # Accumulated values. |
| self.path = parent.path[:] + suite.get("path", []) |
| self.graphs = parent.graphs[:] + [suite["name"]] |
| self.flags = parent.flags[:] + suite.get("flags", []) |
| self.test_flags = parent.test_flags[:] + suite.get("test_flags", []) |
| |
| # Values independent of parent node. |
| self.resources = suite.get("resources", []) |
| |
| # Descrete values (with parent defaults). |
| self.binary = suite.get("binary", parent.binary) |
| self.run_count = suite.get("run_count", parent.run_count) |
| self.run_count = suite.get("run_count_%s" % arch, self.run_count) |
| self.timeout = suite.get("timeout", parent.timeout) |
| self.timeout = suite.get("timeout_%s" % arch, self.timeout) |
| self.units = suite.get("units", parent.units) |
| self.total = suite.get("total", parent.total) |
| self.results_processor = suite.get( |
| "results_processor", parent.results_processor) |
| self.process_size = suite.get("process_size", parent.process_size) |
| |
| # A regular expression for results. If the parent graph provides a |
| # regexp and the current suite has none, a string place holder for the |
| # suite name is expected. |
| # TODO(machenbach): Currently that makes only sense for the leaf level. |
| # Multiple place holders for multiple levels are not supported. |
| if parent.results_regexp: |
| regexp_default = parent.results_regexp % re.escape(suite["name"]) |
| else: |
| regexp_default = None |
| self.results_regexp = suite.get("results_regexp", regexp_default) |
| |
| # A similar regular expression for the standard deviation (optional). |
| if parent.stddev_regexp: |
| stddev_default = parent.stddev_regexp % re.escape(suite["name"]) |
| else: |
| stddev_default = None |
| self.stddev_regexp = suite.get("stddev_regexp", stddev_default) |
| |
| |
| class TraceConfig(GraphConfig): |
| """Represents a leaf in the suite tree structure.""" |
| def __init__(self, suite, parent, arch): |
| super(TraceConfig, self).__init__(suite, parent, arch) |
| assert self.results_regexp |
| |
| def CreateMeasurement(self, perform_measurement): |
| if not perform_measurement: |
| return NullMeasurement() |
| |
| return Measurement( |
| self.graphs, |
| self.units, |
| self.results_regexp, |
| self.stddev_regexp, |
| ) |
| |
| |
| class RunnableConfig(GraphConfig): |
| """Represents a runnable suite definition (i.e. has a main file). |
| """ |
| @property |
| def main(self): |
| return self._suite.get("main", "") |
| |
| def PostProcess(self, stdouts_iter): |
| if self.results_processor: |
| def it(): |
| for i, stdout in enumerate(stdouts_iter()): |
| yield RunResultsProcessor(self.results_processor, stdout, i + 1) |
| return it |
| else: |
| return stdouts_iter |
| |
| def ChangeCWD(self, suite_path): |
| """Changes the cwd to to path defined in the current graph. |
| |
| The tests are supposed to be relative to the suite configuration. |
| """ |
| suite_dir = os.path.abspath(os.path.dirname(suite_path)) |
| bench_dir = os.path.normpath(os.path.join(*self.path)) |
| os.chdir(os.path.join(suite_dir, bench_dir)) |
| |
| def GetCommandFlags(self, extra_flags=None): |
| suffix = ["--"] + self.test_flags if self.test_flags else [] |
| return self.flags + (extra_flags or []) + [self.main] + suffix |
| |
| def GetCommand(self, cmd_prefix, shell_dir, extra_flags=None): |
| # TODO(machenbach): This requires +.exe if run on windows. |
| extra_flags = extra_flags or [] |
| if self.binary != 'd8' and '--prof' in extra_flags: |
| print "Profiler supported only on a benchmark run with d8" |
| |
| if self.process_size: |
| cmd_prefix = ["/usr/bin/time", "--format=MaxMemory: %MKB"] + cmd_prefix |
| if self.binary.endswith('.py'): |
| # Copy cmd_prefix instead of update (+=). |
| cmd_prefix = cmd_prefix + [sys.executable] |
| |
| return command.Command( |
| cmd_prefix=cmd_prefix, |
| shell=os.path.join(shell_dir, self.binary), |
| args=self.GetCommandFlags(extra_flags=extra_flags), |
| timeout=self.timeout or 60) |
| |
| def Run(self, runner, trybot): |
| """Iterates over several runs and handles the output for all traces.""" |
| stdout, stdout_secondary = Unzip(runner()) |
| return ( |
| AccumulateResults( |
| self.graphs, |
| self._children, |
| iter_output=self.PostProcess(stdout), |
| perform_measurement=True, |
| calc_total=self.total, |
| ), |
| AccumulateResults( |
| self.graphs, |
| self._children, |
| iter_output=self.PostProcess(stdout_secondary), |
| perform_measurement=trybot, # only run second time on trybots |
| calc_total=self.total, |
| ), |
| ) |
| |
| |
| class RunnableTraceConfig(TraceConfig, RunnableConfig): |
| """Represents a runnable suite definition that is a leaf.""" |
| def __init__(self, suite, parent, arch): |
| super(RunnableTraceConfig, self).__init__(suite, parent, arch) |
| |
| def Run(self, runner, trybot): |
| """Iterates over several runs and handles the output.""" |
| measurement = self.CreateMeasurement(perform_measurement=True) |
| measurement_secondary = self.CreateMeasurement(perform_measurement=trybot) |
| for stdout, stdout_secondary in runner(): |
| measurement.ConsumeOutput(stdout) |
| measurement_secondary.ConsumeOutput(stdout_secondary) |
| return ( |
| measurement.GetResults(), |
| measurement_secondary.GetResults(), |
| ) |
| |
| |
| class RunnableGenericConfig(RunnableConfig): |
| """Represents a runnable suite definition with generic traces.""" |
| def __init__(self, suite, parent, arch): |
| super(RunnableGenericConfig, self).__init__(suite, parent, arch) |
| |
| def Run(self, runner, trybot): |
| stdout, stdout_secondary = Unzip(runner()) |
| return ( |
| AccumulateGenericResults(self.graphs, self.units, stdout), |
| AccumulateGenericResults(self.graphs, self.units, stdout_secondary), |
| ) |
| |
| |
| def MakeGraphConfig(suite, arch, parent): |
| """Factory method for making graph configuration objects.""" |
| if isinstance(parent, RunnableConfig): |
| # Below a runnable can only be traces. |
| return TraceConfig(suite, parent, arch) |
| elif suite.get("main") is not None: |
| # A main file makes this graph runnable. Empty strings are accepted. |
| if suite.get("tests"): |
| # This graph has subgraphs (traces). |
| return RunnableConfig(suite, parent, arch) |
| else: |
| # This graph has no subgraphs, it's a leaf. |
| return RunnableTraceConfig(suite, parent, arch) |
| elif suite.get("generic"): |
| # This is a generic suite definition. It is either a runnable executable |
| # or has a main js file. |
| return RunnableGenericConfig(suite, parent, arch) |
| elif suite.get("tests"): |
| # This is neither a leaf nor a runnable. |
| return GraphConfig(suite, parent, arch) |
| else: # pragma: no cover |
| raise Exception("Invalid suite configuration.") |
| |
| |
| def BuildGraphConfigs(suite, arch, parent): |
| """Builds a tree structure of graph objects that corresponds to the suite |
| configuration. |
| """ |
| |
| # TODO(machenbach): Implement notion of cpu type? |
| if arch not in suite.get("archs", SUPPORTED_ARCHS): |
| return None |
| |
| graph = MakeGraphConfig(suite, arch, parent) |
| for subsuite in suite.get("tests", []): |
| BuildGraphConfigs(subsuite, arch, graph) |
| parent.AppendChild(graph) |
| return graph |
| |
| |
| def FlattenRunnables(node, node_cb): |
| """Generator that traverses the tree structure and iterates over all |
| runnables. |
| """ |
| node_cb(node) |
| if isinstance(node, RunnableConfig): |
| yield node |
| elif isinstance(node, Node): |
| for child in node._children: |
| for result in FlattenRunnables(child, node_cb): |
| yield result |
| else: # pragma: no cover |
| raise Exception("Invalid suite configuration.") |
| |
| |
| class Platform(object): |
| def __init__(self, options): |
| self.shell_dir = options.shell_dir |
| self.shell_dir_secondary = options.shell_dir_secondary |
| self.extra_flags = options.extra_flags.split() |
| |
| @staticmethod |
| def GetPlatform(options): |
| if options.android_build_tools: |
| return AndroidPlatform(options) |
| else: |
| return DesktopPlatform(options) |
| |
| def _Run(self, runnable, count, secondary=False): |
| raise NotImplementedError() # pragma: no cover |
| |
| def Run(self, runnable, count): |
| """Execute the benchmark's main file. |
| |
| If options.shell_dir_secondary is specified, the benchmark is run twice, |
| e.g. with and without patch. |
| Args: |
| runnable: A Runnable benchmark instance. |
| count: The number of this (repeated) run. |
| Returns: A tuple with the two benchmark outputs. The latter will be None if |
| options.shell_dir_secondary was not specified. |
| """ |
| stdout = self._Run(runnable, count, secondary=False) |
| if self.shell_dir_secondary: |
| return stdout, self._Run(runnable, count, secondary=True) |
| else: |
| return stdout, None |
| |
| |
| class DesktopPlatform(Platform): |
| def __init__(self, options): |
| super(DesktopPlatform, self).__init__(options) |
| self.command_prefix = [] |
| |
| if options.prioritize or options.affinitize != None: |
| self.command_prefix = ["schedtool"] |
| if options.prioritize: |
| self.command_prefix += ["-n", "-20"] |
| if options.affinitize != None: |
| # schedtool expects a bit pattern when setting affinity, where each |
| # bit set to '1' corresponds to a core where the process may run on. |
| # First bit corresponds to CPU 0. Since the 'affinitize' parameter is |
| # a core number, we need to map to said bit pattern. |
| cpu = int(options.affinitize) |
| core = 1 << cpu |
| self.command_prefix += ["-a", ("0x%x" % core)] |
| self.command_prefix += ["-e"] |
| |
| def PreExecution(self): |
| pass |
| |
| def PostExecution(self): |
| pass |
| |
| def PreTests(self, node, path): |
| if isinstance(node, RunnableConfig): |
| node.ChangeCWD(path) |
| |
| def _Run(self, runnable, count, secondary=False): |
| suffix = ' - secondary' if secondary else '' |
| shell_dir = self.shell_dir_secondary if secondary else self.shell_dir |
| title = ">>> %%s (#%d)%s:" % ((count + 1), suffix) |
| cmd = runnable.GetCommand(self.command_prefix, shell_dir, self.extra_flags) |
| try: |
| output = cmd.execute() |
| except OSError as e: # pragma: no cover |
| print title % "OSError" |
| print e |
| return "" |
| |
| print title % "Stdout" |
| print output.stdout |
| if output.stderr: # pragma: no cover |
| # Print stderr for debugging. |
| print title % "Stderr" |
| print output.stderr |
| if output.timed_out: |
| print ">>> Test timed out after %ss." % runnable.timeout |
| if '--prof' in self.extra_flags: |
| os_prefix = {"linux": "linux", "macos": "mac"}.get(utils.GuessOS()) |
| if os_prefix: |
| tick_tools = os.path.join(TOOLS_BASE, "%s-tick-processor" % os_prefix) |
| subprocess.check_call(tick_tools + " --only-summary", shell=True) |
| else: # pragma: no cover |
| print "Profiler option currently supported on Linux and Mac OS." |
| |
| # time outputs to stderr |
| if runnable.process_size: |
| return output.stdout + output.stderr |
| return output.stdout |
| |
| |
| class AndroidPlatform(Platform): # pragma: no cover |
| DEVICE_DIR = "/data/local/tmp/v8/" |
| |
| def __init__(self, options): |
| super(AndroidPlatform, self).__init__(options) |
| LoadAndroidBuildTools(options.android_build_tools) |
| |
| if not options.device: |
| # Detect attached device if not specified. |
| devices = adb_wrapper.AdbWrapper.Devices() |
| assert devices and len(devices) == 1, ( |
| "None or multiple devices detected. Please specify the device on " |
| "the command-line with --device") |
| options.device = str(devices[0]) |
| self.adb_wrapper = adb_wrapper.AdbWrapper(options.device) |
| self.device = device_utils.DeviceUtils(self.adb_wrapper) |
| |
| def PreExecution(self): |
| perf = perf_control.PerfControl(self.device) |
| perf.SetHighPerfMode() |
| |
| # Remember what we have already pushed to the device. |
| self.pushed = set() |
| |
| def PostExecution(self): |
| perf = perf_control.PerfControl(self.device) |
| perf.SetDefaultPerfMode() |
| self.device.RemovePath( |
| AndroidPlatform.DEVICE_DIR, force=True, recursive=True) |
| |
| def _PushFile(self, host_dir, file_name, target_rel=".", |
| skip_if_missing=False): |
| file_on_host = os.path.join(host_dir, file_name) |
| file_on_device_tmp = os.path.join( |
| AndroidPlatform.DEVICE_DIR, "_tmp_", file_name) |
| file_on_device = os.path.join( |
| AndroidPlatform.DEVICE_DIR, target_rel, file_name) |
| folder_on_device = os.path.dirname(file_on_device) |
| |
| # Only attempt to push files that exist. |
| if not os.path.exists(file_on_host): |
| if not skip_if_missing: |
| logging.critical('Missing file on host: %s' % file_on_host) |
| return |
| |
| # Only push files not yet pushed in one execution. |
| if file_on_host in self.pushed: |
| return |
| else: |
| self.pushed.add(file_on_host) |
| |
| # Work-around for "text file busy" errors. Push the files to a temporary |
| # location and then copy them with a shell command. |
| output = self.adb_wrapper.Push(file_on_host, file_on_device_tmp) |
| # Success looks like this: "3035 KB/s (12512056 bytes in 4.025s)". |
| # Errors look like this: "failed to copy ... ". |
| if output and not re.search('^[0-9]', output.splitlines()[-1]): |
| logging.critical('PUSH FAILED: ' + output) |
| self.adb_wrapper.Shell("mkdir -p %s" % folder_on_device) |
| self.adb_wrapper.Shell("cp %s %s" % (file_on_device_tmp, file_on_device)) |
| |
| def _PushExecutable(self, shell_dir, target_dir, binary): |
| self._PushFile(shell_dir, binary, target_dir) |
| |
| # Push external startup data. Backwards compatible for revisions where |
| # these files didn't exist. |
| self._PushFile( |
| shell_dir, |
| "natives_blob.bin", |
| target_dir, |
| skip_if_missing=True, |
| ) |
| self._PushFile( |
| shell_dir, |
| "snapshot_blob.bin", |
| target_dir, |
| skip_if_missing=True, |
| ) |
| self._PushFile( |
| shell_dir, |
| "icudtl.dat", |
| target_dir, |
| skip_if_missing=True, |
| ) |
| |
| def PreTests(self, node, path): |
| if isinstance(node, RunnableConfig): |
| node.ChangeCWD(path) |
| suite_dir = os.path.abspath(os.path.dirname(path)) |
| if node.path: |
| bench_rel = os.path.normpath(os.path.join(*node.path)) |
| bench_abs = os.path.join(suite_dir, bench_rel) |
| else: |
| bench_rel = "." |
| bench_abs = suite_dir |
| |
| self._PushExecutable(self.shell_dir, "bin", node.binary) |
| if self.shell_dir_secondary: |
| self._PushExecutable( |
| self.shell_dir_secondary, "bin_secondary", node.binary) |
| |
| if isinstance(node, RunnableConfig): |
| self._PushFile(bench_abs, node.main, bench_rel) |
| for resource in node.resources: |
| self._PushFile(bench_abs, resource, bench_rel) |
| |
| def _Run(self, runnable, count, secondary=False): |
| suffix = ' - secondary' if secondary else '' |
| target_dir = "bin_secondary" if secondary else "bin" |
| title = ">>> %%s (#%d)%s:" % ((count + 1), suffix) |
| cache = cache_control.CacheControl(self.device) |
| cache.DropRamCaches() |
| binary_on_device = os.path.join( |
| AndroidPlatform.DEVICE_DIR, target_dir, runnable.binary) |
| cmd = [binary_on_device] + runnable.GetCommandFlags(self.extra_flags) |
| |
| # Relative path to benchmark directory. |
| if runnable.path: |
| bench_rel = os.path.normpath(os.path.join(*runnable.path)) |
| else: |
| bench_rel = "." |
| |
| try: |
| output = self.device.RunShellCommand( |
| cmd, |
| cwd=os.path.join(AndroidPlatform.DEVICE_DIR, bench_rel), |
| check_return=True, |
| timeout=runnable.timeout, |
| retries=0, |
| ) |
| stdout = "\n".join(output) |
| print title % "Stdout" |
| print stdout |
| except device_errors.CommandTimeoutError: |
| print ">>> Test timed out after %ss." % runnable.timeout |
| stdout = "" |
| if runnable.process_size: |
| return stdout + "MaxMemory: Unsupported" |
| return stdout |
| |
| class CustomMachineConfiguration: |
| def __init__(self, disable_aslr = False, governor = None): |
| self.aslr_backup = None |
| self.governor_backup = None |
| self.disable_aslr = disable_aslr |
| self.governor = governor |
| |
| def __enter__(self): |
| if self.disable_aslr: |
| self.aslr_backup = CustomMachineConfiguration.GetASLR() |
| CustomMachineConfiguration.SetASLR(0) |
| if self.governor != None: |
| self.governor_backup = CustomMachineConfiguration.GetCPUGovernor() |
| CustomMachineConfiguration.SetCPUGovernor(self.governor) |
| return self |
| |
| def __exit__(self, type, value, traceback): |
| if self.aslr_backup != None: |
| CustomMachineConfiguration.SetASLR(self.aslr_backup) |
| if self.governor_backup != None: |
| CustomMachineConfiguration.SetCPUGovernor(self.governor_backup) |
| |
| @staticmethod |
| def GetASLR(): |
| try: |
| with open("/proc/sys/kernel/randomize_va_space", "r") as f: |
| return int(f.readline().strip()) |
| except Exception as e: |
| print "Failed to get current ASLR settings." |
| raise e |
| |
| @staticmethod |
| def SetASLR(value): |
| try: |
| with open("/proc/sys/kernel/randomize_va_space", "w") as f: |
| f.write(str(value)) |
| except Exception as e: |
| print "Failed to update ASLR to %s." % value |
| print "Are we running under sudo?" |
| raise e |
| |
| new_value = CustomMachineConfiguration.GetASLR() |
| if value != new_value: |
| raise Exception("Present value is %s" % new_value) |
| |
| @staticmethod |
| def GetCPUCoresRange(): |
| try: |
| with open("/sys/devices/system/cpu/present", "r") as f: |
| indexes = f.readline() |
| r = map(int, indexes.split("-")) |
| if len(r) == 1: |
| return range(r[0], r[0] + 1) |
| return range(r[0], r[1] + 1) |
| except Exception as e: |
| print "Failed to retrieve number of CPUs." |
| raise e |
| |
| @staticmethod |
| def GetCPUPathForId(cpu_index): |
| ret = "/sys/devices/system/cpu/cpu" |
| ret += str(cpu_index) |
| ret += "/cpufreq/scaling_governor" |
| return ret |
| |
| @staticmethod |
| def GetCPUGovernor(): |
| try: |
| cpu_indices = CustomMachineConfiguration.GetCPUCoresRange() |
| ret = None |
| for cpu_index in cpu_indices: |
| cpu_device = CustomMachineConfiguration.GetCPUPathForId(cpu_index) |
| with open(cpu_device, "r") as f: |
| # We assume the governors of all CPUs are set to the same value |
| val = f.readline().strip() |
| if ret == None: |
| ret = val |
| elif ret != val: |
| raise Exception("CPU cores have differing governor settings") |
| return ret |
| except Exception as e: |
| print "Failed to get the current CPU governor." |
| print "Is the CPU governor disabled? Check BIOS." |
| raise e |
| |
| @staticmethod |
| def SetCPUGovernor(value): |
| try: |
| cpu_indices = CustomMachineConfiguration.GetCPUCoresRange() |
| for cpu_index in cpu_indices: |
| cpu_device = CustomMachineConfiguration.GetCPUPathForId(cpu_index) |
| with open(cpu_device, "w") as f: |
| f.write(value) |
| |
| except Exception as e: |
| print "Failed to change CPU governor to %s." % value |
| print "Are we running under sudo?" |
| raise e |
| |
| cur_value = CustomMachineConfiguration.GetCPUGovernor() |
| if cur_value != value: |
| raise Exception("Could not set CPU governor. Present value is %s" |
| % cur_value ) |
| |
| def Main(args): |
| logging.getLogger().setLevel(logging.INFO) |
| parser = optparse.OptionParser() |
| parser.add_option("--android-build-tools", |
| help="Path to chromium's build/android. Specifying this " |
| "option will run tests using android platform.") |
| parser.add_option("--arch", |
| help=("The architecture to run tests for, " |
| "'auto' or 'native' for auto-detect"), |
| default="x64") |
| parser.add_option("--buildbot", |
| help="Adapt to path structure used on buildbots", |
| default=False, action="store_true") |
| parser.add_option("--device", |
| help="The device ID to run Android tests on. If not given " |
| "it will be autodetected.") |
| parser.add_option("--extra-flags", |
| help="Additional flags to pass to the test executable", |
| default="") |
| parser.add_option("--json-test-results", |
| help="Path to a file for storing json results.") |
| parser.add_option("--json-test-results-secondary", |
| "--json-test-results-no-patch", # TODO(sergiyb): Deprecate. |
| help="Path to a file for storing json results from run " |
| "without patch or for reference build run.") |
| parser.add_option("--outdir", help="Base directory with compile output", |
| default="out") |
| parser.add_option("--outdir-secondary", |
| "--outdir-no-patch", # TODO(sergiyb): Deprecate. |
| help="Base directory with compile output without patch or " |
| "for reference build") |
| parser.add_option("--binary-override-path", |
| help="JavaScript engine binary. By default, d8 under " |
| "architecture-specific build dir. " |
| "Not supported in conjunction with outdir-secondary.") |
| parser.add_option("--prioritize", |
| help="Raise the priority to nice -20 for the benchmarking " |
| "process.Requires Linux, schedtool, and sudo privileges.", |
| default=False, action="store_true") |
| parser.add_option("--affinitize", |
| help="Run benchmarking process on the specified core. " |
| "For example: " |
| "--affinitize=0 will run the benchmark process on core 0. " |
| "--affinitize=3 will run the benchmark process on core 3. " |
| "Requires Linux, schedtool, and sudo privileges.", |
| default=None) |
| parser.add_option("--noaslr", |
| help="Disable ASLR for the duration of the benchmarked " |
| "process. Requires Linux and sudo privileges.", |
| default=False, action="store_true") |
| parser.add_option("--cpu-governor", |
| help="Set cpu governor to specified policy for the " |
| "duration of the benchmarked process. Typical options: " |
| "'powersave' for more stable results, or 'performance' " |
| "for shorter completion time of suite, with potentially " |
| "more noise in results.") |
| parser.add_option("--filter", |
| help="Only run the benchmarks beginning with this string. " |
| "For example: " |
| "--filter=JSTests/TypedArrays/ will run only TypedArray " |
| "benchmarks from the JSTests suite.", |
| default="") |
| |
| (options, args) = parser.parse_args(args) |
| |
| if len(args) == 0: # pragma: no cover |
| parser.print_help() |
| return 1 |
| |
| if options.arch in ["auto", "native"]: # pragma: no cover |
| options.arch = ARCH_GUESS |
| |
| if not options.arch in SUPPORTED_ARCHS: # pragma: no cover |
| print "Unknown architecture %s" % options.arch |
| return 1 |
| |
| if options.device and not options.android_build_tools: # pragma: no cover |
| print "Specifying a device requires Android build tools." |
| return 1 |
| |
| if (options.json_test_results_secondary and |
| not options.outdir_secondary): # pragma: no cover |
| print("For writing secondary json test results, a secondary outdir patch " |
| "must be specified.") |
| return 1 |
| |
| workspace = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) |
| |
| if options.buildbot: |
| build_config = "Release" |
| else: |
| build_config = "%s.release" % options.arch |
| |
| if options.binary_override_path == None: |
| options.shell_dir = os.path.join(workspace, options.outdir, build_config) |
| default_binary_name = "d8" |
| else: |
| if not os.path.isfile(options.binary_override_path): |
| print "binary-override-path must be a file name" |
| return 1 |
| if options.outdir_secondary: |
| print "specify either binary-override-path or outdir-secondary" |
| return 1 |
| options.shell_dir = os.path.abspath( |
| os.path.dirname(options.binary_override_path)) |
| default_binary_name = os.path.basename(options.binary_override_path) |
| |
| if options.outdir_secondary: |
| options.shell_dir_secondary = os.path.join( |
| workspace, options.outdir_secondary, build_config) |
| else: |
| options.shell_dir_secondary = None |
| |
| if options.json_test_results: |
| options.json_test_results = os.path.abspath(options.json_test_results) |
| |
| if options.json_test_results_secondary: |
| options.json_test_results_secondary = os.path.abspath( |
| options.json_test_results_secondary) |
| |
| # Ensure all arguments have absolute path before we start changing current |
| # directory. |
| args = map(os.path.abspath, args) |
| |
| prev_aslr = None |
| prev_cpu_gov = None |
| platform = Platform.GetPlatform(options) |
| |
| results = Results() |
| results_secondary = Results() |
| with CustomMachineConfiguration(governor = options.cpu_governor, |
| disable_aslr = options.noaslr) as conf: |
| for path in args: |
| if not os.path.exists(path): # pragma: no cover |
| results.errors.append("Configuration file %s does not exist." % path) |
| continue |
| |
| with open(path) as f: |
| suite = json.loads(f.read()) |
| |
| # If no name is given, default to the file name without .json. |
| suite.setdefault("name", os.path.splitext(os.path.basename(path))[0]) |
| |
| # Setup things common to one test suite. |
| platform.PreExecution() |
| |
| # Build the graph/trace tree structure. |
| default_parent = DefaultSentinel(default_binary_name) |
| root = BuildGraphConfigs(suite, options.arch, default_parent) |
| |
| # Callback to be called on each node on traversal. |
| def NodeCB(node): |
| platform.PreTests(node, path) |
| |
| # Traverse graph/trace tree and iterate over all runnables. |
| for runnable in FlattenRunnables(root, NodeCB): |
| runnable_name = "/".join(runnable.graphs) |
| if not runnable_name.startswith(options.filter): |
| continue |
| print ">>> Running suite: %s" % runnable_name |
| |
| def Runner(): |
| """Output generator that reruns several times.""" |
| for i in xrange(0, max(1, runnable.run_count)): |
| # TODO(machenbach): Allow timeout per arch like with run_count per |
| # arch. |
| yield platform.Run(runnable, i) |
| |
| # Let runnable iterate over all runs and handle output. |
| result, result_secondary = runnable.Run( |
| Runner, trybot=options.shell_dir_secondary) |
| results += result |
| results_secondary += result_secondary |
| platform.PostExecution() |
| |
| if options.json_test_results: |
| results.WriteToFile(options.json_test_results) |
| else: # pragma: no cover |
| print results |
| |
| if options.json_test_results_secondary: |
| results_secondary.WriteToFile(options.json_test_results_secondary) |
| else: # pragma: no cover |
| print results_secondary |
| |
| return min(1, len(results.errors)) |
| |
| if __name__ == "__main__": # pragma: no cover |
| sys.exit(Main(sys.argv[1:])) |