blob: 454a08794304a9a2e2eb19fc0588dc1acc00940a [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 os
import shutil
import sys
import tempfile
import zipfile
from catapult_base import cloud_storage # pylint: disable=import-error
from telemetry.core import exceptions
from telemetry.core import util
from telemetry import decorators
from telemetry.internal.browser import browser_finder
from telemetry.internal.browser import browser_finder_exceptions
from telemetry.internal.browser import browser_info as browser_info_module
from telemetry.internal.platform.profiler import profiler_finder
from telemetry.internal.util import exception_formatter
from telemetry.internal.util import file_handle
from telemetry.page import page_test
from telemetry import story
from telemetry.util import image_util
from telemetry.util import wpr_modes
from telemetry.web_perf import timeline_based_measurement
def _PrepareFinderOptions(finder_options, test, device_type):
browser_options = finder_options.browser_options
# Set up user agent.
browser_options.browser_user_agent_type = device_type
test.CustomizeBrowserOptions(finder_options.browser_options)
if finder_options.profiler:
profiler_class = profiler_finder.FindProfiler(finder_options.profiler)
profiler_class.CustomizeBrowserOptions(browser_options.browser_type,
finder_options)
class SharedPageState(story.SharedState):
"""
This class contains all specific logic necessary to run a Chrome browser
benchmark.
"""
_device_type = None
def __init__(self, test, finder_options, story_set):
super(SharedPageState, self).__init__(test, finder_options, story_set)
if isinstance(test, timeline_based_measurement.TimelineBasedMeasurement):
assert not finder_options.profiler, (
'This is a Timeline Based Measurement benchmark. You cannot run it '
'with the --profiler flag. If you need trace data, tracing is always '
' enabled in Timeline Based Measurement benchmarks and you can get '
'the trace data by using --output-format=json.')
# This is to avoid the cyclic-import caused by timeline_based_page_test.
from telemetry.web_perf import timeline_based_page_test
self._test = timeline_based_page_test.TimelineBasedPageTest(test)
else:
self._test = test
device_type = self._device_type
# TODO(aiolos, nednguyen): Remove this logic of pulling out user_agent_type
# from story_set once all page_set are converted to story_set
# (crbug.com/439512).
def _IsPageSetInstance(s):
# This is needed to avoid importing telemetry.page.page_set which will
# cause cyclic import.
return 'PageSet' == s.__class__.__name__ or 'PageSet' in (
list(c.__name__ for c in s.__class__.__bases__))
if not device_type and _IsPageSetInstance(story_set):
device_type = story_set.user_agent_type
_PrepareFinderOptions(finder_options, self._test, device_type)
self._browser = None
self._finder_options = finder_options
self._possible_browser = self._GetPossibleBrowser(
self._test, finder_options)
self._first_browser = True
self._did_login_for_current_page = False
self._current_page = None
self._current_tab = None
self._migrated_profile = None
self._pregenerated_profile_archive_dir = None
self._test.SetOptions(self._finder_options)
# TODO(crbug/404771): Move network controller options out of
# browser_options and into finder_options.
browser_options = self._finder_options.browser_options
if self._finder_options.use_live_sites:
wpr_mode = wpr_modes.WPR_OFF
elif browser_options.wpr_mode == wpr_modes.WPR_RECORD:
wpr_mode = wpr_modes.WPR_RECORD
else:
wpr_mode = wpr_modes.WPR_REPLAY
self.platform.network_controller.Open(wpr_mode,
browser_options.extra_wpr_args)
@property
def browser(self):
return self._browser
def _FindBrowser(self, finder_options):
possible_browser = browser_finder.FindBrowser(finder_options)
if not possible_browser:
raise browser_finder_exceptions.BrowserFinderException(
'No browser found.\n\nAvailable browsers:\n%s\n' %
'\n'.join(browser_finder.GetAllAvailableBrowserTypes(finder_options)))
return possible_browser
def _GetPossibleBrowser(self, test, finder_options):
"""Return a possible_browser with the given options for |test|. """
possible_browser = self._FindBrowser(finder_options)
finder_options.browser_options.browser_type = (
possible_browser.browser_type)
enabled, msg = decorators.IsEnabled(test, possible_browser)
if not enabled and not finder_options.run_disabled_tests:
logging.warning(msg)
logging.warning('You are trying to run a disabled test.')
logging.warning(
'Pass --also-run-disabled-tests to squelch this message.')
sys.exit(0)
if possible_browser.IsRemote():
possible_browser.RunRemote()
sys.exit(0)
return possible_browser
def _TryCaptureScreenShot(self, page, tab, results):
try:
# TODO(nednguyen): once all platforms support taking screenshot,
# remove the tab checking logic and consider moving this to story_runner.
# (crbug.com/369490)
if tab.browser.platform.CanTakeScreenshot():
tf = tempfile.NamedTemporaryFile(delete=False, suffix='.png')
tf.close()
tab.browser.platform.TakeScreenshot(tf.name)
results.AddProfilingFile(page, file_handle.FromTempFile(tf))
elif tab.IsAlive() and tab.screenshot_supported:
tf = tempfile.NamedTemporaryFile(delete=False, suffix='.png')
tf.close()
image = tab.Screenshot()
image_util.WritePngFile(image, tf.name)
results.AddProfilingFile(page, file_handle.FromTempFile(tf))
else:
logging.warning(
'Either tab has crashed or browser does not support taking tab '
'screenshot. Skip taking screenshot on failure.')
except Exception as e:
logging.warning('Exception when trying to capture screenshot: %s',
repr(e))
def DidRunStory(self, results):
if self._finder_options.profiler:
self._StopProfiling(results)
# We might hang while trying to close the connection, and need to guarantee
# the page will get cleaned up to avoid future tests failing in weird ways.
try:
if self._current_tab and self._current_tab.IsAlive():
self._current_tab.CloseConnections()
except Exception:
if self._current_tab:
self._current_tab.Close()
finally:
if self._current_page.credentials and self._did_login_for_current_page:
self.browser.credentials.LoginNoLongerNeeded(
self._current_tab, self._current_page.credentials)
if self._test.StopBrowserAfterPage(self.browser, self._current_page):
self._StopBrowser()
self._current_page = None
self._current_tab = None
@property
def platform(self):
return self._possible_browser.platform
def _StartBrowser(self, page):
assert self._browser is None
self._possible_browser.SetCredentialsPath(page.credentials_path)
self._test.WillStartBrowser(self.platform)
if page.startup_url:
self._finder_options.browser_options.startup_url = page.startup_url
self._browser = self._possible_browser.Create(self._finder_options)
self._test.DidStartBrowser(self.browser)
if self._first_browser:
self._first_browser = False
self.browser.credentials.WarnIfMissingCredentials(page)
logging.info('OS: %s %s',
self.platform.GetOSName(),
self.platform.GetOSVersionName())
if self.browser.supports_system_info:
system_info = self.browser.GetSystemInfo()
if system_info.model_name:
logging.info('Model: %s', system_info.model_name)
if system_info.gpu:
for i, device in enumerate(system_info.gpu.devices):
logging.info('GPU device %d: %s', i, device)
if system_info.gpu.aux_attributes:
logging.info('GPU Attributes:')
for k, v in sorted(system_info.gpu.aux_attributes.iteritems()):
logging.info(' %-20s: %s', k, v)
if system_info.gpu.feature_status:
logging.info('Feature Status:')
for k, v in sorted(system_info.gpu.feature_status.iteritems()):
logging.info(' %-20s: %s', k, v)
if system_info.gpu.driver_bug_workarounds:
logging.info('Driver Bug Workarounds:')
for workaround in system_info.gpu.driver_bug_workarounds:
logging.info(' %s', workaround)
else:
logging.info('No GPU devices')
else:
logging.warning('System info not supported')
def WillRunStory(self, page):
if not self.platform.tracing_controller.is_tracing_running:
# For TimelineBasedMeasurement benchmarks, tracing has already started.
# For PageTest benchmarks, tracing has not yet started. We need to make
# sure no tracing state is left before starting the browser for PageTest
# benchmarks.
self.platform.tracing_controller.ClearStateIfNeeded()
if self._ShouldDownloadPregeneratedProfileArchive():
self._DownloadPregeneratedProfileArchive()
if self._ShouldMigrateProfile():
self._MigratePregeneratedProfile()
page_set = page.page_set
self._current_page = page
if self._browser and (self._test.RestartBrowserBeforeEachPage()
or page.startup_url):
assert not self.platform.tracing_controller.is_tracing_running, (
'Should not restart browser when tracing is already running. For '
'TimelineBasedMeasurement (TBM) benchmarks, you should not use '
'startup_url. Use benchmark.ShouldTearDownStateAfterEachStoryRun '
'instead.')
self._StopBrowser()
started_browser = not self.browser
archive_path = page_set.WprFilePathForStory(page)
# TODO(nednguyen, perezju): Ideally we should just let the network
# controller raise an exception when the archive_path is not found.
if archive_path is not None and not os.path.isfile(archive_path):
logging.warning('WPR archive missing: %s', archive_path)
archive_path = None
self.platform.network_controller.StartReplay(
archive_path, page.make_javascript_deterministic)
if self.browser:
# Set new credential path for browser.
self.browser.credentials.credentials_path = page.credentials_path
else:
self._StartBrowser(page)
if self.browser.supports_tab_control and self._test.close_tabs_before_run:
# Create a tab if there's none.
if len(self.browser.tabs) == 0:
self.browser.tabs.New()
# Ensure only one tab is open, unless the test is a multi-tab test.
if not self._test.is_multi_tab_test:
while len(self.browser.tabs) > 1:
self.browser.tabs[-1].Close()
# Must wait for tab to commit otherwise it can commit after the next
# navigation has begun and RenderFrameHostManager::DidNavigateMainFrame()
# will cancel the next navigation because it's pending. This manifests as
# the first navigation in a PageSet freezing indefinitely because the
# navigation was silently canceled when |self.browser.tabs[0]| was
# committed. Only do this when we just started the browser, otherwise
# there are cases where previous pages in a PageSet never complete
# loading so we'll wait forever.
if started_browser:
self.browser.tabs[0].WaitForDocumentReadyStateToBeComplete()
# Start profiling if needed.
if self._finder_options.profiler:
self._StartProfiling(self._current_page)
def CanRunStory(self, page):
return self.CanRunOnBrowser(browser_info_module.BrowserInfo(self.browser),
page)
def CanRunOnBrowser(self, browser_info,
page): # pylint: disable=unused-argument
"""Override this to return whether the browser brought up by this state
instance is suitable for running the given page.
Args:
browser_info: an instance of telemetry.core.browser_info.BrowserInfo
page: an instance of telemetry.page.Page
"""
del browser_info, page # unused
return True
def _PreparePage(self):
self._current_tab = self._test.TabForPage(self._current_page, self.browser)
if self._current_page.is_file:
self.platform.SetHTTPServerDirectories(
self._current_page.page_set.serving_dirs |
set([self._current_page.serving_dir]))
if self._current_page.credentials:
if not self.browser.credentials.LoginNeeded(
self._current_tab, self._current_page.credentials):
raise page_test.Failure(
'Login as ' + self._current_page.credentials + ' failed')
self._did_login_for_current_page = True
if self._test.clear_cache_before_each_run:
self._current_tab.ClearCache(force=True)
@property
def current_page(self):
return self._current_page
@property
def current_tab(self):
return self._current_tab
@property
def page_test(self):
return self._test
def RunStory(self, results):
try:
self._PreparePage()
self._current_page.Run(self)
self._test.ValidateAndMeasurePage(
self._current_page, self._current_tab, results)
except exceptions.Error:
if self._finder_options.browser_options.take_screenshot_for_failed_page:
self._TryCaptureScreenShot(self._current_page, self._current_tab,
results)
if self._test.is_multi_tab_test:
# Avoid trying to recover from an unknown multi-tab state.
exception_formatter.PrintFormattedException(
msg='Telemetry Error during multi tab test:')
raise page_test.MultiTabTestAppCrashError
raise
except Exception:
if self._finder_options.browser_options.take_screenshot_for_failed_page:
self._TryCaptureScreenShot(self._current_page, self._current_tab,
results)
raise
def TearDownState(self):
if self._migrated_profile:
shutil.rmtree(self._migrated_profile)
self._migrated_profile = None
self._StopBrowser()
self.platform.StopAllLocalServers()
self.platform.network_controller.Close()
def _StopBrowser(self):
if self._browser:
self._browser.Close()
self._browser = None
def _StartProfiling(self, page):
output_file = os.path.join(self._finder_options.output_dir,
page.file_safe_name)
is_repeating = (self._finder_options.page_repeat != 1 or
self._finder_options.pageset_repeat != 1)
if is_repeating:
output_file = util.GetSequentialFileName(output_file)
self.browser.profiling_controller.Start(
self._finder_options.profiler, output_file)
def _StopProfiling(self, results):
if self.browser:
profiler_files = self.browser.profiling_controller.Stop()
for f in profiler_files:
if os.path.isfile(f):
results.AddProfilingFile(self._current_page,
file_handle.FromFilePath(f))
def _ShouldMigrateProfile(self):
return not self._migrated_profile
def _MigrateProfile(self, finder_options, found_browser,
initial_profile, final_profile):
"""Migrates a profile to be compatible with a newer version of Chrome.
Launching Chrome with the old profile will perform the migration.
"""
# Save the current input and output profiles.
saved_input_profile = finder_options.browser_options.profile_dir
saved_output_profile = finder_options.output_profile_path
# Set the input and output profiles.
finder_options.browser_options.profile_dir = initial_profile
finder_options.output_profile_path = final_profile
# Launch the browser, then close it.
browser = found_browser.Create(finder_options)
browser.Close()
# Load the saved input and output profiles.
finder_options.browser_options.profile_dir = saved_input_profile
finder_options.output_profile_path = saved_output_profile
def _MigratePregeneratedProfile(self):
"""Migrates the pre-generated profile by launching Chrome with it.
On success, updates self._migrated_profile and
self._finder_options.browser_options.profile_dir with the directory of the
migrated profile.
"""
self._migrated_profile = tempfile.mkdtemp()
logging.info("Starting migration of pre-generated profile to %s",
self._migrated_profile)
pregenerated_profile = self._finder_options.browser_options.profile_dir
possible_browser = self._FindBrowser(self._finder_options)
self._MigrateProfile(self._finder_options, possible_browser,
pregenerated_profile, self._migrated_profile)
self._finder_options.browser_options.profile_dir = self._migrated_profile
logging.info("Finished migration of pre-generated profile to %s",
self._migrated_profile)
def GetPregeneratedProfileArchiveDir(self):
return self._pregenerated_profile_archive_dir
def SetPregeneratedProfileArchiveDir(self, archive_path):
"""
Benchmarks can set a pre-generated profile archive to indicate that when
Chrome is launched, it should have a --user-data-dir set to the
pre-generated profile, rather than to an empty profile.
If the benchmark is invoked with the option --profile-dir=<dir>, that
option overrides this value.
"""
self._pregenerated_profile_archive_dir = archive_path
def _ShouldDownloadPregeneratedProfileArchive(self):
"""Whether to download a pre-generated profile archive."""
# There is no pre-generated profile archive.
if not self.GetPregeneratedProfileArchiveDir():
return False
# If profile dir is specified on command line, use that instead.
if self._finder_options.browser_options.profile_dir:
logging.warning("Profile directory specified on command line: %s, this"
"overrides the benchmark's default profile directory.",
self._finder_options.browser_options.profile_dir)
return False
# If the browser is remote, a local download has no effect.
if self._possible_browser.IsRemote():
return False
return True
def _DownloadPregeneratedProfileArchive(self):
"""Download and extract the profile directory archive if one exists.
On success, updates self._finder_options.browser_options.profile_dir with
the directory of the extracted profile.
"""
# Download profile directory from cloud storage.
generated_profile_archive_path = self.GetPregeneratedProfileArchiveDir()
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)
# Check to make sure the zip file exists.
if not os.path.isfile(generated_profile_archive_path):
raise Exception("Profile directory archive not downloaded: ",
generated_profile_archive_path)
# The location to extract the profile into.
extracted_profile_dir_path = (
os.path.splitext(generated_profile_archive_path)[0])
# Unzip profile directory.
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)
self._finder_options.browser_options.profile_dir = (
extracted_profile_dir_path)
class SharedMobilePageState(SharedPageState):
_device_type = 'mobile'
class SharedDesktopPageState(SharedPageState):
_device_type = 'desktop'
class SharedTabletPageState(SharedPageState):
_device_type = 'tablet'
class Shared10InchTabletPageState(SharedPageState):
_device_type = 'tablet_10_inch'