blob: 1c42c0cb07466f1d8e87a47d9d0d4b643e49ee42 [file] [log] [blame]
# Copyright 2014 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.
import logging
import optparse
import os
import shutil
import sys
import zipfile
from telemetry import decorators
from telemetry import page
from telemetry.core import browser_finder
from telemetry.core import command_line
from telemetry.core import util
from telemetry.user_story import user_story_runner
from telemetry.page import page_set
from telemetry.page import page_test
from telemetry.page import test_expectations
from telemetry.results import results_options
from telemetry.util import cloud_storage
from telemetry.util import exception_formatter
from telemetry.web_perf import timeline_based_measurement
Disabled = decorators.Disabled
Enabled = decorators.Enabled
class InvalidOptionsError(Exception):
"""Raised for invalid benchmark options."""
pass
class BenchmarkMetadata(object):
def __init__(self, name, description='', rerun_options=None):
self._name = name
self._description = description
self._rerun_options = rerun_options
@property
def name(self):
return self._name
@property
def description(self):
return self._description
@property
def rerun_options(self):
return self._rerun_options
class Benchmark(command_line.Command):
"""Base class for a Telemetry benchmark.
A benchmark packages a measurement and a PageSet together.
Benchmarks default to using TBM unless you override the value of
Benchmark.test, or override the CreatePageTest method.
New benchmarks should override CreateUserStorySet.
"""
options = {}
test = timeline_based_measurement.TimelineBasedMeasurement
def __init__(self, max_failures=None):
"""Creates a new Benchmark.
Args:
max_failures: The number of user story run's failures before bailing
from executing subsequent page runs. If None, we never bail.
"""
self._max_failures = max_failures
self._has_original_tbm_options = (
self.CreateTimelineBasedMeasurementOptions.__func__ ==
Benchmark.CreateTimelineBasedMeasurementOptions.__func__)
has_original_create_page_test = (
self.CreatePageTest.__func__ == Benchmark.CreatePageTest.__func__)
assert self._has_original_tbm_options or has_original_create_page_test, (
'Cannot override both CreatePageTest and '
'CreateTimelineBasedMeasurementOptions.')
@classmethod
def Name(cls):
return '%s.%s' % (cls.__module__.split('.')[-1], cls.__name__)
@classmethod
def AddCommandLineArgs(cls, parser):
group = optparse.OptionGroup(parser, '%s test options' % cls.Name())
if hasattr(cls, 'AddBenchmarkCommandLineArgs'):
cls.AddBenchmarkCommandLineArgs(group)
if cls.HasTraceRerunDebugOption():
group.add_option(
'--rerun-with-debug-trace',
action='store_true',
help='Rerun option that enables more extensive tracing.')
if group.option_list:
parser.add_option_group(group)
@classmethod
def HasTraceRerunDebugOption(cls):
if hasattr(cls, 'HasBenchmarkTraceRerunDebugOption'):
if cls.HasBenchmarkTraceRerunDebugOption():
return True
return False
def GetTraceRerunCommands(self):
if self.HasTraceRerunDebugOption():
return [['Debug Trace', '--rerun-with-debug-trace']]
return []
def SetupTraceRerunOptions(self, browser_options, tbm_options):
if self.HasTraceRerunDebugOption():
if browser_options.rerun_with_debug_trace:
self.SetupBenchmarkDebugTraceRerunOptions(tbm_options)
else:
self.SetupBenchmarkDefaultTraceRerunOptions(tbm_options)
def SetupBenchmarkDefaultTraceRerunOptions(self, tbm_options):
"""Setup tracing categories associated with default trace option."""
def SetupBenchmarkDebugTraceRerunOptions(self, tbm_options):
"""Setup tracing categories associated with debug trace option."""
@classmethod
def SetArgumentDefaults(cls, parser):
default_values = parser.get_default_values()
invalid_options = [
o for o in cls.options if not hasattr(default_values, o)]
if invalid_options:
raise InvalidOptionsError('Invalid benchmark options: %s',
', '.join(invalid_options))
parser.set_defaults(**cls.options)
@classmethod
def ProcessCommandLineArgs(cls, parser, args):
pass
# pylint: disable=unused-argument
@classmethod
def ValueCanBeAddedPredicate(cls, value, is_first_result):
"""Returns whether |value| can be added to the test results.
Override this method to customize the logic of adding values to test
results.
Args:
value: a value.Value instance.
is_first_result: True if |value| is the first result for its
corresponding user story.
Returns:
True if |value| should be added to the test results.
Otherwise, it returns False.
"""
return True
def CustomizeBrowserOptions(self, options):
"""Add browser options that are required by this benchmark."""
def GetMetadata(self):
return BenchmarkMetadata(
self.Name(), self.__doc__, self.GetTraceRerunCommands())
def Run(self, finder_options):
"""Run this test with the given options.
Returns:
The number of failure values (up to 254) or 255 if there is an uncaught
exception.
"""
self.CustomizeBrowserOptions(finder_options.browser_options)
pt = self.CreatePageTest(finder_options)
pt.__name__ = self.__class__.__name__
if hasattr(self, '_disabled_strings'):
# pylint: disable=protected-access
pt._disabled_strings = self._disabled_strings
if hasattr(self, '_enabled_strings'):
# pylint: disable=protected-access
pt._enabled_strings = self._enabled_strings
expectations = self.CreateExpectations()
us = self.CreateUserStorySet(finder_options)
if isinstance(pt, page_test.PageTest):
if any(not isinstance(p, page.Page) for p in us.user_stories):
raise Exception(
'PageTest must be used with UserStorySet containing only '
'telemetry.page.Page user stories.')
self._DownloadGeneratedProfileArchive(finder_options)
benchmark_metadata = self.GetMetadata()
with results_options.CreateResults(
benchmark_metadata, finder_options,
self.ValueCanBeAddedPredicate) as results:
try:
user_story_runner.Run(pt, us, expectations, finder_options, results,
max_failures=self._max_failures)
return_code = min(254, len(results.failures))
except Exception:
exception_formatter.PrintFormattedException()
return_code = 255
bucket = cloud_storage.BUCKET_ALIASES[finder_options.upload_bucket]
if finder_options.upload_results:
results.UploadTraceFilesToCloud(bucket)
results.UploadProfilingFilesToCloud(bucket)
results.PrintSummary()
return return_code
def _DownloadGeneratedProfileArchive(self, options):
"""Download and extract profile directory archive if one exists."""
archive_name = getattr(self, 'generated_profile_archive', None)
# If attribute not specified, nothing to do.
if not archive_name:
return
# If profile dir specified on command line, nothing to do.
if options.browser_options.profile_dir:
logging.warning("Profile directory specified on command line: %s, this"
"overrides the benchmark's default profile directory.",
options.browser_options.profile_dir)
return
# Download profile directory from cloud storage.
found_browser = browser_finder.FindBrowser(options)
test_data_dir = os.path.join(util.GetChromiumSrcDir(), 'tools', 'perf',
'generated_profiles',
found_browser.target_os)
generated_profile_archive_path = os.path.normpath(
os.path.join(test_data_dir, archive_name))
try:
cloud_storage.GetIfChanged(generated_profile_archive_path,
cloud_storage.PUBLIC_BUCKET)
except (cloud_storage.CredentialsError,
cloud_storage.PermissionError) as e:
if os.path.exists(generated_profile_archive_path):
# If the profile directory archive exists, assume the user has their
# own local copy simply warn.
logging.warning('Could not download Profile archive: %s',
generated_profile_archive_path)
else:
# If the archive profile directory doesn't exist, this is fatal.
logging.error('Can not run without required profile archive: %s. '
'If you believe you have credentials, follow the '
'instructions below.',
generated_profile_archive_path)
logging.error(str(e))
sys.exit(-1)
# Unzip profile directory.
extracted_profile_dir_path = (
os.path.splitext(generated_profile_archive_path)[0])
if not os.path.isfile(generated_profile_archive_path):
raise Exception("Profile directory archive not downloaded: ",
generated_profile_archive_path)
with zipfile.ZipFile(generated_profile_archive_path) as f:
try:
f.extractall(os.path.dirname(generated_profile_archive_path))
except e:
# Cleanup any leftovers from unzipping.
if os.path.exists(extracted_profile_dir_path):
shutil.rmtree(extracted_profile_dir_path)
logging.error("Error extracting profile directory zip file: %s", e)
sys.exit(-1)
# Run with freshly extracted profile directory.
logging.info("Using profile archive directory: %s",
extracted_profile_dir_path)
options.browser_options.profile_dir = extracted_profile_dir_path
def CreateTimelineBasedMeasurementOptions(self):
"""Return the TimelineBasedMeasurementOptions for this Benchmark.
Override this method to configure a TimelineBasedMeasurement benchmark.
Otherwise, override CreatePageTest for PageTest tests. Do not override
both methods.
"""
return timeline_based_measurement.Options()
def CreatePageTest(self, options): # pylint: disable=unused-argument
"""Return the PageTest for this Benchmark.
Override this method for PageTest tests.
Override, override CreateTimelineBasedMeasurementOptions to configure
TimelineBasedMeasurement tests. Do not override both methods.
Args:
options: a browser_options.BrowserFinderOptions instance
Returns:
|test()| if |test| is a PageTest class.
Otherwise, a TimelineBasedMeasurement instance.
"""
is_page_test = issubclass(self.test, page_test.PageTest)
is_tbm = self.test == timeline_based_measurement.TimelineBasedMeasurement
if not is_page_test and not is_tbm:
raise TypeError('"%s" is not a PageTest or a TimelineBasedMeasurement.' %
self.test.__name__)
if is_page_test:
assert self._has_original_tbm_options, (
'Cannot override CreateTimelineBasedMeasurementOptions '
'with a PageTest.')
return self.test() # pylint: disable=no-value-for-parameter
opts = self.CreateTimelineBasedMeasurementOptions()
self.SetupTraceRerunOptions(options, opts)
return timeline_based_measurement.TimelineBasedMeasurement(opts)
def CreatePageSet(self, options): # pylint: disable=unused-argument
"""Get the page set this test will run on.
By default, it will create a page set from the this test's page_set
attribute. Override to generate a custom page set.
"""
if not hasattr(self, 'page_set'):
raise NotImplementedError('This test has no "page_set" attribute.')
if not issubclass(self.page_set, page_set.PageSet):
raise TypeError('"%s" is not a PageSet.' % self.page_set.__name__)
return self.page_set()
def CreateUserStorySet(self, options):
return self.CreatePageSet(options)
@classmethod
def CreateExpectations(cls):
"""Get the expectations this test will run with.
By default, it will create an empty expectations set. Override to generate
custom expectations.
"""
return test_expectations.TestExpectations()
def AddCommandLineArgs(parser):
user_story_runner.AddCommandLineArgs(parser)
def ProcessCommandLineArgs(parser, args):
user_story_runner.ProcessCommandLineArgs(parser, args)