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