blob: be7ea2d1ed6ceb37aa9144af33dfd286a48752ef [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.
import logging
import pprint
import shlex
import socket
import sys
from telemetry.core import exceptions
from telemetry import decorators
from telemetry.internal.backends import browser_backend
from telemetry.internal.backends.chrome import extension_backend
from telemetry.internal.backends.chrome import tab_list_backend
from telemetry.internal.backends.chrome_inspector import devtools_client_backend
from telemetry.internal.backends.chrome_inspector import inspector_websocket
from telemetry.internal.browser import web_contents
from telemetry.testing import options_for_unittests
import py_utils
class ChromeBrowserBackend(browser_backend.BrowserBackend):
"""An abstract class for chrome browser backends. Provides basic functionality
once a remote-debugger port has been established."""
# It is OK to have abstract methods. pylint: disable=abstract-method
def __init__(self, platform_backend, browser_options,
browser_directory, profile_directory,
supports_extensions, supports_tab_control):
super(ChromeBrowserBackend, self).__init__(
platform_backend=platform_backend,
browser_options=browser_options,
supports_extensions=supports_extensions,
tab_list_backend=tab_list_backend.TabListBackend)
self._browser_directory = browser_directory
self._profile_directory = profile_directory
self._supports_tab_control = supports_tab_control
self._devtools_client = None
self._extensions_to_load = browser_options.extensions_to_load
if not supports_extensions and len(self._extensions_to_load) > 0:
raise browser_backend.ExtensionsNotSupportedException(
'Extensions are not supported on the selected browser')
if (self.browser_options.dont_override_profile and
not options_for_unittests.AreSet()):
sys.stderr.write('Warning: Not overriding profile. This can cause '
'unexpected effects due to profile-specific settings, '
'such as about:flags settings, cookies, and '
'extensions.\n')
@property
def devtools_client(self):
return self._devtools_client
@property
@decorators.Cache
def extension_backend(self):
if not self.supports_extensions:
return None
return extension_backend.ExtensionBackendDict(self)
def _ArgsNeedProxyServer(self, args):
"""Returns True if args for Chrome indicate the need for proxy server."""
if '--enable-spdy-proxy-auth' in args:
return True
return [arg for arg in args if arg.startswith('--proxy-server=')]
def HasDevToolsConnection(self):
return self._devtools_client and self._devtools_client.IsAlive()
def _FindDevToolsPortAndTarget(self):
"""Clients should return a (devtools_port, browser_target) pair.
May also raise EnvironmentError (IOError or OSError) if this information
could not be determined; the call will be retried until it succeeds or
a timeout is met.
"""
raise NotImplementedError
def _GetDevToolsClient(self):
# If the agent does not appear to be ready, it could be because we got the
# details of an older agent that no longer exists. It's thus important to
# re-read and update the port and target on each retry.
try:
devtools_port, browser_target = self._FindDevToolsPortAndTarget()
except EnvironmentError:
return None # Port information not ready, will retry.
return devtools_client_backend.GetDevToolsBackEndIfReady(
devtools_port=devtools_port,
app_backend=self,
browser_target=browser_target)
def BindDevToolsClient(self):
"""Find an existing DevTools agent and bind this browser backend to it."""
if self._devtools_client:
# In case we are launching a second browser instance (as is done by
# the CrOS backend), ensure that the old devtools_client is closed,
# otherwise re-creating it will fail.
self._devtools_client.Close()
self._devtools_client = None
try:
self._devtools_client = py_utils.WaitFor(
self._GetDevToolsClient,
timeout=self.browser_options.browser_startup_timeout)
except (py_utils.TimeoutException, exceptions.ProcessGoneException) as e:
if not self.IsBrowserRunning():
raise exceptions.BrowserGoneException(self.browser, e)
raise exceptions.BrowserConnectionGoneException(self.browser, e)
def _WaitForExtensionsToLoad(self):
""" Wait for all extensions to load.
Be sure to check whether the browser_backend supports_extensions before
calling this method.
"""
assert self._supports_extensions
assert self._devtools_client, (
'Waiting for extensions required devtool client to be initiated first')
try:
py_utils.WaitFor(self._AllExtensionsLoaded, timeout=60)
except py_utils.TimeoutException:
logging.error('ExtensionsToLoad: ' + repr(
[e.extension_id for e in self._extensions_to_load]))
logging.error('Extension list: ' + pprint.pformat(
self.extension_backend, indent=4))
raise
def _AllExtensionsLoaded(self):
# Extension pages are loaded from an about:blank page,
# so we need to check that the document URL is the extension
# page in addition to the ready state.
for e in self._extensions_to_load:
try:
extension_objects = self.extension_backend[e.extension_id]
except KeyError:
return False
for extension_object in extension_objects:
try:
res = extension_object.EvaluateJavaScript(
"""
document.URL.lastIndexOf({{ url }}, 0) == 0 &&
(document.readyState == 'complete' ||
document.readyState == 'interactive')
""",
url='chrome-extension://%s/' % e.extension_id)
except exceptions.EvaluateException:
# If the inspected page is not ready, we will get an error
# when we evaluate a JS expression, but we can just keep polling
# until the page is ready (crbug.com/251913).
res = None
# TODO(tengs): We don't have full support for getting the Chrome
# version before launch, so for now we use a generic workaround to
# check for an extension binding bug in old versions of Chrome.
# See crbug.com/263162 for details.
if res and extension_object.EvaluateJavaScript(
'chrome.runtime == null'):
extension_object.Reload()
if not res:
return False
return True
@property
def browser_directory(self):
return self._browser_directory
@property
def profile_directory(self):
return self._profile_directory
@property
def supports_tab_control(self):
return self._supports_tab_control
@property
def supports_tracing(self):
return True
def GetProcessName(self, cmd_line):
"""Returns a user-friendly name for the process of the given |cmd_line|."""
if not cmd_line:
# TODO(tonyg): Eventually we should make all of these known and add an
# assertion.
return 'unknown'
if 'nacl_helper_bootstrap' in cmd_line:
return 'nacl_helper_bootstrap'
if ':sandboxed_process' in cmd_line:
return 'renderer'
if ':privileged_process' in cmd_line:
return 'gpu-process'
args = shlex.split(cmd_line)
types = [arg.split('=')[1] for arg in args if arg.startswith('--type=')]
if not types:
return 'browser'
return types[0]
@staticmethod
def GetThreadType(thread_name):
if not thread_name:
return 'unknown'
if (thread_name.startswith('Chrome_ChildIO') or
thread_name.startswith('Chrome_IO')):
return 'io'
if thread_name.startswith('Compositor'):
return 'compositor'
if thread_name.startswith('CrGpuMain'):
return 'gpu'
if (thread_name.startswith('ChildProcessMai') or
thread_name.startswith('CrRendererMain')):
return 'main'
if thread_name.startswith('RenderThread'):
return 'render'
def Close(self):
# If Chrome tracing is running, flush the trace before closing the browser.
tracing_backend = self._platform_backend.tracing_controller_backend
if tracing_backend.is_chrome_tracing_running:
tracing_backend.FlushTracing()
if self._devtools_client:
self._devtools_client.Close()
self._devtools_client = None
def GetSystemInfo(self):
try:
return self.devtools_client.GetSystemInfo()
except (inspector_websocket.WebSocketException, socket.error) as e:
if not self.IsBrowserRunning():
raise exceptions.BrowserGoneException(self.browser, e)
raise exceptions.BrowserConnectionGoneException(self.browser, e)
@property
def supports_memory_dumping(self):
return True
def DumpMemory(self, timeout=None):
return self.devtools_client.DumpMemory(timeout=timeout)
@property
def supports_overriding_memory_pressure_notifications(self):
return True
def SetMemoryPressureNotificationsSuppressed(
self, suppressed, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT):
self.devtools_client.SetMemoryPressureNotificationsSuppressed(
suppressed, timeout)
def SimulateMemoryPressureNotification(
self, pressure_level, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT):
self.devtools_client.SimulateMemoryPressureNotification(
pressure_level, timeout)
def GetDirectoryPathsToFlushOsPageCacheFor(self):
""" Return a list of directories to purge from OS page cache.
Will only be called when page cache clearing is necessary for a benchmark.
The caller will then attempt to purge all files from OS page cache for each
returned directory recursively.
"""
paths_to_flush = []
if self.profile_directory:
paths_to_flush.append(self.profile_directory)
if self.browser_directory:
paths_to_flush.append(self.browser_directory)
return paths_to_flush
# TODO(crbug.com/787834): consider migrating profile_directory &
# browser_directory out of browser_backend so we don't have to rely on
# creating browser_backend before clearing browser caches.
def ClearCaches(self):
""" Clear system caches related to browser.
This clears DNS caches, then clears OS page cache on file paths that are
related to the browser (iff
browser_options.clear_sytem_cache_for_browser_and_profile_on_start is True).
Note: this is done with best effort and may have no actual effects on the
system.
"""
platform = self.platform_backend.platform
platform.FlushDnsCache()
if self.browser_options.clear_sytem_cache_for_browser_and_profile_on_start:
paths_to_flush = self.GetDirectoryPathsToFlushOsPageCacheFor()
if (platform.CanFlushIndividualFilesFromSystemCache() and
paths_to_flush):
platform.FlushSystemCacheForDirectories(paths_to_flush)
elif platform.SupportFlushEntireSystemCache():
platform.FlushEntireSystemCache()
else:
logging.warning(
'Flush system cache is not supported. Did not flush OS page cache.')
@property
def supports_cpu_metrics(self):
return True
@property
def supports_memory_metrics(self):
return True
@property
def supports_power_metrics(self):
return True