aboutsummaryrefslogtreecommitdiff
path: root/catapult/telemetry/telemetry/internal/backends/chrome_inspector/devtools_client_backend.py
diff options
context:
space:
mode:
Diffstat (limited to 'catapult/telemetry/telemetry/internal/backends/chrome_inspector/devtools_client_backend.py')
-rw-r--r--catapult/telemetry/telemetry/internal/backends/chrome_inspector/devtools_client_backend.py490
1 files changed, 490 insertions, 0 deletions
diff --git a/catapult/telemetry/telemetry/internal/backends/chrome_inspector/devtools_client_backend.py b/catapult/telemetry/telemetry/internal/backends/chrome_inspector/devtools_client_backend.py
new file mode 100644
index 00000000..dc24172f
--- /dev/null
+++ b/catapult/telemetry/telemetry/internal/backends/chrome_inspector/devtools_client_backend.py
@@ -0,0 +1,490 @@
+# 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 re
+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_inspector import devtools_http
+from telemetry.internal.backends.chrome_inspector import inspector_backend
+from telemetry.internal.backends.chrome_inspector import inspector_websocket
+from telemetry.internal.backends.chrome_inspector import memory_backend
+from telemetry.internal.backends.chrome_inspector import tracing_backend
+from telemetry.internal.backends.chrome_inspector import websocket
+from telemetry.internal.platform.tracing_agent import chrome_tracing_agent
+from telemetry.internal.platform.tracing_agent import (
+ chrome_tracing_devtools_manager)
+from telemetry.timeline import trace_data as trace_data_module
+
+
+BROWSER_INSPECTOR_WEBSOCKET_URL = 'ws://127.0.0.1:%i/devtools/browser'
+
+
+class TabNotFoundError(exceptions.Error):
+ pass
+
+
+def IsDevToolsAgentAvailable(port, app_backend):
+ """Returns True if a DevTools agent is available on the given port."""
+ if (isinstance(app_backend, browser_backend.BrowserBackend) and
+ app_backend.supports_tracing):
+ inspector_websocket_instance = inspector_websocket.InspectorWebsocket()
+ try:
+ if not _IsInspectorWebsocketAvailable(inspector_websocket_instance, port):
+ return False
+ finally:
+ inspector_websocket_instance.Disconnect()
+
+ devtools_http_instance = devtools_http.DevToolsHttp(port)
+ try:
+ return _IsDevToolsAgentAvailable(devtools_http_instance)
+ finally:
+ devtools_http_instance.Disconnect()
+
+
+def _IsInspectorWebsocketAvailable(inspector_websocket_instance, port):
+ try:
+ inspector_websocket_instance.Connect(
+ BROWSER_INSPECTOR_WEBSOCKET_URL % port, timeout=10)
+ except websocket.WebSocketException:
+ return False
+ except socket.error:
+ return False
+ except Exception as e:
+ sys.stderr.write('Unidentified exception while checking if wesocket is'
+ 'available on port %i. Exception message: %s\n' %
+ (port, e.message))
+ return False
+ else:
+ return True
+
+
+# TODO(nednguyen): Find a more reliable way to check whether the devtool agent
+# is still alive.
+def _IsDevToolsAgentAvailable(devtools_http_instance):
+ try:
+ devtools_http_instance.Request('')
+ except devtools_http.DevToolsClientConnectionError:
+ return False
+ else:
+ return True
+
+
+class DevToolsClientBackend(object):
+ """An object that communicates with Chrome's devtools.
+
+ This class owns a map of InspectorBackends. It is responsible for creating
+ them and destroying them.
+ """
+ def __init__(self, devtools_port, remote_devtools_port, app_backend):
+ """Creates a new DevToolsClientBackend.
+
+ A DevTools agent must exist on the given devtools_port.
+
+ Args:
+ devtools_port: The port to use to connect to DevTools agent.
+ remote_devtools_port: In some cases (e.g., app running on
+ Android device, devtools_port is the forwarded port on the
+ host platform. We also need to know the remote_devtools_port
+ so that we can uniquely identify the DevTools agent.
+ app_backend: For the app that contains the DevTools agent.
+ """
+ self._devtools_port = devtools_port
+ self._remote_devtools_port = remote_devtools_port
+ self._devtools_http = devtools_http.DevToolsHttp(devtools_port)
+ self._browser_inspector_websocket = None
+ self._tracing_backend = None
+ self._memory_backend = None
+ self._app_backend = app_backend
+ self._devtools_context_map_backend = _DevToolsContextMapBackend(
+ self._app_backend, self)
+
+ self._tab_ids = None
+
+ if not self.supports_tracing:
+ return
+ chrome_tracing_devtools_manager.RegisterDevToolsClient(
+ self, self._app_backend.platform_backend)
+
+ # Telemetry has started Chrome tracing if there is trace config, so start
+ # tracing on this newly created devtools client if needed.
+ trace_config = (self._app_backend.platform_backend
+ .tracing_controller_backend.GetChromeTraceConfig())
+ if not trace_config:
+ self._CreateTracingBackendIfNeeded(is_tracing_running=False)
+ return
+
+ if self.support_startup_tracing:
+ self._CreateTracingBackendIfNeeded(is_tracing_running=True)
+ return
+
+ self._CreateTracingBackendIfNeeded(is_tracing_running=False)
+ self.StartChromeTracing(trace_config)
+
+ @property
+ def remote_port(self):
+ return self._remote_devtools_port
+
+ @property
+ def supports_tracing(self):
+ if not isinstance(self._app_backend, browser_backend.BrowserBackend):
+ return False
+ return self._app_backend.supports_tracing
+
+ @property
+ def supports_overriding_memory_pressure_notifications(self):
+ if not isinstance(self._app_backend, browser_backend.BrowserBackend):
+ return False
+ return self._app_backend.supports_overriding_memory_pressure_notifications
+
+
+ @property
+ def is_tracing_running(self):
+ if not self.supports_tracing:
+ return False
+ if not self._tracing_backend:
+ return False
+ return self._tracing_backend.is_tracing_running
+
+ @property
+ def support_startup_tracing(self):
+ # Startup tracing with --trace-config-file flag was not supported until
+ # Chromium branch number 2512 (see crrev.com/1309243004 and
+ # crrev.com/1353583002).
+ if not chrome_tracing_agent.ChromeTracingAgent.IsStartupTracingSupported(
+ self._app_backend.platform_backend):
+ return False
+ # TODO(zhenw): Remove this once stable Chrome and reference browser have
+ # passed 2512.
+ return self.GetChromeBranchNumber() >= 2512
+
+ @property
+ def support_modern_devtools_tracing_start_api(self):
+ # Modern DevTools Tracing.start API (via 'traceConfig' parameter) was not
+ # supported until Chromium branch number 2683 (see crrev.com/1808353002).
+ # TODO(petrcermak): Remove this once stable Chrome and reference browser
+ # have passed 2683.
+ return self.GetChromeBranchNumber() >= 2683
+
+ def IsAlive(self):
+ """Whether the DevTools server is available and connectable."""
+ return (self._devtools_http and
+ _IsDevToolsAgentAvailable(self._devtools_http))
+
+ def Close(self):
+ if self._tracing_backend:
+ self._tracing_backend.Close()
+ self._tracing_backend = None
+ if self._memory_backend:
+ self._memory_backend.Close()
+ self._memory_backend = None
+
+ if self._devtools_context_map_backend:
+ self._devtools_context_map_backend.Clear()
+
+ # Close the browser inspector socket last (in case the backend needs to
+ # interact with it before closing).
+ if self._browser_inspector_websocket:
+ self._browser_inspector_websocket.Disconnect()
+ self._browser_inspector_websocket = None
+
+ assert self._devtools_http
+ self._devtools_http.Disconnect()
+ self._devtools_http = None
+
+
+ @decorators.Cache
+ def GetChromeBranchNumber(self):
+ # Detect version information.
+ resp = self._devtools_http.RequestJson('version')
+ if 'Protocol-Version' in resp:
+ if 'Browser' in resp:
+ branch_number_match = re.search(r'Chrome/\d+\.\d+\.(\d+)\.\d+',
+ resp['Browser'])
+ else:
+ branch_number_match = re.search(
+ r'Chrome/\d+\.\d+\.(\d+)\.\d+ (Mobile )?Safari',
+ resp['User-Agent'])
+
+ if branch_number_match:
+ branch_number = int(branch_number_match.group(1))
+ if branch_number:
+ return branch_number
+
+ # Branch number can't be determined, so fail any branch number checks.
+ return 0
+
+ def _ListInspectableContexts(self):
+ return self._devtools_http.RequestJson('')
+
+ def RequestNewTab(self, timeout):
+ """Creates a new tab.
+
+ Returns:
+ A JSON string as returned by DevTools. Example:
+ {
+ "description": "",
+ "devtoolsFrontendUrl":
+ "/devtools/inspector.html?ws=host:port/devtools/page/id-string",
+ "id": "id-string",
+ "title": "Page Title",
+ "type": "page",
+ "url": "url",
+ "webSocketDebuggerUrl": "ws://host:port/devtools/page/id-string"
+ }
+
+ Raises:
+ devtools_http.DevToolsClientConnectionError
+ """
+ return self._devtools_http.Request('new', timeout=timeout)
+
+ def CloseTab(self, tab_id, timeout):
+ """Closes the tab with the given id.
+
+ Raises:
+ devtools_http.DevToolsClientConnectionError
+ TabNotFoundError
+ """
+ try:
+ return self._devtools_http.Request('close/%s' % tab_id,
+ timeout=timeout)
+ except devtools_http.DevToolsClientUrlError:
+ error = TabNotFoundError(
+ 'Unable to close tab, tab id not found: %s' % tab_id)
+ raise error, None, sys.exc_info()[2]
+
+ def ActivateTab(self, tab_id, timeout):
+ """Activates the tab with the given id.
+
+ Raises:
+ devtools_http.DevToolsClientConnectionError
+ TabNotFoundError
+ """
+ try:
+ return self._devtools_http.Request('activate/%s' % tab_id,
+ timeout=timeout)
+ except devtools_http.DevToolsClientUrlError:
+ error = TabNotFoundError(
+ 'Unable to activate tab, tab id not found: %s' % tab_id)
+ raise error, None, sys.exc_info()[2]
+
+ def GetUrl(self, tab_id):
+ """Returns the URL of the tab with |tab_id|, as reported by devtools.
+
+ Raises:
+ devtools_http.DevToolsClientConnectionError
+ """
+ for c in self._ListInspectableContexts():
+ if c['id'] == tab_id:
+ return c['url']
+ return None
+
+ def IsInspectable(self, tab_id):
+ """Whether the tab with |tab_id| is inspectable, as reported by devtools.
+
+ Raises:
+ devtools_http.DevToolsClientConnectionError
+ """
+ contexts = self._ListInspectableContexts()
+ return tab_id in [c['id'] for c in contexts]
+
+ def GetUpdatedInspectableContexts(self):
+ """Returns an updated instance of _DevToolsContextMapBackend."""
+ contexts = self._ListInspectableContexts()
+ self._devtools_context_map_backend._Update(contexts)
+ return self._devtools_context_map_backend
+
+ def _CreateTracingBackendIfNeeded(self, is_tracing_running=False):
+ assert self.supports_tracing
+ if not self._tracing_backend:
+ self._CreateAndConnectBrowserInspectorWebsocketIfNeeded()
+ self._tracing_backend = tracing_backend.TracingBackend(
+ self._browser_inspector_websocket, is_tracing_running,
+ self.support_modern_devtools_tracing_start_api)
+
+ def _CreateMemoryBackendIfNeeded(self):
+ assert self.supports_overriding_memory_pressure_notifications
+ if not self._memory_backend:
+ self._CreateAndConnectBrowserInspectorWebsocketIfNeeded()
+ self._memory_backend = memory_backend.MemoryBackend(
+ self._browser_inspector_websocket)
+
+ def _CreateAndConnectBrowserInspectorWebsocketIfNeeded(self):
+ if not self._browser_inspector_websocket:
+ self._browser_inspector_websocket = (
+ inspector_websocket.InspectorWebsocket())
+ self._browser_inspector_websocket.Connect(
+ BROWSER_INSPECTOR_WEBSOCKET_URL % self._devtools_port, timeout=10)
+
+ def IsChromeTracingSupported(self):
+ if not self.supports_tracing:
+ return False
+ self._CreateTracingBackendIfNeeded()
+ return self._tracing_backend.IsTracingSupported()
+
+ def StartChromeTracing(self, trace_config, timeout=10):
+ """
+ Args:
+ trace_config: An tracing_config.TracingConfig instance.
+ """
+ assert trace_config and trace_config.enable_chrome_trace
+ self._CreateTracingBackendIfNeeded()
+ return self._tracing_backend.StartTracing(
+ trace_config.chrome_trace_config, timeout)
+
+ def RecordChromeClockSyncMarker(self, sync_id):
+ assert self.is_tracing_running, 'Tracing must be running to clock sync.'
+ self._tracing_backend.RecordClockSyncMarker(sync_id)
+
+ def StopChromeTracing(self):
+ assert self.is_tracing_running
+ self._tab_ids = []
+ try:
+ context_map = self.GetUpdatedInspectableContexts()
+ for context in context_map.contexts:
+ if context['type'] not in ['iframe', 'page', 'webview']:
+ continue
+ context_id = context['id']
+ backend = context_map.GetInspectorBackend(context_id)
+ # TODO(catapult:#3028): Fix interpolation of JavaScript values.
+ backend.EvaluateJavaScript(
+ "console.time('" + backend.id + "');" +
+ "console.timeEnd('" + backend.id + "');" +
+ "console.time.toString().indexOf('[native code]') != -1;")
+ self._tab_ids.append(backend.id)
+ finally:
+ self._tracing_backend.StopTracing()
+
+ def CollectChromeTracingData(self, trace_data_builder, timeout=30):
+ try:
+ trace_data_builder.AddTraceFor(
+ trace_data_module.TAB_ID_PART, self._tab_ids[:])
+ self._tab_ids = None
+ finally:
+ self._tracing_backend.CollectTraceData(trace_data_builder, timeout)
+
+ def DumpMemory(self, timeout=30):
+ """Dumps memory.
+
+ Returns:
+ GUID of the generated dump if successful, None otherwise.
+
+ Raises:
+ TracingTimeoutException: If more than |timeout| seconds has passed
+ since the last time any data is received.
+ TracingUnrecoverableException: If there is a websocket error.
+ TracingUnexpectedResponseException: If the response contains an error
+ or does not contain the expected result.
+ """
+ self._CreateTracingBackendIfNeeded()
+ return self._tracing_backend.DumpMemory(timeout)
+
+ def SetMemoryPressureNotificationsSuppressed(self, suppressed, timeout=30):
+ """Enable/disable suppressing memory pressure notifications.
+
+ Args:
+ suppressed: If true, memory pressure notifications will be suppressed.
+ timeout: The timeout in seconds.
+
+ Raises:
+ MemoryTimeoutException: If more than |timeout| seconds has passed
+ since the last time any data is received.
+ MemoryUnrecoverableException: If there is a websocket error.
+ MemoryUnexpectedResponseException: If the response contains an error
+ or does not contain the expected result.
+ """
+ self._CreateMemoryBackendIfNeeded()
+ return self._memory_backend.SetMemoryPressureNotificationsSuppressed(
+ suppressed, timeout)
+
+ def SimulateMemoryPressureNotification(self, pressure_level, timeout=30):
+ """Simulate a memory pressure notification.
+
+ Args:
+ pressure level: The memory pressure level of the notification ('moderate'
+ or 'critical').
+ timeout: The timeout in seconds.
+
+ Raises:
+ MemoryTimeoutException: If more than |timeout| seconds has passed
+ since the last time any data is received.
+ MemoryUnrecoverableException: If there is a websocket error.
+ MemoryUnexpectedResponseException: If the response contains an error
+ or does not contain the expected result.
+ """
+ self._CreateMemoryBackendIfNeeded()
+ return self._memory_backend.SimulateMemoryPressureNotification(
+ pressure_level, timeout)
+
+
+class _DevToolsContextMapBackend(object):
+ def __init__(self, app_backend, devtools_client):
+ self._app_backend = app_backend
+ self._devtools_client = devtools_client
+ self._contexts = None
+ self._inspector_backends_dict = {}
+
+ @property
+ def contexts(self):
+ """The most up to date contexts data.
+
+ Returned in the order returned by devtools agent."""
+ return self._contexts
+
+ def GetContextInfo(self, context_id):
+ for context in self._contexts:
+ if context['id'] == context_id:
+ return context
+ raise KeyError('Cannot find a context with id=%s' % context_id)
+
+ def GetInspectorBackend(self, context_id):
+ """Gets an InspectorBackend instance for the given context_id.
+
+ This lazily creates InspectorBackend for the context_id if it does
+ not exist yet. Otherwise, it will return the cached instance."""
+ if context_id in self._inspector_backends_dict:
+ return self._inspector_backends_dict[context_id]
+
+ for context in self._contexts:
+ if context['id'] == context_id:
+ new_backend = inspector_backend.InspectorBackend(
+ self._app_backend.app, self._devtools_client, context)
+ self._inspector_backends_dict[context_id] = new_backend
+ return new_backend
+
+ raise KeyError('Cannot find a context with id=%s' % context_id)
+
+ def _Update(self, contexts):
+ # Remove InspectorBackend that is not in the current inspectable
+ # contexts list.
+ context_ids = [context['id'] for context in contexts]
+ for context_id in self._inspector_backends_dict.keys():
+ if context_id not in context_ids:
+ backend = self._inspector_backends_dict[context_id]
+ backend.Disconnect()
+ del self._inspector_backends_dict[context_id]
+
+ valid_contexts = []
+ for context in contexts:
+ # If the context does not have webSocketDebuggerUrl, skip it.
+ # If an InspectorBackend is already created for the tab,
+ # webSocketDebuggerUrl will be missing, and this is expected.
+ context_id = context['id']
+ if context_id not in self._inspector_backends_dict:
+ if 'webSocketDebuggerUrl' not in context:
+ logging.debug('webSocketDebuggerUrl missing, removing %s'
+ % context_id)
+ continue
+ valid_contexts.append(context)
+ self._contexts = valid_contexts
+
+ def Clear(self):
+ for backend in self._inspector_backends_dict.values():
+ backend.Disconnect()
+ self._inspector_backends_dict = {}
+ self._contexts = None