diff options
Diffstat (limited to 'chrome/test/pyautolib/remote_inspector_client.py')
-rwxr-xr-x | chrome/test/pyautolib/remote_inspector_client.py | 1211 |
1 files changed, 0 insertions, 1211 deletions
diff --git a/chrome/test/pyautolib/remote_inspector_client.py b/chrome/test/pyautolib/remote_inspector_client.py deleted file mode 100755 index 95b9cf6..0000000 --- a/chrome/test/pyautolib/remote_inspector_client.py +++ /dev/null @@ -1,1211 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2012 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. - -"""Chrome remote inspector utility for pyauto tests. - -This script provides a python interface that acts as a front-end for Chrome's -remote inspector module, communicating via sockets to interact with Chrome in -the same way that the Developer Tools does. This -- in theory -- should allow -a pyauto test to do anything that Chrome's Developer Tools does, as long as the -appropriate communication with the remote inspector is implemented in this -script. - -This script assumes that Chrome is already running on the local machine with -flag '--remote-debugging-port=9222' to enable remote debugging on port 9222. - -To use this module, first create an instance of class RemoteInspectorClient; -doing this sets up a connection to Chrome's remote inspector. Then call the -appropriate functions on that object to perform the desired actions with the -remote inspector. When done, call Stop() on the RemoteInspectorClient object -to stop communication with the remote inspector. - -For example, to take v8 heap snapshots from a pyauto test: - -import remote_inspector_client -my_client = remote_inspector_client.RemoteInspectorClient() -snapshot_info = my_client.HeapSnapshot(include_summary=True) -// Do some stuff... -new_snapshot_info = my_client.HeapSnapshot(include_summary=True) -my_client.Stop() - -It is expected that a test will only use one instance of RemoteInspectorClient -at a time. If a second instance is instantiated, a RuntimeError will be raised. -RemoteInspectorClient could be made into a singleton in the future if the need -for it arises. -""" - -import asyncore -import datetime -import logging -import optparse -import pprint -import re -import simplejson -import socket -import sys -import threading -import time -import urllib2 -import urlparse - - -class _DevToolsSocketRequest(object): - """A representation of a single DevToolsSocket request. - - A DevToolsSocket request is used for communication with a remote Chrome - instance when interacting with the renderer process of a given webpage. - Requests and results are passed as specially-formatted JSON messages, - according to a communication protocol defined in WebKit. The string - representation of this request will be a JSON message that is properly - formatted according to the communication protocol. - - Public Attributes: - method: The string method name associated with this request. - id: A unique integer id associated with this request. - params: A dictionary of input parameters associated with this request. - results: A dictionary of relevant results obtained from the remote Chrome - instance that are associated with this request. - is_fulfilled: A boolean indicating whether or not this request has been sent - and all relevant results for it have been obtained (i.e., this value is - True only if all results for this request are known). - is_fulfilled_condition: A threading.Condition for waiting for the request to - be fulfilled. - """ - - def __init__(self, method, params, message_id): - """Initialize. - - Args: - method: The string method name for this request. - message_id: An integer id for this request, which is assumed to be unique - from among all requests. - """ - self.method = method - self.id = message_id - self.params = params - self.results = {} - self.is_fulfilled = False - self.is_fulfilled_condition = threading.Condition() - - def __repr__(self): - json_dict = {} - json_dict['method'] = self.method - json_dict['id'] = self.id - if self.params: - json_dict['params'] = self.params - return simplejson.dumps(json_dict, separators=(',', ':')) - - -class _DevToolsSocketClient(asyncore.dispatcher): - """Client that communicates with a remote Chrome instance via sockets. - - This class works in conjunction with the _RemoteInspectorThread class to - communicate with a remote Chrome instance following the remote debugging - communication protocol in WebKit. This class performs the lower-level work - of socket communication. - - Public Attributes: - handshake_done: A boolean indicating whether or not the client has completed - the required protocol handshake with the remote Chrome instance. - inspector_thread: An instance of the _RemoteInspectorThread class that is - working together with this class to communicate with a remote Chrome - instance. - """ - - def __init__(self, verbose, show_socket_messages, hostname, port, path): - """Initialize. - - Args: - verbose: A boolean indicating whether or not to use verbose logging. - show_socket_messages: A boolean indicating whether or not to show the - socket messages sent/received when communicating with the remote - Chrome instance. - hostname: The string hostname of the DevToolsSocket to which to connect. - port: The integer port number of the DevToolsSocket to which to connect. - path: The string path of the DevToolsSocket to which to connect. - """ - asyncore.dispatcher.__init__(self) - - self._logger = logging.getLogger('_DevToolsSocketClient') - self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) - - self._show_socket_messages = show_socket_messages - - self._read_buffer = '' - self._write_buffer = '' - - self._socket_buffer_lock = threading.Lock() - - self.handshake_done = False - self.inspector_thread = None - - # Connect to the remote Chrome instance and initiate the protocol handshake. - self.create_socket(socket.AF_INET, socket.SOCK_STREAM) - self.connect((hostname, port)) - - fields = [ - 'Upgrade: WebSocket', - 'Connection: Upgrade', - 'Host: %s:%d' % (hostname, port), - 'Origin: http://%s:%d' % (hostname, port), - 'Sec-WebSocket-Key1: 4k0L66E ZU 8 5 <18 <TK 7 7', - 'Sec-WebSocket-Key2: s2 20 `# 4| 3 9 U_ 1299', - ] - handshake_msg = ('GET %s HTTP/1.1\r\n%s\r\n\r\n\x47\x30\x22\x2D\x5A\x3F' - '\x47\x58' % (path, '\r\n'.join(fields))) - self._Write(handshake_msg.encode('utf-8')) - - def SendMessage(self, msg): - """Causes a request message to be sent to the remote Chrome instance. - - Args: - msg: A string message to be sent; assumed to be a JSON message in proper - format according to the remote debugging protocol in WebKit. - """ - # According to the communication protocol, each request message sent over - # the wire must begin with '\x00' and end with '\xff'. - self._Write('\x00' + msg.encode('utf-8') + '\xff') - - def _Write(self, msg): - """Causes a raw message to be sent to the remote Chrome instance. - - Args: - msg: A raw string message to be sent. - """ - self._write_buffer += msg - self.handle_write() - - def handle_write(self): - """Called if a writable socket can be written; overridden from asyncore.""" - self._socket_buffer_lock.acquire() - if self._write_buffer: - sent = self.send(self._write_buffer) - if self._show_socket_messages: - msg_type = ['Handshake', 'Message'][self._write_buffer[0] == '\x00' and - self._write_buffer[-1] == '\xff'] - msg = ('========================\n' - 'Sent %s:\n' - '========================\n' - '%s\n' - '========================') % (msg_type, - self._write_buffer[:sent-1]) - print msg - self._write_buffer = self._write_buffer[sent:] - self._socket_buffer_lock.release() - - def handle_read(self): - """Called when a socket can be read; overridden from asyncore.""" - self._socket_buffer_lock.acquire() - if self.handshake_done: - # Process a message reply from the remote Chrome instance. - self._read_buffer += self.recv(4096) - pos = self._read_buffer.find('\xff') - while pos >= 0: - pos += len('\xff') - data = self._read_buffer[:pos-len('\xff')] - pos2 = data.find('\x00') - if pos2 >= 0: - data = data[pos2 + 1:] - self._read_buffer = self._read_buffer[pos:] - if self._show_socket_messages: - msg = ('========================\n' - 'Received Message:\n' - '========================\n' - '%s\n' - '========================') % data - print msg - if self.inspector_thread: - self.inspector_thread.NotifyReply(data) - pos = self._read_buffer.find('\xff') - else: - # Process a handshake reply from the remote Chrome instance. - self._read_buffer += self.recv(4096) - pos = self._read_buffer.find('\r\n\r\n') - if pos >= 0: - pos += len('\r\n\r\n') - data = self._read_buffer[:pos] - self._read_buffer = self._read_buffer[pos:] - self.handshake_done = True - if self._show_socket_messages: - msg = ('=========================\n' - 'Received Handshake Reply:\n' - '=========================\n' - '%s\n' - '=========================') % data - print msg - self._socket_buffer_lock.release() - - def handle_close(self): - """Called when the socket is closed; overridden from asyncore.""" - if self._show_socket_messages: - msg = ('=========================\n' - 'Socket closed.\n' - '=========================') - print msg - self.close() - - def writable(self): - """Determines if writes can occur for this socket; overridden from asyncore. - - Returns: - True, if there is something to write to the socket, or - False, otherwise. - """ - return len(self._write_buffer) > 0 - - def handle_expt(self): - """Called when out-of-band data exists; overridden from asyncore.""" - self.handle_error() - - def handle_error(self): - """Called when an exception is raised; overridden from asyncore.""" - if self._show_socket_messages: - msg = ('=========================\n' - 'Socket error.\n' - '=========================') - print msg - self.close() - self.inspector_thread.ClientSocketExceptionOccurred() - asyncore.dispatcher.handle_error(self) - - -class _RemoteInspectorThread(threading.Thread): - """Manages communication using Chrome's remote inspector protocol. - - This class works in conjunction with the _DevToolsSocketClient class to - communicate with a remote Chrome instance following the remote inspector - communication protocol in WebKit. This class performs the higher-level work - of managing request and reply messages, whereas _DevToolsSocketClient handles - the lower-level work of socket communication. - """ - - def __init__(self, url, tab_index, tab_filter, verbose, show_socket_messages, - agent_name): - """Initialize. - - Args: - url: The base URL to connent to. - tab_index: The integer index of the tab in the remote Chrome instance to - use for snapshotting. - tab_filter: When specified, is run over tabs of the remote Chrome - instances to choose which one to connect to. - verbose: A boolean indicating whether or not to use verbose logging. - show_socket_messages: A boolean indicating whether or not to show the - socket messages sent/received when communicating with the remote - Chrome instance. - """ - threading.Thread.__init__(self) - self._logger = logging.getLogger('_RemoteInspectorThread') - self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) - - self._killed = False - self._requests = [] - self._action_queue = [] - self._action_queue_condition = threading.Condition() - self._action_specific_callback = None # Callback only for current action. - self._action_specific_callback_lock = threading.Lock() - self._general_callbacks = [] # General callbacks that can be long-lived. - self._general_callbacks_lock = threading.Lock() - self._condition_to_wait = None - self._agent_name = agent_name - - # Create a DevToolsSocket client and wait for it to complete the remote - # debugging protocol handshake with the remote Chrome instance. - result = self._IdentifyDevToolsSocketConnectionInfo( - url, tab_index, tab_filter) - self._client = _DevToolsSocketClient( - verbose, show_socket_messages, result['host'], result['port'], - result['path']) - self._client.inspector_thread = self - while asyncore.socket_map: - if self._client.handshake_done or self._killed: - break - asyncore.loop(timeout=1, count=1, use_poll=True) - - def ClientSocketExceptionOccurred(self): - """Notifies that the _DevToolsSocketClient encountered an exception.""" - self.Kill() - - def NotifyReply(self, msg): - """Notifies of a reply message received from the remote Chrome instance. - - Args: - msg: A string reply message received from the remote Chrome instance; - assumed to be a JSON message formatted according to the remote - debugging communication protocol in WebKit. - """ - reply_dict = simplejson.loads(msg) - - # Notify callbacks of this message received from the remote inspector. - self._action_specific_callback_lock.acquire() - if self._action_specific_callback: - self._action_specific_callback(reply_dict) - self._action_specific_callback_lock.release() - - self._general_callbacks_lock.acquire() - if self._general_callbacks: - for callback in self._general_callbacks: - callback(reply_dict) - self._general_callbacks_lock.release() - - if 'result' in reply_dict: - # This is the result message associated with a previously-sent request. - request = self.GetRequestWithId(reply_dict['id']) - if request: - request.is_fulfilled_condition.acquire() - request.is_fulfilled_condition.notify() - request.is_fulfilled_condition.release() - - def run(self): - """Start this thread; overridden from threading.Thread.""" - while not self._killed: - self._action_queue_condition.acquire() - if self._action_queue: - # There's a request to the remote inspector that needs to be processed. - messages, callback = self._action_queue.pop(0) - self._action_specific_callback_lock.acquire() - self._action_specific_callback = callback - self._action_specific_callback_lock.release() - - # Prepare the request list. - for message_id, message in enumerate(messages): - self._requests.append( - _DevToolsSocketRequest(message[0], message[1], message_id)) - - # Send out each request. Wait until each request is complete before - # sending the next request. - for request in self._requests: - self._FillInParams(request) - self._client.SendMessage(str(request)) - - request.is_fulfilled_condition.acquire() - self._condition_to_wait = request.is_fulfilled_condition - request.is_fulfilled_condition.wait() - request.is_fulfilled_condition.release() - - if self._killed: - self._client.close() - return - - # Clean up so things are ready for the next request. - self._requests = [] - - self._action_specific_callback_lock.acquire() - self._action_specific_callback = None - self._action_specific_callback_lock.release() - - # Wait until there is something to process. - self._condition_to_wait = self._action_queue_condition - self._action_queue_condition.wait() - self._action_queue_condition.release() - self._client.close() - - def Kill(self): - """Notify this thread that it should stop executing.""" - self._killed = True - # The thread might be waiting on a condition. - if self._condition_to_wait: - self._condition_to_wait.acquire() - self._condition_to_wait.notify() - self._condition_to_wait.release() - - def PerformAction(self, request_messages, reply_message_callback): - """Notify this thread of an action to perform using the remote inspector. - - Args: - request_messages: A list of strings representing the requests to make - using the remote inspector. - reply_message_callback: A callable to be invoked any time a message is - received from the remote inspector while the current action is - being performed. The callable should accept a single argument, - which is a dictionary representing a message received. - """ - self._action_queue_condition.acquire() - self._action_queue.append((request_messages, reply_message_callback)) - self._action_queue_condition.notify() - self._action_queue_condition.release() - - def AddMessageCallback(self, callback): - """Add a callback to invoke for messages received from the remote inspector. - - Args: - callback: A callable to be invoked any time a message is received from the - remote inspector. The callable should accept a single argument, which - is a dictionary representing a message received. - """ - self._general_callbacks_lock.acquire() - self._general_callbacks.append(callback) - self._general_callbacks_lock.release() - - def RemoveMessageCallback(self, callback): - """Remove a callback from the set of those to invoke for messages received. - - Args: - callback: A callable to remove from consideration. - """ - self._general_callbacks_lock.acquire() - self._general_callbacks.remove(callback) - self._general_callbacks_lock.release() - - def GetRequestWithId(self, request_id): - """Identifies the request with the specified id. - - Args: - request_id: An integer request id; should be unique for each request. - - Returns: - A request object associated with the given id if found, or - None otherwise. - """ - found_request = [x for x in self._requests if x.id == request_id] - if found_request: - return found_request[0] - return None - - def GetFirstUnfulfilledRequest(self, method): - """Identifies the first unfulfilled request with the given method name. - - An unfulfilled request is one for which all relevant reply messages have - not yet been received from the remote inspector. - - Args: - method: The string method name of the request for which to search. - - Returns: - The first request object in the request list that is not yet fulfilled - and is also associated with the given method name, or - None if no such request object can be found. - """ - for request in self._requests: - if not request.is_fulfilled and request.method == method: - return request - return None - - def _GetLatestRequestOfType(self, ref_req, method): - """Identifies the latest specified request before a reference request. - - This function finds the latest request with the specified method that - occurs before the given reference request. - - Args: - ref_req: A reference request from which to start looking. - method: The string method name of the request for which to search. - - Returns: - The latest _DevToolsSocketRequest object with the specified method, - if found, or None otherwise. - """ - start_looking = False - for request in self._requests[::-1]: - if request.id == ref_req.id: - start_looking = True - elif start_looking: - if request.method == method: - return request - return None - - def _FillInParams(self, request): - """Fills in parameters for requests as necessary before the request is sent. - - Args: - request: The _DevToolsSocketRequest object associated with a request - message that is about to be sent. - """ - if request.method == self._agent_name +'.takeHeapSnapshot': - # We always want detailed v8 heap snapshot information. - request.params = {'detailed': True} - elif request.method == self._agent_name + '.getHeapSnapshot': - # To actually request the snapshot data from a previously-taken snapshot, - # we need to specify the unique uid of the snapshot we want. - # The relevant uid should be contained in the last - # 'Profiler.takeHeapSnapshot' request object. - last_req = self._GetLatestRequestOfType(request, - self._agent_name + '.takeHeapSnapshot') - if last_req and 'uid' in last_req.results: - request.params = {'uid': last_req.results['uid']} - elif request.method == self._agent_name + '.getProfile': - # TODO(eustas): Remove this case after M27 is released. - last_req = self._GetLatestRequestOfType(request, - self._agent_name + '.takeHeapSnapshot') - if last_req and 'uid' in last_req.results: - request.params = {'type': 'HEAP', 'uid': last_req.results['uid']} - - @staticmethod - def _IdentifyDevToolsSocketConnectionInfo(url, tab_index, tab_filter): - """Identifies DevToolsSocket connection info from a remote Chrome instance. - - Args: - url: The base URL to connent to. - tab_index: The integer index of the tab in the remote Chrome instance to - which to connect. - tab_filter: When specified, is run over tabs of the remote Chrome instance - to choose which one to connect to. - - Returns: - A dictionary containing the DevToolsSocket connection info: - { - 'host': string, - 'port': integer, - 'path': string, - } - - Raises: - RuntimeError: When DevToolsSocket connection info cannot be identified. - """ - try: - f = urllib2.urlopen(url + '/json') - result = f.read() - logging.debug(result) - result = simplejson.loads(result) - except urllib2.URLError, e: - raise RuntimeError( - 'Error accessing Chrome instance debugging port: ' + str(e)) - - if tab_filter: - connect_to = filter(tab_filter, result)[0] - else: - if tab_index >= len(result): - raise RuntimeError( - 'Specified tab index %d doesn\'t exist (%d tabs found)' % - (tab_index, len(result))) - connect_to = result[tab_index] - - logging.debug(simplejson.dumps(connect_to)) - - if 'webSocketDebuggerUrl' not in connect_to: - raise RuntimeError('No socket URL exists for the specified tab.') - - socket_url = connect_to['webSocketDebuggerUrl'] - parsed = urlparse.urlparse(socket_url) - # On ChromeOS, the "ws://" scheme may not be recognized, leading to an - # incorrect netloc (and empty hostname and port attributes) in |parsed|. - # Change the scheme to "http://" to fix this. - if not parsed.hostname or not parsed.port: - socket_url = 'http' + socket_url[socket_url.find(':'):] - parsed = urlparse.urlparse(socket_url) - # Warning: |parsed.scheme| is incorrect after this point. - return ({'host': parsed.hostname, - 'port': parsed.port, - 'path': parsed.path}) - - -class _RemoteInspectorDriverThread(threading.Thread): - """Drives the communication service with the remote inspector.""" - - def __init__(self): - """Initialize.""" - threading.Thread.__init__(self) - - def run(self): - """Drives the communication service with the remote inspector.""" - try: - while asyncore.socket_map: - asyncore.loop(timeout=1, count=1, use_poll=True) - except KeyboardInterrupt: - pass - - -class _V8HeapSnapshotParser(object): - """Parses v8 heap snapshot data.""" - _CHILD_TYPES = ['context', 'element', 'property', 'internal', 'hidden', - 'shortcut', 'weak'] - _NODE_TYPES = ['hidden', 'array', 'string', 'object', 'code', 'closure', - 'regexp', 'number', 'native', 'synthetic'] - - @staticmethod - def ParseSnapshotData(raw_data): - """Parses raw v8 heap snapshot data and returns the summarized results. - - The raw heap snapshot data is represented as a JSON object with the - following keys: 'snapshot', 'nodes', and 'strings'. - - The 'snapshot' value provides the 'title' and 'uid' attributes for the - snapshot. For example: - { u'title': u'org.webkit.profiles.user-initiated.1', u'uid': 1} - - The 'nodes' value is a list of node information from the v8 heap, with a - special first element that describes the node serialization layout (see - HeapSnapshotJSONSerializer::SerializeNodes). All other list elements - contain information about nodes in the v8 heap, according to the - serialization layout. - - The 'strings' value is a list of strings, indexed by values in the 'nodes' - list to associate nodes with strings. - - Args: - raw_data: A string representing the raw v8 heap snapshot data. - - Returns: - A dictionary containing the summarized v8 heap snapshot data: - { - 'total_v8_node_count': integer, # Total number of nodes in the v8 heap. - 'total_shallow_size': integer, # Total heap size, in bytes. - } - """ - total_node_count = 0 - total_shallow_size = 0 - constructors = {} - - # TODO(dennisjeffrey): The following line might be slow, especially on - # ChromeOS. Investigate faster alternatives. - heap = simplejson.loads(raw_data) - - index = 1 # Bypass the special first node list item. - node_list = heap['nodes'] - while index < len(node_list): - node_type = node_list[index] - node_name = node_list[index + 1] - node_id = node_list[index + 2] - node_self_size = node_list[index + 3] - node_retained_size = node_list[index + 4] - node_dominator = node_list[index + 5] - node_children_count = node_list[index + 6] - index += 7 - - node_children = [] - for i in xrange(node_children_count): - child_type = node_list[index] - child_type_string = _V8HeapSnapshotParser._CHILD_TYPES[int(child_type)] - child_name_index = node_list[index + 1] - child_to_node = node_list[index + 2] - index += 3 - - child_info = { - 'type': child_type_string, - 'name_or_index': child_name_index, - 'to_node': child_to_node, - } - node_children.append(child_info) - - # Get the constructor string for this node so nodes can be grouped by - # constructor. - # See HeapSnapshot.js: WebInspector.HeapSnapshotNode.prototype. - type_string = _V8HeapSnapshotParser._NODE_TYPES[int(node_type)] - constructor_name = None - if type_string == 'hidden': - constructor_name = '(system)' - elif type_string == 'object': - constructor_name = heap['strings'][int(node_name)] - elif type_string == 'native': - pos = heap['strings'][int(node_name)].find('/') - if pos >= 0: - constructor_name = heap['strings'][int(node_name)][:pos].rstrip() - else: - constructor_name = heap['strings'][int(node_name)] - elif type_string == 'code': - constructor_name = '(compiled code)' - else: - constructor_name = '(' + type_string + ')' - - node_obj = { - 'type': type_string, - 'name': heap['strings'][int(node_name)], - 'id': node_id, - 'self_size': node_self_size, - 'retained_size': node_retained_size, - 'dominator': node_dominator, - 'children_count': node_children_count, - 'children': node_children, - } - - if constructor_name not in constructors: - constructors[constructor_name] = [] - constructors[constructor_name].append(node_obj) - - total_node_count += 1 - total_shallow_size += node_self_size - - # TODO(dennisjeffrey): Have this function also return more detailed v8 - # heap snapshot data when a need for it arises (e.g., using |constructors|). - result = {} - result['total_v8_node_count'] = total_node_count - result['total_shallow_size'] = total_shallow_size - return result - - -# TODO(dennisjeffrey): The "verbose" option used in this file should re-use -# pyauto's verbose flag. -class RemoteInspectorClient(object): - """Main class for interacting with Chrome's remote inspector. - - Upon initialization, a socket connection to Chrome's remote inspector will - be established. Users of this class should call Stop() to close the - connection when it's no longer needed. - - Public Methods: - Stop: Close the connection to the remote inspector. Should be called when - a user is done using this module. - HeapSnapshot: Takes a v8 heap snapshot and returns the summarized data. - GetMemoryObjectCounts: Retrieves memory object count information. - CollectGarbage: Forces a garbage collection. - StartTimelineEventMonitoring: Starts monitoring for timeline events. - StopTimelineEventMonitoring: Stops monitoring for timeline events. - """ - - # TODO(dennisjeffrey): Allow a user to specify a window index too (not just a - # tab index), when running through PyAuto. - def __init__(self, tab_index=0, tab_filter=None, - verbose=False, show_socket_messages=False, - url='http://localhost:9222'): - """Initialize. - - Args: - tab_index: The integer index of the tab in the remote Chrome instance to - which to connect. Defaults to 0 (the first tab). - tab_filter: When specified, is run over tabs of the remote Chrome - instance to choose which one to connect to. - verbose: A boolean indicating whether or not to use verbose logging. - show_socket_messages: A boolean indicating whether or not to show the - socket messages sent/received when communicating with the remote - Chrome instance. - """ - self._tab_index = tab_index - self._tab_filter = tab_filter - self._verbose = verbose - self._show_socket_messages = show_socket_messages - - self._timeline_started = False - - logging.basicConfig() - self._logger = logging.getLogger('RemoteInspectorClient') - self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) - - # Creating _RemoteInspectorThread might raise an exception. This prevents an - # AttributeError in the destructor. - self._remote_inspector_thread = None - self._remote_inspector_driver_thread = None - - self._version = self._GetVersion(url) - - # TODO(loislo): Remove this hack after M28 is released. - self._agent_name = 'Profiler' - if self._IsBrowserDayNumberGreaterThan(1470): - self._agent_name = 'HeapProfiler' - - # Start up a thread for long-term communication with the remote inspector. - self._remote_inspector_thread = _RemoteInspectorThread( - url, tab_index, tab_filter, verbose, show_socket_messages, - self._agent_name) - self._remote_inspector_thread.start() - # At this point, a connection has already been made to the remote inspector. - - # This thread calls asyncore.loop, which activates the channel service. - self._remote_inspector_driver_thread = _RemoteInspectorDriverThread() - self._remote_inspector_driver_thread.start() - - def __del__(self): - """Called on destruction of this object.""" - self.Stop() - - def Stop(self): - """Stop/close communication with the remote inspector.""" - if self._remote_inspector_thread: - self._remote_inspector_thread.Kill() - self._remote_inspector_thread.join() - self._remote_inspector_thread = None - if self._remote_inspector_driver_thread: - self._remote_inspector_driver_thread.join() - self._remote_inspector_driver_thread = None - - def HeapSnapshot(self, include_summary=False): - """Takes a v8 heap snapshot. - - Returns: - A dictionary containing information for a single v8 heap - snapshot that was taken. - { - 'url': string, # URL of the webpage that was snapshotted. - 'raw_data': string, # The raw data as JSON string. - 'total_v8_node_count': integer, # Total number of nodes in the v8 heap. - # Only if |include_summary| is True. - 'total_heap_size': integer, # Total v8 heap size (number of bytes). - # Only if |include_summary| is True. - } - """ - HEAP_SNAPSHOT_MESSAGES = [ - ('Page.getResourceTree', {}), - ('Debugger.enable', {}), - (self._agent_name + '.clearProfiles', {}), - (self._agent_name + '.takeHeapSnapshot', {}), - (self._agent_name + '.getHeapSnapshot', {}), - ] - - self._current_heap_snapshot = [] - self._url = '' - self._collected_heap_snapshot_data = {} - - done_condition = threading.Condition() - - def HandleReply(reply_dict): - """Processes a reply message received from the remote Chrome instance. - - Args: - reply_dict: A dictionary object representing the reply message received - from the remote inspector. - """ - if 'result' in reply_dict: - # This is the result message associated with a previously-sent request. - request = self._remote_inspector_thread.GetRequestWithId( - reply_dict['id']) - if 'frameTree' in reply_dict['result']: - self._url = reply_dict['result']['frameTree']['frame']['url'] - elif request.method == self._agent_name + '.getHeapSnapshot': - # A heap snapshot has been completed. Analyze and output the data. - self._logger.debug('Heap snapshot taken: %s', self._url) - # TODO(dennisjeffrey): Parse the heap snapshot on-the-fly as the data - # is coming in over the wire, so we can avoid storing the entire - # snapshot string in memory. - raw_snapshot_data = ''.join(self._current_heap_snapshot) - self._collected_heap_snapshot_data = { - 'url': self._url, - 'raw_data': raw_snapshot_data} - if include_summary: - self._logger.debug('Now analyzing heap snapshot...') - parser = _V8HeapSnapshotParser() - time_start = time.time() - self._logger.debug('Raw snapshot data size: %.2f MB', - len(raw_snapshot_data) / (1024.0 * 1024.0)) - result = parser.ParseSnapshotData(raw_snapshot_data) - self._logger.debug('Time to parse data: %.2f sec', - time.time() - time_start) - count = result['total_v8_node_count'] - self._collected_heap_snapshot_data['total_v8_node_count'] = count - total_size = result['total_shallow_size'] - self._collected_heap_snapshot_data['total_heap_size'] = total_size - - done_condition.acquire() - done_condition.notify() - done_condition.release() - elif 'method' in reply_dict: - # This is an auxiliary message sent from the remote Chrome instance. - if reply_dict['method'] == self._agent_name + '.addProfileHeader': - snapshot_req = ( - self._remote_inspector_thread.GetFirstUnfulfilledRequest( - self._agent_name + '.takeHeapSnapshot')) - if snapshot_req: - snapshot_req.results['uid'] = reply_dict['params']['header']['uid'] - elif reply_dict['method'] == self._agent_name + '.addHeapSnapshotChunk': - self._current_heap_snapshot.append(reply_dict['params']['chunk']) - - # Tell the remote inspector to take a v8 heap snapshot, then wait until - # the snapshot information is available to return. - self._remote_inspector_thread.PerformAction(HEAP_SNAPSHOT_MESSAGES, - HandleReply) - - done_condition.acquire() - done_condition.wait() - done_condition.release() - - return self._collected_heap_snapshot_data - - def EvaluateJavaScript(self, expression): - """Evaluates a JavaScript expression and returns the result. - - Sends a message containing the expression to the remote Chrome instance we - are connected to, and evaluates it in the context of the tab we are - connected to. Blocks until the result is available and returns it. - - Returns: - A dictionary representing the result. - """ - EVALUATE_MESSAGES = [ - ('Runtime.evaluate', { 'expression': expression, - 'objectGroup': 'group', - 'returnByValue': True }), - ('Runtime.releaseObjectGroup', { 'objectGroup': 'group' }) - ] - - self._result = None - done_condition = threading.Condition() - - def HandleReply(reply_dict): - """Processes a reply message received from the remote Chrome instance. - - Args: - reply_dict: A dictionary object representing the reply message received - from the remote Chrome instance. - """ - if 'result' in reply_dict and 'result' in reply_dict['result']: - self._result = reply_dict['result']['result']['value'] - - done_condition.acquire() - done_condition.notify() - done_condition.release() - - # Tell the remote inspector to evaluate the given expression, then wait - # until that information is available to return. - self._remote_inspector_thread.PerformAction(EVALUATE_MESSAGES, - HandleReply) - - done_condition.acquire() - done_condition.wait() - done_condition.release() - - return self._result - - def GetMemoryObjectCounts(self): - """Retrieves memory object count information. - - Returns: - A dictionary containing the memory object count information: - { - 'DOMNodeCount': integer, # Total number of DOM nodes. - 'EventListenerCount': integer, # Total number of event listeners. - } - """ - MEMORY_COUNT_MESSAGES = [ - ('Memory.getDOMCounters', {}) - ] - - self._event_listener_count = None - self._dom_node_count = None - - done_condition = threading.Condition() - def HandleReply(reply_dict): - """Processes a reply message received from the remote Chrome instance. - - Args: - reply_dict: A dictionary object representing the reply message received - from the remote Chrome instance. - """ - if 'result' in reply_dict: - self._event_listener_count = reply_dict['result']['jsEventListeners'] - self._dom_node_count = reply_dict['result']['nodes'] - - done_condition.acquire() - done_condition.notify() - done_condition.release() - - # Tell the remote inspector to collect memory count info, then wait until - # that information is available to return. - self._remote_inspector_thread.PerformAction(MEMORY_COUNT_MESSAGES, - HandleReply) - - done_condition.acquire() - done_condition.wait() - done_condition.release() - - return { - 'DOMNodeCount': self._dom_node_count, - 'EventListenerCount': self._event_listener_count, - } - - def CollectGarbage(self): - """Forces a garbage collection.""" - COLLECT_GARBAGE_MESSAGES = [ - ('Profiler.collectGarbage', {}) - ] - - # Tell the remote inspector to do a garbage collect. We can return - # immediately, since there is no result for which to wait. - self._remote_inspector_thread.PerformAction(COLLECT_GARBAGE_MESSAGES, None) - - def StartTimelineEventMonitoring(self, event_callback): - """Starts timeline event monitoring. - - Args: - event_callback: A callable to invoke whenever a timeline event is observed - from the remote inspector. The callable should take a single input, - which is a dictionary containing the detailed information of a - timeline event. - """ - if self._timeline_started: - self._logger.warning('Timeline monitoring already started.') - return - TIMELINE_MESSAGES = [ - ('Timeline.start', {}) - ] - - self._event_callback = event_callback - - done_condition = threading.Condition() - def HandleReply(reply_dict): - """Processes a reply message received from the remote Chrome instance. - - Args: - reply_dict: A dictionary object representing the reply message received - from the remote Chrome instance. - """ - if 'result' in reply_dict: - done_condition.acquire() - done_condition.notify() - done_condition.release() - if reply_dict.get('method') == 'Timeline.eventRecorded': - self._event_callback(reply_dict['params']['record']) - - # Tell the remote inspector to start the timeline. - self._timeline_callback = HandleReply - self._remote_inspector_thread.AddMessageCallback(self._timeline_callback) - self._remote_inspector_thread.PerformAction(TIMELINE_MESSAGES, None) - - done_condition.acquire() - done_condition.wait() - done_condition.release() - - self._timeline_started = True - - def StopTimelineEventMonitoring(self): - """Stops timeline event monitoring.""" - if not self._timeline_started: - self._logger.warning('Timeline monitoring already stopped.') - return - TIMELINE_MESSAGES = [ - ('Timeline.stop', {}) - ] - - done_condition = threading.Condition() - def HandleReply(reply_dict): - """Processes a reply message received from the remote Chrome instance. - - Args: - reply_dict: A dictionary object representing the reply message received - from the remote Chrome instance. - """ - if 'result' in reply_dict: - done_condition.acquire() - done_condition.notify() - done_condition.release() - - # Tell the remote inspector to stop the timeline. - self._remote_inspector_thread.RemoveMessageCallback(self._timeline_callback) - self._remote_inspector_thread.PerformAction(TIMELINE_MESSAGES, HandleReply) - - done_condition.acquire() - done_condition.wait() - done_condition.release() - - self._timeline_started = False - - def _ConvertByteCountToHumanReadableString(self, num_bytes): - """Converts an integer number of bytes into a human-readable string. - - Args: - num_bytes: An integer number of bytes. - - Returns: - A human-readable string representation of the given number of bytes. - """ - if num_bytes < 1024: - return '%d B' % num_bytes - elif num_bytes < 1048576: - return '%.2f KB' % (num_bytes / 1024.0) - else: - return '%.2f MB' % (num_bytes / 1048576.0) - - @staticmethod - def _GetVersion(endpoint): - """Fetches version information from a remote Chrome instance. - - Args: - endpoint: The base URL to connent to. - - Returns: - A dictionary containing Browser and Content version information: - { - 'Browser': { - 'major': integer, - 'minor': integer, - 'fix': integer, - 'day': integer - }, - 'Content': { - 'name': string, - 'major': integer, - 'minor': integer - } - } - - Raises: - RuntimeError: When Browser version info can't be fetched or parsed. - """ - try: - f = urllib2.urlopen(endpoint + '/json/version') - result = f.read(); - result = simplejson.loads(result) - except urllib2.URLError, e: - raise RuntimeError( - 'Error accessing Chrome instance debugging port: ' + str(e)) - - if 'Browser' not in result: - raise RuntimeError('Browser version is not specified.') - - parsed = re.search('^Chrome\/(\d+).(\d+).(\d+).(\d+)', result['Browser']) - if parsed is None: - raise RuntimeError('Browser-Version cannot be parsed.') - try: - day = int(parsed.group(3)) - browser_info = { - 'major': int(parsed.group(1)), - 'minor': int(parsed.group(2)), - 'day': day, - 'fix': int(parsed.group(4)), - } - except ValueError: - raise RuntimeError('Browser-Version cannot be parsed.') - - if 'WebKit-Version' not in result: - raise RuntimeError('Content-Version is not specified.') - - parsed = re.search('^(\d+)\.(\d+)', result['WebKit-Version']) - if parsed is None: - raise RuntimeError('Content-Version cannot be parsed.') - - try: - platform_info = { - 'name': 'Blink' if day > 1464 else 'WebKit', - 'major': int(parsed.group(1)), - 'minor': int(parsed.group(2)), - } - except ValueError: - raise RuntimeError('WebKit-Version cannot be parsed.') - - return { - 'browser': browser_info, - 'platform': platform_info - } - - def _IsContentVersionNotOlderThan(self, major, minor): - """Compares remote Browser Content version with specified one. - - Args: - major: Major Webkit version. - minor: Minor Webkit version. - - Returns: - True if remote Content version is same or newer than specified, - False otherwise. - - Raises: - RuntimeError: If remote Content version hasn't been fetched yet. - """ - if not hasattr(self, '_version'): - raise RuntimeError('Browser version has not been fetched yet.') - version = self._version['platform'] - - if version['major'] < major: - return False - elif version['major'] == major and version['minor'] < minor: - return False - else: - return True - - def _IsBrowserDayNumberGreaterThan(self, day_number): - """Compares remote Chromium day number with specified one. - - Args: - day_number: Forth part of the chromium version. - - Returns: - True if remote Chromium day number is same or newer than specified, - False otherwise. - - Raises: - RuntimeError: If remote Chromium version hasn't been fetched yet. - """ - if not hasattr(self, '_version'): - raise RuntimeError('Browser revision has not been fetched yet.') - version = self._version['browser'] - - return version['day'] > day_number |