Index: chrome/test/pyautolib/perf_snapshot.py |
diff --git a/chrome/test/pyautolib/perf_snapshot.py b/chrome/test/pyautolib/perf_snapshot.py |
index 3135041078321463d73063574495f340bb4c1e0e..e27d638e1636169b251fe6603475d50819f71a71 100755 |
--- a/chrome/test/pyautolib/perf_snapshot.py |
+++ b/chrome/test/pyautolib/perf_snapshot.py |
@@ -37,6 +37,7 @@ import logging |
import optparse |
import simplejson |
import socket |
+import sys |
import threading |
import time |
import urllib2 |
@@ -187,7 +188,7 @@ class _DevToolsSocketRequest(object): |
value is True only if all results for this request are known). |
""" |
def __init__(self, method, message_id): |
- """Initializes a DevToolsSocket request. |
+ """Initialize. |
Args: |
method: The string method name for this request. |
@@ -212,7 +213,7 @@ class _DevToolsSocketRequest(object): |
class _DevToolsSocketClient(asyncore.dispatcher): |
"""Client that communicates with a remote Chrome instance via sockets. |
- This class works in conjunction with the _PerformanceSnapshotterThread class |
+ This class works in conjunction with the _RemoteInspectorBaseThread 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. |
@@ -225,12 +226,12 @@ class _DevToolsSocketClient(asyncore.dispatcher): |
handshake_done: A boolean indicating whether or not the client has completed |
the required protocol handshake with the remote Chrome |
instance. |
- snapshotter: An instance of the _PerformanceSnapshotterThread class that is |
- working together with this class to communicate with a remote |
- Chrome instance. |
+ inspector_thread: An instance of the _RemoteInspectorBaseThread 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): |
- """Initializes the DevToolsSocketClient. |
+ """Initialize. |
Args: |
verbose: A boolean indicating whether or not to use verbose logging. |
@@ -251,8 +252,10 @@ class _DevToolsSocketClient(asyncore.dispatcher): |
self._read_buffer = '' |
self._write_buffer = '' |
+ self._socket_buffer_lock = threading.Lock() |
+ |
self.handshake_done = False |
- self.snapshotter = None |
+ self.inspector_thread = None |
# Connect to the remote Chrome instance and initiate the protocol handshake. |
self.create_socket(socket.AF_INET, socket.SOCK_STREAM) |
@@ -292,6 +295,7 @@ class _DevToolsSocketClient(asyncore.dispatcher): |
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: |
@@ -305,9 +309,11 @@ class _DevToolsSocketClient(asyncore.dispatcher): |
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) |
@@ -326,8 +332,8 @@ class _DevToolsSocketClient(asyncore.dispatcher): |
'%s\n' |
'========================') % data |
print msg |
- if self.snapshotter: |
- self.snapshotter.NotifyReply(data) |
+ 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. |
@@ -345,6 +351,7 @@ class _DevToolsSocketClient(asyncore.dispatcher): |
'%s\n' |
'=========================') % data |
print msg |
+ self._socket_buffer_lock.release() |
def handle_close(self): |
"""Called when the socket is closed; overridden from asyncore.""" |
@@ -366,29 +373,239 @@ class _DevToolsSocketClient(asyncore.dispatcher): |
def handle_error(self): |
"""Called when an exception is raised; overridden from asyncore.""" |
self.close() |
- self.snapshotter.NotifySocketClientException() |
+ self.inspector_thread.NotifySocketClientException() |
asyncore.dispatcher.handle_error(self) |
-class _PerformanceSnapshotterThread(threading.Thread): |
- """Manages communication with a remote Chrome instance to take snapshots. |
+class _RemoteInspectorBaseThread(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 debugging |
+ 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. |
+ This base class should be subclassed for each different type of action that |
+ needs to be performed using the remote inspector (e.g., take a v8 heap |
+ snapshot, force a garbage collect): |
+ |
+ * Each subclass should override the run() method to customize the work done |
+ by the thread, making sure to call self._client.close() when done. |
+ * If overriding __init__ in a subclass, the base class __init__ must also be |
+ invoked. |
+ * The HandleReply() function should be overridden if special handling needs |
+ to be performed using the reply messages received from the remote Chrome |
+ instance. |
+ |
Public Methods: |
+ NotifySocketClientException: Notifies the current object that the |
+ _DevToolsSocketClient encountered an exception. |
+ Called by the _DevToolsSocketClient. |
NotifyReply: Notifies the current object of a reply message that has been |
received from the remote Chrome instance (which would have been |
sent in response to an earlier request). Called by the |
_DevToolsSocketClient. |
- NotifySocketClientException: Notifies the current object that the |
- _DevToolsSocketClient encountered an exception. |
- Called by the _DevToolsSocketClient. |
+ HandleReply: Processes a reply message received from the remote Chrome |
+ instance. Should be overridden by a subclass if special |
+ result handling needs to be performed. |
run: Starts the thread of execution for this object. Invoked implicitly |
- by calling the start() method on this object. |
+ by calling the start() method on this object. Should be overridden |
+ by a subclass. |
+ """ |
+ def __init__(self, tab_index, verbose, show_socket_messages): |
+ """Initialize. |
+ |
+ Args: |
+ tab_index: The integer index of the tab in the remote Chrome instance to |
+ use for snapshotting. |
+ 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('_RemoteInspectorBaseThread') |
+ self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) |
+ |
+ self._killed = False |
+ self._next_request_id = 1 |
+ self._requests = [] |
+ |
+ # Create a DevToolsSocket client and wait for it to complete the remote |
+ # debugging protocol handshake with the remote Chrome instance. |
+ result = self._IdentifyDevToolsSocketConnectionInfo(tab_index) |
+ 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) |
+ |
+ def NotifySocketClientException(self): |
+ """Notifies that the _DevToolsSocketClient encountered an exception.""" |
+ self._killed = True |
+ |
+ 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) |
+ 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_complete = True |
+ self.HandleReply(reply_dict) |
+ |
+ def HandleReply(self, reply_dict): |
+ """Processes a reply message received from the remote Chrome instance. |
+ |
+ Override this function to specially handle reply messages from the remote |
+ Chrome instance. |
+ |
+ Args: |
+ reply_dict: A dictionary representing the reply message received from the |
+ remote Chrome instance. |
+ """ |
+ pass |
+ |
+ def run(self): |
+ """Start _PerformanceSnapshotterThread; overridden from threading.Thread. |
+ |
+ Should be overridden in a subclass. |
+ """ |
+ self._client.close() |
+ |
+ 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 _GetFirstIncompleteRequest(self, method): |
+ """Identifies the first incomplete request with the given method name. |
+ |
+ 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 complete 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_complete 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. |
+ |
+ 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 == 'Profiler.takeHeapSnapshot': |
+ # We always want detailed v8 heap snapshot information. |
+ request.params = {'detailed': True} |
+ elif request.method == 'Profiler.getProfile': |
+ # 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, |
+ 'Profiler.takeHeapSnapshot') |
+ if last_req and 'uid' in last_req.results: |
+ request.params = {'type': 'HEAP', 'uid': last_req.results['uid']} |
+ |
+ @staticmethod |
+ def _IdentifyDevToolsSocketConnectionInfo(tab_index): |
+ """Identifies DevToolsSocket connection info from a remote Chrome instance. |
+ |
+ Args: |
+ tab_index: The integer index of the tab in the remote Chrome instance to |
+ which to connect. |
+ |
+ Returns: |
+ A dictionary containing the DevToolsSocket connection info: |
+ { |
+ 'host': string, |
+ 'port': integer, |
+ 'path': string, |
+ } |
+ |
+ Raises: |
+ RuntimeError: When DevToolsSocket connection info cannot be identified. |
+ """ |
+ try: |
+ # TODO(dennisjeffrey): Do not assume port 9222. The port should be passed |
+ # as input to this function. |
+ f = urllib2.urlopen('http://localhost:9222/json') |
+ result = f.read(); |
+ result = simplejson.loads(result) |
+ except urllib2.URLError, e: |
+ raise RuntimeError( |
+ 'Error accessing Chrome instance debugging port: ' + str(e)) |
+ |
+ if tab_index >= len(result): |
+ raise RuntimeError( |
+ 'Specified tab index %d doesn\'t exist (%d tabs found)' % |
+ (tab_index, len(result))) |
+ |
+ if 'webSocketDebuggerUrl' not in result[tab_index]: |
+ raise RuntimeError('No socket URL exists for the specified tab.') |
+ |
+ socket_url = result[tab_index]['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 _PerformanceSnapshotterThread(_RemoteInspectorBaseThread): |
+ """Manages communication with a remote Chrome to take v8 heap snapshots. |
Public Attributes: |
collected_heap_snapshot_data: A list of dictionaries, where each dictionary |
@@ -405,7 +622,7 @@ class _PerformanceSnapshotterThread(threading.Thread): |
def __init__( |
self, tab_index, output_file, interval, num_snapshots, verbose, |
show_socket_messages, interactive_mode): |
- """Initializes a _PerformanceSnapshotterThread object. |
+ """Initialize. |
Args: |
tab_index: The integer index of the tab in the remote Chrome instance to |
@@ -423,56 +640,30 @@ class _PerformanceSnapshotterThread(threading.Thread): |
with the remote Chrome instance. |
interactive_mode: A boolean indicating whether or not to take snapshots |
in interactive mode. |
- |
- Raises: |
- RuntimeError: When no proper connection can be made to a remote Chrome |
- instance. |
""" |
- threading.Thread.__init__(self) |
- |
- self._logger = logging.getLogger('_PerformanceSnapshotterThread') |
- self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) |
+ _RemoteInspectorBaseThread.__init__(self, tab_index, verbose, |
+ show_socket_messages) |
self._output_file = output_file |
self._interval = interval |
self._num_snapshots = num_snapshots |
self._interactive_mode = interactive_mode |
- self._next_request_id = 1 |
- self._requests = [] |
self._current_heap_snapshot = [] |
self._url = '' |
self.collected_heap_snapshot_data = [] |
self.last_snapshot_start_time = 0 |
- self._killed = False |
- |
- # Create a DevToolsSocket client and wait for it to complete the remote |
- # debugging protocol handshake with the remote Chrome instance. |
- result = self._IdentifyDevToolsSocketConnectionInfo(tab_index) |
- self._client = _DevToolsSocketClient( |
- verbose, show_socket_messages, result['host'], result['port'], |
- result['path']) |
- self._client.snapshotter = self |
- while asyncore.socket_map: |
- if self._client.handshake_done or self._killed: |
- break |
- asyncore.loop(timeout=1, count=1) |
- |
- def NotifyReply(self, msg): |
- """Notifies of a reply message received from the remote Chrome instance. |
+ def HandleReply(self, reply_dict): |
+ """Processes 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: A dictionary object representing the reply message received |
+ from the remote inspector. |
""" |
- reply_dict = simplejson.loads(msg) |
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_complete = True |
if 'frameTree' in reply_dict['result']: |
self._url = reply_dict['result']['frameTree']['frame']['url'] |
elif 'method' in reply_dict: |
@@ -524,10 +715,6 @@ class _PerformanceSnapshotterThread(threading.Thread): |
self._logger.debug('Heap snapshot analysis complete (%s).', |
total_size_str) |
- def NotifySocketClientException(self): |
- """Notifies that the _DevToolsSocketClient encountered an exception.""" |
- self._killed = True |
- |
@staticmethod |
def _ConvertBytesToHumanReadableString(num_bytes): |
"""Converts an integer number of bytes into a human-readable string. |
@@ -545,131 +732,12 @@ class _PerformanceSnapshotterThread(threading.Thread): |
else: |
return '%.2f MB' % (num_bytes / 1048576.0) |
- def _IdentifyDevToolsSocketConnectionInfo(self, tab_index): |
- """Identifies DevToolsSocket connection info from a remote Chrome instance. |
- |
- Args: |
- tab_index: The integer index of the tab in the remote Chrome instance to |
- which to connect. |
- |
- Returns: |
- A dictionary containing the DevToolsSocket connection info: |
- { |
- 'host': string, |
- 'port': integer, |
- 'path': string, |
- } |
- |
- Raises: |
- RuntimeError: When DevToolsSocket connection info cannot be identified. |
- """ |
- try: |
- # TODO(dennisjeffrey): Do not assume port 9222. The port should be passed |
- # as input to this function. |
- f = urllib2.urlopen('http://localhost:9222/json') |
- result = f.read(); |
- result = simplejson.loads(result) |
- except urllib2.URLError, e: |
- raise RuntimeError( |
- 'Error accessing Chrome instance debugging port: ' + str(e)) |
- |
- if tab_index >= len(result): |
- raise RuntimeError( |
- 'Specified tab index %d doesn\'t exist (%d tabs found)' % |
- (tab_index, len(result))) |
- |
- if 'webSocketDebuggerUrl' not in result[tab_index]: |
- raise RuntimeError('No socket URL exists for the specified tab.') |
- |
- socket_url = result[tab_index]['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}) |
- |
def _ResetRequests(self): |
"""Clears snapshot-related info in preparation for a new snapshot.""" |
self._requests = [] |
self._current_heap_snapshot = [] |
self._url = '' |
- 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 _GetFirstIncompleteRequest(self, method): |
- """Identifies the first incomplete request with the given method name. |
- |
- 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 complete 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_complete 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. |
- |
- Returns: |
- The latest request 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 request object associated with a request message that is |
- about to be sent. |
- """ |
- if request.method == 'Profiler.takeHeapSnapshot': |
- # We always want detailed heap snapshot information. |
- request.params = {'detailed': True} |
- elif request.method == 'Profiler.getProfile': |
- # 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, |
- 'Profiler.takeHeapSnapshot') |
- if last_req and 'uid' in last_req.results: |
- request.params = {'type': 'HEAP', 'uid': last_req.results['uid']} |
- |
def _TakeHeapSnapshot(self): |
"""Takes a heap snapshot by communicating with _DevToolsSocketClient. |
@@ -740,12 +808,46 @@ class _PerformanceSnapshotterThread(threading.Thread): |
self._logger.debug('Snapshotter thread finished.') |
+class _GarbageCollectThread(_RemoteInspectorBaseThread): |
+ """Manages communication with a remote Chrome to force a garbage collect.""" |
+ |
+ _COLLECT_GARBAGE_MESSAGES = [ |
+ 'Profiler.collectGarbage', |
+ ] |
+ |
+ def run(self): |
+ """Start _GarbageCollectThread; overridden from threading.Thread.""" |
+ if self._killed: |
+ return |
+ |
+ # Prepare the request list. |
+ for message in self._COLLECT_GARBAGE_MESSAGES: |
+ self._requests.append( |
+ _DevToolsSocketRequest(message, self._next_request_id)) |
+ self._next_request_id += 1 |
+ |
+ # 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)) |
+ while not request.is_complete: |
+ if self._killed: |
+ return |
+ time.sleep(0.1) |
+ self._client.close() |
+ return |
+ |
+ |
+# TODO(dennisjeffrey): The "verbose" option used in this file should re-use |
+# pyauto's verbose flag. |
class PerformanceSnapshotter(object): |
"""Main class for taking v8 heap snapshots. |
Public Methods: |
HeapSnapshot: Begins taking heap snapshots according to the initialization |
parameters for the current object. |
+ GarbageCollect: Forces a garbage collection. |
SetInteractiveMode: Sets the current object to take snapshots in interactive |
mode. Only used by the main() function in this script |
when the 'interactive mode' command-line flag is set. |
@@ -757,7 +859,7 @@ class PerformanceSnapshotter(object): |
def __init__( |
self, tab_index=0, output_file=None, interval=DEFAULT_SNAPSHOT_INTERVAL, |
num_snapshots=1, verbose=False, show_socket_messages=False): |
- """Initializes a PerformanceSnapshotter object. |
+ """Initialize. |
Args: |
tab_index: The integer index of the tab in the remote Chrome instance to |
@@ -816,6 +918,20 @@ class PerformanceSnapshotter(object): |
self._logger.debug('Done taking snapshots.') |
return snapshotter_thread.collected_heap_snapshot_data |
+ def GarbageCollect(self): |
+ """Forces a garbage collection.""" |
+ gc_thread = _GarbageCollectThread(self._tab_index, self._verbose, |
+ self._show_socket_messages) |
+ gc_thread.start() |
+ try: |
+ while asyncore.socket_map: |
+ if not gc_thread.is_alive(): |
+ break |
+ asyncore.loop(timeout=1, count=1) |
+ except KeyboardInterrupt: |
+ pass |
+ gc_thread.join() |
+ |
def SetInteractiveMode(self): |
"""Sets the current object to take snapshots in interactive mode.""" |
self._interactive_mode = True |