blob: 8a941ac9c5627797b6abf2fa6ccf8c9949313b68 [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
from six.moves import input # pylint: disable=redefined-builtin
from telemetry.core import exceptions
from telemetry.core import platform as platform_module
from telemetry.internal.backends.chrome import gpu_compositing_checker
from telemetry.internal.browser import browser_info as browser_info_module
from telemetry.internal.browser import browser_interval_profiling_controller
from telemetry.internal.platform import android_device
from telemetry.page import cache_temperature
from telemetry.page import legacy_page_test
from telemetry.page import traffic_setting
from telemetry import story as story_module
from telemetry.util import screenshot
class SharedPageState(story_module.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, possible_browser):
super(SharedPageState, self).__init__(
test, finder_options, story_set, possible_browser)
self._page_test = None
if issubclass(type(test), legacy_page_test.LegacyPageTest):
# We only need a page_test for legacy measurements that involve running
# some commands before/after starting the browser or navigating to a page.
# This is not needed for newer timeline (tracing) based benchmarks which
# just collect a trace, then measurements are done after the fact by
# analysing the trace itself.
self._page_test = test
self._page_test_results = None
if (self._device_type == 'desktop' and
platform_module.GetHostPlatform().GetOSName() == 'chromeos'):
self._device_type = 'chromeos'
browser_options = finder_options.browser_options
browser_options.browser_user_agent_type = self._device_type
if self._page_test:
self._page_test.CustomizeBrowserOptions(browser_options)
self._browser = None
self._finder_options = finder_options
self._first_browser = True
self._current_page = None
self._current_tab = None
if self._page_test:
self._page_test.SetOptions(self._finder_options)
self._extra_wpr_args = browser_options.extra_wpr_args
if (hasattr(finder_options, 'use_local_wpr') and
finder_options.use_local_wpr):
self._extra_wpr_args.append('--use-local-wpr')
if (hasattr(finder_options, 'disable_fuzzy_url_matching') and
finder_options.disable_fuzzy_url_matching):
self._extra_wpr_args.append('--disable-fuzzy-url-matching')
profiling_mod = browser_interval_profiling_controller
self._interval_profiling_controller = (
profiling_mod.BrowserIntervalProfilingController(
possible_browser=self._possible_browser,
process_name=finder_options.interval_profiling_target,
periods=finder_options.interval_profiling_periods,
frequency=finder_options.interval_profiling_frequency,
profiler_options=finder_options.interval_profiler_options))
self.platform.SetPerformanceMode(finder_options.performance_mode)
self._perf_mode_set = (finder_options.performance_mode !=
android_device.KEEP_PERFORMANCE_MODE)
self.platform.network_controller.Open(self.wpr_mode)
self.platform.Initialize()
self._video_recording_enabled = (self._finder_options.capture_screen_video
and self.platform.CanRecordVideo())
@property
def interval_profiling_controller(self):
return self._interval_profiling_controller
@property
def browser(self):
return self._browser
def DumpStateUponStoryRunFailure(self, results):
# Dump browser standard output and log.
if self._browser:
self._browser.DumpStateUponFailure()
else:
logging.warning('Cannot dump browser state: No browser.')
# Capture a screenshot
if self._finder_options.browser_options.take_screenshot_for_failed_page:
fh = screenshot.TryCaptureScreenShot(self.platform, self._current_tab)
if fh is not None:
with results.CaptureArtifact('screenshot.png') as path:
shutil.move(fh.GetAbsPath(), path)
else:
logging.warning('Taking screenshots upon failures disabled.')
def DidRunStory(self, results):
self._AllowInteractionForStage('after-run-story')
try:
if not self.ShouldReuseBrowserForAllStoryRuns():
self._StopBrowser()
elif self._current_tab:
# 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.IsAlive():
self._current_tab.CloseConnections()
except Exception as exc: # pylint: disable=broad-except
logging.warning(
'%s raised while closing tab connections; tab will be closed.',
type(exc).__name__)
self._current_tab.Close()
self._interval_profiling_controller.GetResults(
self._current_page.file_safe_name, results)
finally:
self._current_page = None
self._current_tab = None
if self._video_recording_enabled:
with results.CaptureArtifact('recording.mp4') as video_path:
self.platform.StopVideoRecording(video_path)
def ShouldReuseBrowserForAllStoryRuns(self):
"""Whether a single browser instance should be reused to run all stories.
This should return False in most situations in order to help maitain
independence between measurements taken on different story runs.
The default implementation only allows reusing the browser in ChromeOs,
where bringing up the browser for each story is expensive.
"""
return self.platform.GetOSName() == 'chromeos'
@property
def platform(self):
return self._possible_browser.platform
def _AllowInteractionForStage(self, stage):
if self._finder_options.pause == stage:
input('Pausing for interaction at %s... Press Enter to continue.' %
stage)
def _StartBrowser(self, page):
assert self._browser is None
self._AllowInteractionForStage('before-start-browser')
if self._page_test:
self._page_test.WillStartBrowser(self.platform)
# Create a deep copy of browser_options so that we can add page-level
# arguments and url to it without polluting the run for the next page.
browser_options = self._finder_options.browser_options.Copy()
browser_options.AppendExtraBrowserArgs(page.extra_browser_args)
self._possible_browser.SetUpEnvironment(browser_options)
# Clear caches before starting browser.
self.platform.FlushDnsCache()
if browser_options.flush_os_page_caches_on_start:
self._possible_browser.FlushOsPageCaches()
self._browser = self._possible_browser.Create()
if self._page_test:
self._page_test.DidStartBrowser(self.browser)
if browser_options.assert_gpu_compositing:
gpu_compositing_checker.AssertGpuCompositingEnabled(
self._browser.GetSystemInfo())
if self._first_browser:
self._first_browser = False
# Cut back on mostly redundant logs length per crbug.com/943650.
self._finder_options.browser_options.trim_logs = True
self._AllowInteractionForStage('after-start-browser')
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()
self._current_page = page
archive_path = page.story_set.WprFilePathForStory(
page, self.platform.GetOSName())
# TODO(crbug.com/1029785): 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, self._extra_wpr_args)
reusing_browser = self.browser is not None
if self._video_recording_enabled:
self.platform.StartVideoRecording()
if not reusing_browser:
self._StartBrowser(page)
if self.browser.supports_tab_control:
if reusing_browser:
# Try to close all previous tabs to maintain some independence between
# individual story runs. Note that the final tab.Close(keep_one=True)
# will create a fresh new tab before the last one is closed.
while len(self.browser.tabs) > 1:
self.browser.tabs[-1].Close()
self.browser.tabs[-1].Close(keep_one=True)
else:
# Create a tab if there's none.
if len(self.browser.tabs) == 0:
self.browser.tabs.New()
# 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.
self.browser.tabs[0].WaitForDocumentReadyStateToBeComplete()
# Reset traffic shaping to speed up cache temperature setup.
self.platform.network_controller.UpdateTrafficSettings(0, 0, 0)
cache_temperature.EnsurePageCacheTemperature(
self._current_page, self.browser)
if self._current_page.traffic_setting != traffic_setting.NONE:
s = traffic_setting.NETWORK_CONFIGS[self._current_page.traffic_setting]
self.platform.network_controller.UpdateTrafficSettings(
round_trip_latency_ms=s.round_trip_latency_ms,
download_bandwidth_kbps=s.download_bandwidth_kbps,
upload_bandwidth_kbps=s.upload_bandwidth_kbps)
self._AllowInteractionForStage('before-run-story')
def CanRunStory(self, page):
return self.CanRunOnBrowser(browser_info_module.BrowserInfo(self.browser),
page)
def CanRunOnBrowser(self, browser_info, page):
"""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 _GetCurrentTab(self):
try:
return self.browser.tabs[0]
# The tab may have gone away in some case, so we create a new tab and retry
# (See crbug.com/496280)
except exceptions.DevtoolsTargetCrashException as e:
logging.error('Tab may have crashed: %s' % str(e))
self.browser.tabs.New()
# See below in WillRunStory for why this waiting is needed.
self.browser.tabs[0].WaitForDocumentReadyStateToBeComplete()
return self.browser.tabs[0]
def _PreparePage(self):
self._current_tab = self._GetCurrentTab()
if self._current_page.is_file:
self.platform.SetHTTPServerDirectories(
self._current_page.story_set.serving_dirs
| {self._current_page.serving_dir},
self._current_page.story_set.request_handler_class)
@property
def current_page(self):
return self._current_page
@property
def current_tab(self):
return self._current_tab
def NavigateToPage(self, action_runner, page):
# Method called by page.Run(), lives in shared_state to avoid exposing
# references to the legacy self._page_test object.
if self._page_test:
self._page_test.WillNavigateToPage(page, action_runner.tab)
with self.interval_profiling_controller.SamplePeriod(
'navigation', action_runner):
page.RunNavigateSteps(action_runner)
if self._page_test:
self._page_test.DidNavigateToPage(page, action_runner.tab)
def RunPageInteractions(self, action_runner, page):
# The purpose is similar to NavigateToPage.
with self.interval_profiling_controller.SamplePeriod(
'interactions', action_runner):
page.RunPageInteractions(action_runner)
if self._page_test:
self._page_test.ValidateAndMeasurePage(
page, action_runner.tab, self._page_test_results)
def RunStory(self, results):
self._PreparePage()
self._page_test_results = results
self._current_page.Run(self)
self._page_test_results = None
def TearDownState(self):
self._StopBrowser()
self.platform.StopAllLocalServers()
self.platform.network_controller.Close()
if self._perf_mode_set:
self.platform.SetPerformanceMode(android_device.NORMAL_PERFORMANCE_MODE)
def _StopBrowser(self):
if self._browser:
self._browser.Close()
self._browser = None
if self._possible_browser:
self._possible_browser.CleanUpEnvironment()
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'