blob: 598bc5315e1937707c3cb2461f00a39586ab008f [file] [log] [blame]
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Parses the command line, discovers the appropriate benchmarks, and runs them.
Handles benchmark configuration, but all the logic for
actually running the benchmark is in Benchmark and PageRunner."""
import argparse
import hashlib
import json
import logging
import os
import sys
from telemetry import benchmark
from telemetry.core import discover
from telemetry import decorators
from telemetry.internal.browser import browser_finder
from telemetry.internal.browser import browser_options
from telemetry.internal.util import binary_manager
from telemetry.internal.util import command_line
from telemetry.internal.util import ps_util
from telemetry.util import matching
# Right now, we only have one of each of our power perf bots. This means that
# all eligible Telemetry benchmarks are run unsharded, which results in very
# long (12h) cycle times. We'd like to reduce the number of tests that we run
# on each bot drastically until we get more of the same hardware to shard tests
# with, but we can't do so until we've verified that the hardware configuration
# is a viable one for Chrome Telemetry tests. This is done by seeing at least
# one all-green test run. As this happens for each bot, we'll add it to this
# whitelist, making it eligible to run only BattOr power tests.
GOOD_POWER_PERF_BOT_WHITELIST = [
"Mac Power Dual-GPU Perf",
"Mac Power Low-End Perf"
]
DEFAULT_LOG_FORMAT = (
'(%(levelname)s) %(asctime)s %(module)s.%(funcName)s:%(lineno)d '
'%(message)s')
def _IsBenchmarkEnabled(benchmark_class, possible_browser):
return (issubclass(benchmark_class, benchmark.Benchmark) and
not benchmark_class.ShouldDisable(possible_browser) and
decorators.IsEnabled(benchmark_class, possible_browser)[0])
def PrintBenchmarkList(benchmarks, possible_browser, output_pipe=sys.stdout):
""" Print benchmarks that are not filtered in the same order of benchmarks in
the |benchmarks| list.
Args:
benchmarks: the list of benchmarks to be printed (in the same order of the
list).
possible_browser: the possible_browser instance that's used for checking
which benchmarks are enabled.
output_pipe: the stream in which benchmarks are printed on.
"""
if not benchmarks:
print >> output_pipe, 'No benchmarks found!'
return
b = None # Need this to stop pylint from complaining undefined variable.
if any(not issubclass(b, benchmark.Benchmark) for b in benchmarks):
assert False, '|benchmarks| param contains non benchmark class: %s' % b
# Align the benchmark names to the longest one.
format_string = ' %%-%ds %%s' % max(len(b.Name()) for b in benchmarks)
disabled_benchmarks = []
print >> output_pipe, 'Available benchmarks %sare:' % (
'for %s ' % possible_browser.browser_type if possible_browser else '')
# Sort the benchmarks by benchmark name.
benchmarks = sorted(benchmarks, key=lambda b: b.Name())
for b in benchmarks:
if not possible_browser or _IsBenchmarkEnabled(b, possible_browser):
print >> output_pipe, format_string % (b.Name(), b.Description())
else:
disabled_benchmarks.append(b)
if disabled_benchmarks:
print >> output_pipe, (
'\nDisabled benchmarks for %s are (force run with -d):' %
possible_browser.browser_type)
for b in disabled_benchmarks:
print >> output_pipe, format_string % (b.Name(), b.Description())
print >> output_pipe, (
'Pass --browser to list benchmarks for another browser.\n')
class Help(command_line.OptparseCommand):
"""Display help information about a command"""
usage = '[command]'
def __init__(self, commands):
self._all_commands = commands
def Run(self, args):
if len(args.positional_args) == 1:
commands = _MatchingCommands(args.positional_args[0], self._all_commands)
if len(commands) == 1:
command = commands[0]
parser = command.CreateParser()
command.AddCommandLineArgs(parser, None)
parser.print_help()
return 0
print >> sys.stderr, ('usage: %s [command] [<options>]' % _ScriptName())
print >> sys.stderr, 'Available commands are:'
for command in self._all_commands:
print >> sys.stderr, ' %-10s %s' % (
command.Name(), command.Description())
print >> sys.stderr, ('"%s help <command>" to see usage information '
'for a specific command.' % _ScriptName())
return 0
class List(command_line.OptparseCommand):
"""Lists the available benchmarks"""
usage = '[benchmark_name] [<options>]'
@classmethod
def CreateParser(cls):
options = browser_options.BrowserFinderOptions()
parser = options.CreateParser('%%prog %s %s' % (cls.Name(), cls.usage))
return parser
@classmethod
def AddCommandLineArgs(cls, parser, _):
parser.add_option('-j', '--json-output-file', type='string')
parser.add_option('-n', '--num-shards', type='int', default=1)
@classmethod
def ProcessCommandLineArgs(cls, parser, args, environment):
if not args.positional_args:
args.benchmarks = _Benchmarks(environment)
elif len(args.positional_args) == 1:
args.benchmarks = _MatchBenchmarkName(args.positional_args[0],
environment, exact_matches=False)
else:
parser.error('Must provide at most one benchmark name.')
def Run(self, args):
possible_browser = browser_finder.FindBrowser(args)
if args.browser_type in (
'release', 'release_x64', 'debug', 'debug_x64', 'canary',
'android-chromium', 'android-chrome'):
args.browser_type = 'reference'
possible_reference_browser = browser_finder.FindBrowser(args)
else:
possible_reference_browser = None
if args.json_output_file:
with open(args.json_output_file, 'w') as f:
f.write(_GetJsonBenchmarkList(possible_browser,
possible_reference_browser,
args.benchmarks, args.num_shards))
else:
PrintBenchmarkList(args.benchmarks, possible_browser)
return 0
class Run(command_line.OptparseCommand):
"""Run one or more benchmarks (default)"""
usage = 'benchmark_name [page_set] [<options>]'
@classmethod
def CreateParser(cls):
options = browser_options.BrowserFinderOptions()
parser = options.CreateParser('%%prog %s %s' % (cls.Name(), cls.usage))
return parser
@classmethod
def AddCommandLineArgs(cls, parser, environment):
benchmark.AddCommandLineArgs(parser)
# Allow benchmarks to add their own command line options.
matching_benchmarks = []
for arg in sys.argv[1:]:
matching_benchmarks += _MatchBenchmarkName(arg, environment)
if matching_benchmarks:
# TODO(dtu): After move to argparse, add command-line args for all
# benchmarks to subparser. Using subparsers will avoid duplicate
# arguments.
matching_benchmark = matching_benchmarks.pop()
matching_benchmark.AddCommandLineArgs(parser)
# The benchmark's options override the defaults!
matching_benchmark.SetArgumentDefaults(parser)
@classmethod
def ProcessCommandLineArgs(cls, parser, args, environment):
all_benchmarks = _Benchmarks(environment)
if not args.positional_args:
possible_browser = (
browser_finder.FindBrowser(args) if args.browser_type else None)
PrintBenchmarkList(all_benchmarks, possible_browser)
sys.exit(-1)
input_benchmark_name = args.positional_args[0]
matching_benchmarks = _MatchBenchmarkName(input_benchmark_name, environment)
if not matching_benchmarks:
print >> sys.stderr, 'No benchmark named "%s".' % input_benchmark_name
print >> sys.stderr
most_likely_matched_benchmarks = matching.GetMostLikelyMatchedObject(
all_benchmarks, input_benchmark_name, lambda x: x.Name())
if most_likely_matched_benchmarks:
print >> sys.stderr, 'Do you mean any of those benchmarks below?'
PrintBenchmarkList(most_likely_matched_benchmarks, None, sys.stderr)
sys.exit(-1)
if len(matching_benchmarks) > 1:
print >> sys.stderr, ('Multiple benchmarks named "%s".' %
input_benchmark_name)
print >> sys.stderr, 'Did you mean one of these?'
print >> sys.stderr
PrintBenchmarkList(matching_benchmarks, None, sys.stderr)
sys.exit(-1)
benchmark_class = matching_benchmarks.pop()
if len(args.positional_args) > 1:
parser.error('Too many arguments.')
assert issubclass(benchmark_class, benchmark.Benchmark), (
'Trying to run a non-Benchmark?!')
benchmark.ProcessCommandLineArgs(parser, args)
benchmark_class.ProcessCommandLineArgs(parser, args)
cls._benchmark = benchmark_class
def Run(self, args):
return min(255, self._benchmark().Run(args))
def _ScriptName():
return os.path.basename(sys.argv[0])
def _MatchingCommands(string, commands):
return [command for command in commands
if command.Name().startswith(string)]
@decorators.Cache
def _Benchmarks(environment):
benchmarks = []
for search_dir in environment.benchmark_dirs:
benchmarks += discover.DiscoverClasses(search_dir,
environment.top_level_dir,
benchmark.Benchmark,
index_by_class_name=True).values()
return benchmarks
def _MatchBenchmarkName(input_benchmark_name, environment, exact_matches=True):
def _Matches(input_string, search_string):
if search_string.startswith(input_string):
return True
for part in search_string.split('.'):
if part.startswith(input_string):
return True
return False
# Exact matching.
if exact_matches:
# Don't add aliases to search dict, only allow exact matching for them.
if input_benchmark_name in environment.benchmark_aliases:
exact_match = environment.benchmark_aliases[input_benchmark_name]
else:
exact_match = input_benchmark_name
for benchmark_class in _Benchmarks(environment):
if exact_match == benchmark_class.Name():
return [benchmark_class]
return []
# Fuzzy matching.
return [benchmark_class for benchmark_class in _Benchmarks(environment)
if _Matches(input_benchmark_name, benchmark_class.Name())]
def GetBenchmarkByName(name, environment):
matched = _MatchBenchmarkName(name, environment, exact_matches=True)
# With exact_matches, len(matched) is either 0 or 1.
if len(matched) == 0:
return None
return matched[0]
def _GetJsonBenchmarkList(possible_browser, possible_reference_browser,
benchmark_classes, num_shards):
"""Returns a list of all enabled benchmarks in a JSON format expected by
buildbots.
JSON format:
{ "version": <int>,
"steps": {
<string>: {
"device_affinity": <int>,
"cmd": <string>,
"perf_dashboard_id": <string>,
},
...
}
}
"""
# TODO(charliea): Remove this once we have more power perf bots.
only_run_battor_benchmarks = False
print 'Environment variables: ', os.environ
if os.environ.get('BUILDBOT_BUILDERNAME') in GOOD_POWER_PERF_BOT_WHITELIST:
only_run_battor_benchmarks = True
output = {
'version': 1,
'steps': {
}
}
for benchmark_class in benchmark_classes:
if not _IsBenchmarkEnabled(benchmark_class, possible_browser):
continue
base_name = benchmark_class.Name()
# TODO(charliea): Remove this once we have more power perf bots.
# Only run battor power benchmarks to reduce the cycle time of this bot.
# TODO(rnephew): Enable media.* and power.* tests when Mac BattOr issue
# is solved.
if only_run_battor_benchmarks and not base_name.startswith('battor'):
continue
base_cmd = [sys.executable, os.path.realpath(sys.argv[0]),
'-v', '--output-format=chartjson', '--upload-results',
base_name]
perf_dashboard_id = base_name
# Based on the current timings, we shift the result of the hash function to
# achieve better load balancing. Those shift values are to be revised when
# necessary. The shift value is calculated such that the total cycle time
# is minimized.
hash_shift = {
2 : 47, # for old desktop configurations with 2 slaves
5 : 56, # for new desktop configurations with 5 slaves
21 : 43 # for Android 3 slaves 7 devices configurations
}
shift = hash_shift.get(num_shards, 0)
base_name_hash = hashlib.sha1(base_name).hexdigest()
device_affinity = (int(base_name_hash, 16) >> shift) % num_shards
output['steps'][base_name] = {
'cmd': ' '.join(base_cmd + [
'--browser=%s' % possible_browser.browser_type]),
'device_affinity': device_affinity,
'perf_dashboard_id': perf_dashboard_id,
}
if (possible_reference_browser and
_IsBenchmarkEnabled(benchmark_class, possible_reference_browser)):
output['steps'][base_name + '.reference'] = {
'cmd': ' '.join(base_cmd + [
'--browser=reference', '--output-trace-tag=_ref']),
'device_affinity': device_affinity,
'perf_dashboard_id': perf_dashboard_id,
}
# Make sure that page_cycler_v2.typical_25 is assigned to the same device
# as page_cycler.typical_25 benchmark.
# TODO(nednguyen): remove this hack when crbug.com/618156 is resolved.
if ('page_cycler_v2.typical_25' in output['steps'] and
'page_cycler.typical_25' in output['steps']):
output['steps']['page_cycler_v2.typical_25']['device_affinity'] = (
output['steps']['page_cycler.typical_25']['device_affinity'])
return json.dumps(output, indent=2, sort_keys=True)
def main(environment, extra_commands=None, **log_config_kwargs):
# The log level is set in browser_options.
log_config_kwargs.pop('level', None)
log_config_kwargs.setdefault('format', DEFAULT_LOG_FORMAT)
logging.basicConfig(**log_config_kwargs)
ps_util.EnableListingStrayProcessesUponExitHook()
# Get the command name from the command line.
if len(sys.argv) > 1 and sys.argv[1] == '--help':
sys.argv[1] = 'help'
command_name = 'run'
for arg in sys.argv[1:]:
if not arg.startswith('-'):
command_name = arg
break
# TODO(eakuefner): Remove this hack after we port to argparse.
if command_name == 'help' and len(sys.argv) > 2 and sys.argv[2] == 'run':
command_name = 'run'
sys.argv[2] = '--help'
if extra_commands is None:
extra_commands = []
all_commands = [Help, List, Run] + extra_commands
# Validate and interpret the command name.
commands = _MatchingCommands(command_name, all_commands)
if len(commands) > 1:
print >> sys.stderr, ('"%s" is not a %s command. Did you mean one of these?'
% (command_name, _ScriptName()))
for command in commands:
print >> sys.stderr, ' %-10s %s' % (
command.Name(), command.Description())
return 1
if commands:
command = commands[0]
else:
command = Run
binary_manager.InitDependencyManager(environment.client_configs)
# Parse and run the command.
parser = command.CreateParser()
command.AddCommandLineArgs(parser, environment)
# Set the default chrome root variable.
parser.set_defaults(chrome_root=environment.default_chrome_root)
if isinstance(parser, argparse.ArgumentParser):
commandline_args = sys.argv[1:]
options, args = parser.parse_known_args(commandline_args[1:])
command.ProcessCommandLineArgs(parser, options, args, environment)
else:
options, args = parser.parse_args()
if commands:
args = args[1:]
options.positional_args = args
command.ProcessCommandLineArgs(parser, options, environment)
if command == Help:
command_instance = command(all_commands)
else:
command_instance = command()
if isinstance(command_instance, command_line.OptparseCommand):
return command_instance.Run(options)
else:
return command_instance.Run(options, args)