| Index: chrome/test/pyautolib/remote_inspector_client.py
|
| ===================================================================
|
| --- chrome/test/pyautolib/remote_inspector_client.py (revision 261231)
|
| +++ chrome/test/pyautolib/remote_inspector_client.py (working copy)
|
| @@ -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
|
|
|