| # 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) |
| 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) |
| |
| 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=trace_config, |
| custom_categories=trace_config.tracing_category_filter.filter_string) |
| |
| @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 |
| |
| 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) |
| |
| 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) |
| |
| def IsChromeTracingSupported(self): |
| if not self.supports_tracing: |
| return False |
| self._CreateTracingBackendIfNeeded() |
| return self._tracing_backend.IsTracingSupported() |
| |
| def StartChromeTracing( |
| self, trace_config, custom_categories=None, timeout=10): |
| """ |
| Args: |
| trace_config: An tracing_config.TracingConfig instance. |
| custom_categories: An optional string containing a list of |
| comma separated categories that will be traced |
| instead of the default category set. Example: use |
| "webkit,cc,disabled-by-default-cc.debug" to trace only |
| those three event categories. |
| """ |
| assert trace_config and trace_config.enable_chrome_trace |
| self._CreateTracingBackendIfNeeded() |
| return self._tracing_backend.StartTracing( |
| trace_config, custom_categories, timeout) |
| |
| def StopChromeTracing(self, trace_data_builder, timeout=30): |
| assert self.is_tracing_running |
| 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) |
| success = backend.EvaluateJavaScript( |
| "console.time('" + backend.id + "');" + |
| "console.timeEnd('" + backend.id + "');" + |
| "console.time.toString().indexOf('[native code]') != -1;") |
| if not success: |
| raise Exception('Page stomped on console.time') |
| trace_data_builder.AddEventsTo( |
| trace_data_module.TAB_ID_PART, [backend.id]) |
| finally: |
| self._tracing_backend.StopTracing(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 |