blob: 278a4cf358b60daeb1cfd9367a650b7e86f7e97f [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 sys
import time
from py_utils import cloud_storage # pylint: disable=import-error
from telemetry.core import exceptions
from telemetry import decorators
from telemetry.internal.actions import page_action
from telemetry.internal.browser import browser_finder
from telemetry.internal.results import results_options
from telemetry.internal.util import exception_formatter
from telemetry import page
from telemetry.page import legacy_page_test
from telemetry import story as story_module
from telemetry.util import wpr_modes
from telemetry.value import failure
from telemetry.value import skip
from telemetry.web_perf import story_test
class ArchiveError(Exception):
pass
def AddCommandLineArgs(parser):
story_module.StoryFilter.AddCommandLineArgs(parser)
results_options.AddResultsOptions(parser)
# Page set options
group = optparse.OptionGroup(parser, 'Page set repeat options')
group.add_option('--page-repeat', default=1, type='int',
help='Number of times to repeat each individual page '
'before proceeding with the next page in the pageset.')
group.add_option('--pageset-repeat', default=1, type='int',
help='Number of times to repeat the entire pageset.')
group.add_option('--max-failures', default=None, type='int',
help='Maximum number of test failures before aborting '
'the run. Defaults to the number specified by the '
'PageTest.')
parser.add_option_group(group)
# WPR options
group = optparse.OptionGroup(parser, 'Web Page Replay options')
group.add_option('--use-live-sites',
dest='use_live_sites', action='store_true',
help='Run against live sites and ignore the Web Page Replay archives.')
parser.add_option_group(group)
parser.add_option('-d', '--also-run-disabled-tests',
dest='run_disabled_tests',
action='store_true', default=False,
help='Ignore @Disabled and @Enabled restrictions.')
def ProcessCommandLineArgs(parser, args):
story_module.StoryFilter.ProcessCommandLineArgs(parser, args)
results_options.ProcessCommandLineArgs(parser, args)
# Page set options
if args.page_repeat < 1:
parser.error('--page-repeat must be a positive integer.')
if args.pageset_repeat < 1:
parser.error('--pageset-repeat must be a positive integer.')
def _RunStoryAndProcessErrorIfNeeded(story, results, state, test):
def ProcessError(description=None):
state.DumpStateUponFailure(story, results)
results.AddValue(failure.FailureValue(story, sys.exc_info(), description))
try:
if isinstance(test, story_test.StoryTest):
test.WillRunStory(state.platform)
state.WillRunStory(story)
if not state.CanRunStory(story):
results.AddValue(skip.SkipValue(
story,
'Skipped because story is not supported '
'(SharedState.CanRunStory() returns False).'))
return
state.RunStory(results)
if isinstance(test, story_test.StoryTest):
test.Measure(state.platform, results)
except (legacy_page_test.Failure, exceptions.TimeoutException,
exceptions.LoginException, exceptions.ProfilingException):
ProcessError()
except exceptions.Error:
ProcessError()
raise
except page_action.PageActionNotSupported as e:
results.AddValue(
skip.SkipValue(story, 'Unsupported page action: %s' % e))
except Exception:
ProcessError(description='Unhandlable exception raised.')
raise
finally:
has_existing_exception = (sys.exc_info() != (None, None, None))
try:
state.DidRunStory(results)
# if state.DidRunStory raises exception, things are messed up badly and we
# do not need to run test.DidRunStory at that point.
if isinstance(test, story_test.StoryTest):
test.DidRunStory(state.platform)
else:
test.DidRunPage(state.platform)
except Exception:
if not has_existing_exception:
state.DumpStateUponFailure(story, results)
raise
# Print current exception and propagate existing exception.
exception_formatter.PrintFormattedException(
msg='Exception raised when cleaning story run: ')
class StoryGroup(object):
def __init__(self, shared_state_class):
self._shared_state_class = shared_state_class
self._stories = []
@property
def shared_state_class(self):
return self._shared_state_class
@property
def stories(self):
return self._stories
def AddStory(self, story):
assert (story.shared_state_class is
self._shared_state_class)
self._stories.append(story)
def StoriesGroupedByStateClass(story_set, allow_multiple_groups):
""" Returns a list of story groups which each contains stories with
the same shared_state_class.
Example:
Assume A1, A2, A3 are stories with same shared story class, and
similar for B1, B2.
If their orders in story set is A1 A2 B1 B2 A3, then the grouping will
be [A1 A2] [B1 B2] [A3].
It's purposefully done this way to make sure that order of
stories are the same of that defined in story_set. It's recommended that
stories with the same states should be arranged next to each others in
story sets to reduce the overhead of setting up & tearing down the
shared story state.
"""
story_groups = []
story_groups.append(
StoryGroup(story_set[0].shared_state_class))
for story in story_set:
if (story.shared_state_class is not
story_groups[-1].shared_state_class):
if not allow_multiple_groups:
raise ValueError('This StorySet is only allowed to have one '
'SharedState but contains the following '
'SharedState classes: %s, %s.\n Either '
'remove the extra SharedStates or override '
'allow_mixed_story_states.' % (
story_groups[-1].shared_state_class,
story.shared_state_class))
story_groups.append(
StoryGroup(story.shared_state_class))
story_groups[-1].AddStory(story)
return story_groups
def Run(test, story_set, finder_options, results, max_failures=None,
tear_down_after_story=False, tear_down_after_story_set=False):
"""Runs a given test against a given page_set with the given options.
Stop execution for unexpected exceptions such as KeyboardInterrupt.
We "white list" certain exceptions for which the story runner
can continue running the remaining stories.
"""
# Filter page set based on options.
stories = filter(story_module.StoryFilter.IsSelected, story_set)
if (not finder_options.use_live_sites and
finder_options.browser_options.wpr_mode != wpr_modes.WPR_RECORD):
serving_dirs = story_set.serving_dirs
if story_set.bucket:
for directory in serving_dirs:
cloud_storage.GetFilesInDirectoryIfChanged(directory,
story_set.bucket)
if story_set.archive_data_file and not _UpdateAndCheckArchives(
story_set.archive_data_file, story_set.wpr_archive_info,
stories):
return
if not stories:
return
# Effective max failures gives priority to command-line flag value.
effective_max_failures = finder_options.max_failures
if effective_max_failures is None:
effective_max_failures = max_failures
story_groups = StoriesGroupedByStateClass(
stories,
story_set.allow_mixed_story_states)
for group in story_groups:
state = None
try:
for storyset_repeat_counter in xrange(finder_options.pageset_repeat):
for story in group.stories:
for story_repeat_counter in xrange(finder_options.page_repeat):
if not state:
# Construct shared state by using a copy of finder_options. Shared
# state may update the finder_options. If we tear down the shared
# state after this story run, we want to construct the shared
# state for the next story from the original finder_options.
state = group.shared_state_class(
test, finder_options.Copy(), story_set)
results.WillRunPage(
story, storyset_repeat_counter, story_repeat_counter)
try:
_WaitForThermalThrottlingIfNeeded(state.platform)
_RunStoryAndProcessErrorIfNeeded(story, results, state, test)
except exceptions.Error:
# Catch all Telemetry errors to give the story a chance to retry.
# The retry is enabled by tearing down the state and creating
# a new state instance in the next iteration.
try:
# If TearDownState raises, do not catch the exception.
# (The Error was saved as a failure value.)
state.TearDownState()
finally:
# Later finally-blocks use state, so ensure it is cleared.
state = None
finally:
has_existing_exception = sys.exc_info() != (None, None, None)
try:
if state:
_CheckThermalThrottling(state.platform)
results.DidRunPage(story)
except Exception:
if not has_existing_exception:
raise
# Print current exception and propagate existing exception.
exception_formatter.PrintFormattedException(
msg='Exception from result processing:')
if state and tear_down_after_story:
state.TearDownState()
state = None
if (effective_max_failures is not None and
len(results.failures) > effective_max_failures):
logging.error('Too many failures. Aborting.')
return
if state and tear_down_after_story_set:
state.TearDownState()
state = None
finally:
if state:
has_existing_exception = sys.exc_info() != (None, None, None)
try:
state.TearDownState()
except Exception:
if not has_existing_exception:
raise
# Print current exception and propagate existing exception.
exception_formatter.PrintFormattedException(
msg='Exception from TearDownState:')
def RunBenchmark(benchmark, 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.
"""
benchmark.CustomizeBrowserOptions(finder_options.browser_options)
possible_browser = browser_finder.FindBrowser(finder_options)
if possible_browser and benchmark.ShouldDisable(possible_browser):
logging.warning('%s is disabled on the selected browser', benchmark.Name())
if finder_options.run_disabled_tests:
logging.warning(
'Running benchmark anyway due to: --also-run-disabled-tests')
else:
logging.warning(
'Try --also-run-disabled-tests to force the benchmark to run.')
return 1
pt = benchmark.CreatePageTest(finder_options)
pt.__name__ = benchmark.__class__.__name__
disabled_attr_name = decorators.DisabledAttributeName(benchmark)
# pylint: disable=protected-access
pt._disabled_strings = getattr(benchmark, disabled_attr_name, set())
if hasattr(benchmark, '_enabled_strings'):
# pylint: disable=protected-access
pt._enabled_strings = benchmark._enabled_strings
stories = benchmark.CreateStorySet(finder_options)
if isinstance(pt, legacy_page_test.LegacyPageTest):
if any(not isinstance(p, page.Page) for p in stories.stories):
raise Exception(
'PageTest must be used with StorySet containing only '
'telemetry.page.Page stories.')
should_tear_down_state_after_each_story_run = (
benchmark.ShouldTearDownStateAfterEachStoryRun())
# HACK: restarting shared state has huge overhead on cros (crbug.com/645329),
# hence we default this to False when test is run against CrOS.
# TODO(cros-team): figure out ways to remove this hack.
if (possible_browser.platform.GetOSName() == 'chromeos' and
not benchmark.IsShouldTearDownStateAfterEachStoryRunOverriden()):
should_tear_down_state_after_each_story_run = False
benchmark_metadata = benchmark.GetMetadata()
with results_options.CreateResults(
benchmark_metadata, finder_options,
benchmark.ValueCanBeAddedPredicate) as results:
try:
Run(pt, stories, finder_options, results, benchmark.max_failures,
should_tear_down_state_after_each_story_run,
benchmark.ShouldTearDownStateAfterEachStorySetRun())
return_code = min(254, len(results.failures))
except Exception:
exception_formatter.PrintFormattedException()
return_code = 255
try:
if finder_options.upload_results:
bucket = finder_options.upload_bucket
if bucket in cloud_storage.BUCKET_ALIASES:
bucket = cloud_storage.BUCKET_ALIASES[bucket]
results.UploadTraceFilesToCloud(bucket)
results.UploadProfilingFilesToCloud(bucket)
finally:
results.PrintSummary()
return return_code
def _UpdateAndCheckArchives(archive_data_file, wpr_archive_info,
filtered_stories):
"""Verifies that all stories are local or have WPR archives.
Logs warnings and returns False if any are missing.
"""
# Report any problems with the entire story set.
if any(not story.is_local for story in filtered_stories):
if not archive_data_file:
logging.error('The story set is missing an "archive_data_file" '
'property.\nTo run from live sites pass the flag '
'--use-live-sites.\nTo create an archive file add an '
'archive_data_file property to the story set and then '
'run record_wpr.')
raise ArchiveError('No archive data file.')
if not wpr_archive_info:
logging.error('The archive info file is missing.\n'
'To fix this, either add svn-internal to your '
'.gclient using http://goto/read-src-internal, '
'or create a new archive using record_wpr.')
raise ArchiveError('No archive info file.')
wpr_archive_info.DownloadArchivesIfNeeded()
# Report any problems with individual story.
stories_missing_archive_path = []
stories_missing_archive_data = []
for story in filtered_stories:
if not story.is_local:
archive_path = wpr_archive_info.WprFilePathForStory(story)
if not archive_path:
stories_missing_archive_path.append(story)
elif not os.path.isfile(archive_path):
stories_missing_archive_data.append(story)
if stories_missing_archive_path:
logging.error(
'The story set archives for some stories do not exist.\n'
'To fix this, record those stories using record_wpr.\n'
'To ignore this warning and run against live sites, '
'pass the flag --use-live-sites.')
logging.error(
'stories without archives: %s',
', '.join(story.display_name
for story in stories_missing_archive_path))
if stories_missing_archive_data:
logging.error(
'The story set archives for some stories are missing.\n'
'Someone forgot to check them in, uploaded them to the '
'wrong cloud storage bucket, or they were deleted.\n'
'To fix this, record those stories using record_wpr.\n'
'To ignore this warning and run against live sites, '
'pass the flag --use-live-sites.')
logging.error(
'stories missing archives: %s',
', '.join(story.display_name
for story in stories_missing_archive_data))
if stories_missing_archive_path or stories_missing_archive_data:
raise ArchiveError('Archive file is missing stories.')
# Only run valid stories if no problems with the story set or
# individual stories.
return True
def _WaitForThermalThrottlingIfNeeded(platform):
if not platform.CanMonitorThermalThrottling():
return
thermal_throttling_retry = 0
while (platform.IsThermallyThrottled() and
thermal_throttling_retry < 3):
logging.warning('Thermally throttled, waiting (%d)...',
thermal_throttling_retry)
thermal_throttling_retry += 1
time.sleep(thermal_throttling_retry * 2)
if thermal_throttling_retry and platform.IsThermallyThrottled():
logging.warning('Device is thermally throttled before running '
'performance tests, results will vary.')
def _CheckThermalThrottling(platform):
if not platform.CanMonitorThermalThrottling():
return
if platform.HasBeenThermallyThrottled():
logging.warning('Device has been thermally throttled during '
'performance tests, results will vary.')