2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Chrome remote inspector utility for pyauto tests.
8 This script provides a python interface that acts as a front-end for Chrome's
9 remote inspector module, communicating via sockets to interact with Chrome in
10 the same way that the Developer Tools does. This -- in theory -- should allow
11 a pyauto test to do anything that Chrome's Developer Tools does, as long as the
12 appropriate communication with the remote inspector is implemented in this
15 This script assumes that Chrome is already running on the local machine with
16 flag '--remote-debugging-port=9222' to enable remote debugging on port 9222.
18 To use this module, first create an instance of class RemoteInspectorClient;
19 doing this sets up a connection to Chrome's remote inspector. Then call the
20 appropriate functions on that object to perform the desired actions with the
21 remote inspector. When done, call Stop() on the RemoteInspectorClient object
22 to stop communication with the remote inspector.
24 For example, to take v8 heap snapshots from a pyauto test:
26 import remote_inspector_client
27 my_client = remote_inspector_client.RemoteInspectorClient()
28 snapshot_info = my_client.HeapSnapshot(include_summary=True)
30 new_snapshot_info = my_client.HeapSnapshot(include_summary=True)
33 It is expected that a test will only use one instance of RemoteInspectorClient
34 at a time. If a second instance is instantiated, a RuntimeError will be raised.
35 RemoteInspectorClient could be made into a singleton in the future if the need
54 class _DevToolsSocketRequest(object):
55 """A representation of a single DevToolsSocket request.
57 A DevToolsSocket request is used for communication with a remote Chrome
58 instance when interacting with the renderer process of a given webpage.
59 Requests and results are passed as specially-formatted JSON messages,
60 according to a communication protocol defined in WebKit. The string
61 representation of this request will be a JSON message that is properly
62 formatted according to the communication protocol.
65 method: The string method name associated with this request.
66 id: A unique integer id associated with this request.
67 params: A dictionary of input parameters associated with this request.
68 results: A dictionary of relevant results obtained from the remote Chrome
69 instance that are associated with this request.
70 is_fulfilled: A boolean indicating whether or not this request has been sent
71 and all relevant results for it have been obtained (i.e., this value is
72 True only if all results for this request are known).
73 is_fulfilled_condition: A threading.Condition for waiting for the request to
77 def __init__(self
, method
, params
, message_id
):
81 method: The string method name for this request.
82 message_id: An integer id for this request, which is assumed to be unique
83 from among all requests.
89 self
.is_fulfilled
= False
90 self
.is_fulfilled_condition
= threading
.Condition()
94 json_dict
['method'] = self
.method
95 json_dict
['id'] = self
.id
97 json_dict
['params'] = self
.params
98 return simplejson
.dumps(json_dict
, separators
=(',', ':'))
101 class _DevToolsSocketClient(asyncore
.dispatcher
):
102 """Client that communicates with a remote Chrome instance via sockets.
104 This class works in conjunction with the _RemoteInspectorThread class to
105 communicate with a remote Chrome instance following the remote debugging
106 communication protocol in WebKit. This class performs the lower-level work
107 of socket communication.
110 handshake_done: A boolean indicating whether or not the client has completed
111 the required protocol handshake with the remote Chrome instance.
112 inspector_thread: An instance of the _RemoteInspectorThread class that is
113 working together with this class to communicate with a remote Chrome
117 def __init__(self
, verbose
, show_socket_messages
, hostname
, port
, path
):
121 verbose: A boolean indicating whether or not to use verbose logging.
122 show_socket_messages: A boolean indicating whether or not to show the
123 socket messages sent/received when communicating with the remote
125 hostname: The string hostname of the DevToolsSocket to which to connect.
126 port: The integer port number of the DevToolsSocket to which to connect.
127 path: The string path of the DevToolsSocket to which to connect.
129 asyncore
.dispatcher
.__init
__(self
)
131 self
._logger
= logging
.getLogger('_DevToolsSocketClient')
132 self
._logger
.setLevel([logging
.WARNING
, logging
.DEBUG
][verbose
])
134 self
._show
_socket
_messages
= show_socket_messages
136 self
._read
_buffer
= ''
137 self
._write
_buffer
= ''
139 self
._socket
_buffer
_lock
= threading
.Lock()
141 self
.handshake_done
= False
142 self
.inspector_thread
= None
144 # Connect to the remote Chrome instance and initiate the protocol handshake.
145 self
.create_socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
146 self
.connect((hostname
, port
))
149 'Upgrade: WebSocket',
150 'Connection: Upgrade',
151 'Host: %s:%d' % (hostname
, port
),
152 'Origin: http://%s:%d' % (hostname
, port
),
153 'Sec-WebSocket-Key1: 4k0L66E ZU 8 5 <18 <TK 7 7',
154 'Sec-WebSocket-Key2: s2 20 `# 4| 3 9 U_ 1299',
156 handshake_msg
= ('GET %s HTTP/1.1\r\n%s\r\n\r\n\x47\x30\x22\x2D\x5A\x3F'
157 '\x47\x58' % (path
, '\r\n'.join(fields
)))
158 self
._Write
(handshake_msg
.encode('utf-8'))
160 def SendMessage(self
, msg
):
161 """Causes a request message to be sent to the remote Chrome instance.
164 msg: A string message to be sent; assumed to be a JSON message in proper
165 format according to the remote debugging protocol in WebKit.
167 # According to the communication protocol, each request message sent over
168 # the wire must begin with '\x00' and end with '\xff'.
169 self
._Write
('\x00' + msg
.encode('utf-8') + '\xff')
171 def _Write(self
, msg
):
172 """Causes a raw message to be sent to the remote Chrome instance.
175 msg: A raw string message to be sent.
177 self
._write
_buffer
+= msg
180 def handle_write(self
):
181 """Called if a writable socket can be written; overridden from asyncore."""
182 self
._socket
_buffer
_lock
.acquire()
183 if self
._write
_buffer
:
184 sent
= self
.send(self
._write
_buffer
)
185 if self
._show
_socket
_messages
:
186 msg_type
= ['Handshake', 'Message'][self
._write
_buffer
[0] == '\x00' and
187 self
._write
_buffer
[-1] == '\xff']
188 msg
= ('========================\n'
190 '========================\n'
192 '========================') % (msg_type
,
193 self
._write
_buffer
[:sent
-1])
195 self
._write
_buffer
= self
._write
_buffer
[sent
:]
196 self
._socket
_buffer
_lock
.release()
198 def handle_read(self
):
199 """Called when a socket can be read; overridden from asyncore."""
200 self
._socket
_buffer
_lock
.acquire()
201 if self
.handshake_done
:
202 # Process a message reply from the remote Chrome instance.
203 self
._read
_buffer
+= self
.recv(4096)
204 pos
= self
._read
_buffer
.find('\xff')
207 data
= self
._read
_buffer
[:pos
-len('\xff')]
208 pos2
= data
.find('\x00')
210 data
= data
[pos2
+ 1:]
211 self
._read
_buffer
= self
._read
_buffer
[pos
:]
212 if self
._show
_socket
_messages
:
213 msg
= ('========================\n'
214 'Received Message:\n'
215 '========================\n'
217 '========================') % data
219 if self
.inspector_thread
:
220 self
.inspector_thread
.NotifyReply(data
)
221 pos
= self
._read
_buffer
.find('\xff')
223 # Process a handshake reply from the remote Chrome instance.
224 self
._read
_buffer
+= self
.recv(4096)
225 pos
= self
._read
_buffer
.find('\r\n\r\n')
227 pos
+= len('\r\n\r\n')
228 data
= self
._read
_buffer
[:pos
]
229 self
._read
_buffer
= self
._read
_buffer
[pos
:]
230 self
.handshake_done
= True
231 if self
._show
_socket
_messages
:
232 msg
= ('=========================\n'
233 'Received Handshake Reply:\n'
234 '=========================\n'
236 '=========================') % data
238 self
._socket
_buffer
_lock
.release()
240 def handle_close(self
):
241 """Called when the socket is closed; overridden from asyncore."""
242 if self
._show
_socket
_messages
:
243 msg
= ('=========================\n'
245 '=========================')
250 """Determines if writes can occur for this socket; overridden from asyncore.
253 True, if there is something to write to the socket, or
256 return len(self
._write
_buffer
) > 0
258 def handle_expt(self
):
259 """Called when out-of-band data exists; overridden from asyncore."""
262 def handle_error(self
):
263 """Called when an exception is raised; overridden from asyncore."""
264 if self
._show
_socket
_messages
:
265 msg
= ('=========================\n'
267 '=========================')
270 self
.inspector_thread
.ClientSocketExceptionOccurred()
271 asyncore
.dispatcher
.handle_error(self
)
274 class _RemoteInspectorThread(threading
.Thread
):
275 """Manages communication using Chrome's remote inspector protocol.
277 This class works in conjunction with the _DevToolsSocketClient class to
278 communicate with a remote Chrome instance following the remote inspector
279 communication protocol in WebKit. This class performs the higher-level work
280 of managing request and reply messages, whereas _DevToolsSocketClient handles
281 the lower-level work of socket communication.
284 def __init__(self
, url
, tab_index
, tab_filter
, verbose
, show_socket_messages
,
289 url: The base URL to connent to.
290 tab_index: The integer index of the tab in the remote Chrome instance to
291 use for snapshotting.
292 tab_filter: When specified, is run over tabs of the remote Chrome
293 instances to choose which one to connect to.
294 verbose: A boolean indicating whether or not to use verbose logging.
295 show_socket_messages: A boolean indicating whether or not to show the
296 socket messages sent/received when communicating with the remote
299 threading
.Thread
.__init
__(self
)
300 self
._logger
= logging
.getLogger('_RemoteInspectorThread')
301 self
._logger
.setLevel([logging
.WARNING
, logging
.DEBUG
][verbose
])
305 self
._action
_queue
= []
306 self
._action
_queue
_condition
= threading
.Condition()
307 self
._action
_specific
_callback
= None # Callback only for current action.
308 self
._action
_specific
_callback
_lock
= threading
.Lock()
309 self
._general
_callbacks
= [] # General callbacks that can be long-lived.
310 self
._general
_callbacks
_lock
= threading
.Lock()
311 self
._condition
_to
_wait
= None
312 self
._agent
_name
= agent_name
314 # Create a DevToolsSocket client and wait for it to complete the remote
315 # debugging protocol handshake with the remote Chrome instance.
316 result
= self
._IdentifyDevToolsSocketConnectionInfo
(
317 url
, tab_index
, tab_filter
)
318 self
._client
= _DevToolsSocketClient(
319 verbose
, show_socket_messages
, result
['host'], result
['port'],
321 self
._client
.inspector_thread
= self
322 while asyncore
.socket_map
:
323 if self
._client
.handshake_done
or self
._killed
:
325 asyncore
.loop(timeout
=1, count
=1, use_poll
=True)
327 def ClientSocketExceptionOccurred(self
):
328 """Notifies that the _DevToolsSocketClient encountered an exception."""
331 def NotifyReply(self
, msg
):
332 """Notifies of a reply message received from the remote Chrome instance.
335 msg: A string reply message received from the remote Chrome instance;
336 assumed to be a JSON message formatted according to the remote
337 debugging communication protocol in WebKit.
339 reply_dict
= simplejson
.loads(msg
)
341 # Notify callbacks of this message received from the remote inspector.
342 self
._action
_specific
_callback
_lock
.acquire()
343 if self
._action
_specific
_callback
:
344 self
._action
_specific
_callback
(reply_dict
)
345 self
._action
_specific
_callback
_lock
.release()
347 self
._general
_callbacks
_lock
.acquire()
348 if self
._general
_callbacks
:
349 for callback
in self
._general
_callbacks
:
351 self
._general
_callbacks
_lock
.release()
353 if 'result' in reply_dict
:
354 # This is the result message associated with a previously-sent request.
355 request
= self
.GetRequestWithId(reply_dict
['id'])
357 request
.is_fulfilled_condition
.acquire()
358 request
.is_fulfilled_condition
.notify()
359 request
.is_fulfilled_condition
.release()
362 """Start this thread; overridden from threading.Thread."""
363 while not self
._killed
:
364 self
._action
_queue
_condition
.acquire()
365 if self
._action
_queue
:
366 # There's a request to the remote inspector that needs to be processed.
367 messages
, callback
= self
._action
_queue
.pop(0)
368 self
._action
_specific
_callback
_lock
.acquire()
369 self
._action
_specific
_callback
= callback
370 self
._action
_specific
_callback
_lock
.release()
372 # Prepare the request list.
373 for message_id
, message
in enumerate(messages
):
374 self
._requests
.append(
375 _DevToolsSocketRequest(message
[0], message
[1], message_id
))
377 # Send out each request. Wait until each request is complete before
378 # sending the next request.
379 for request
in self
._requests
:
380 self
._FillInParams
(request
)
381 self
._client
.SendMessage(str(request
))
383 request
.is_fulfilled_condition
.acquire()
384 self
._condition
_to
_wait
= request
.is_fulfilled_condition
385 request
.is_fulfilled_condition
.wait()
386 request
.is_fulfilled_condition
.release()
392 # Clean up so things are ready for the next request.
395 self
._action
_specific
_callback
_lock
.acquire()
396 self
._action
_specific
_callback
= None
397 self
._action
_specific
_callback
_lock
.release()
399 # Wait until there is something to process.
400 self
._condition
_to
_wait
= self
._action
_queue
_condition
401 self
._action
_queue
_condition
.wait()
402 self
._action
_queue
_condition
.release()
406 """Notify this thread that it should stop executing."""
408 # The thread might be waiting on a condition.
409 if self
._condition
_to
_wait
:
410 self
._condition
_to
_wait
.acquire()
411 self
._condition
_to
_wait
.notify()
412 self
._condition
_to
_wait
.release()
414 def PerformAction(self
, request_messages
, reply_message_callback
):
415 """Notify this thread of an action to perform using the remote inspector.
418 request_messages: A list of strings representing the requests to make
419 using the remote inspector.
420 reply_message_callback: A callable to be invoked any time a message is
421 received from the remote inspector while the current action is
422 being performed. The callable should accept a single argument,
423 which is a dictionary representing a message received.
425 self
._action
_queue
_condition
.acquire()
426 self
._action
_queue
.append((request_messages
, reply_message_callback
))
427 self
._action
_queue
_condition
.notify()
428 self
._action
_queue
_condition
.release()
430 def AddMessageCallback(self
, callback
):
431 """Add a callback to invoke for messages received from the remote inspector.
434 callback: A callable to be invoked any time a message is received from the
435 remote inspector. The callable should accept a single argument, which
436 is a dictionary representing a message received.
438 self
._general
_callbacks
_lock
.acquire()
439 self
._general
_callbacks
.append(callback
)
440 self
._general
_callbacks
_lock
.release()
442 def RemoveMessageCallback(self
, callback
):
443 """Remove a callback from the set of those to invoke for messages received.
446 callback: A callable to remove from consideration.
448 self
._general
_callbacks
_lock
.acquire()
449 self
._general
_callbacks
.remove(callback
)
450 self
._general
_callbacks
_lock
.release()
452 def GetRequestWithId(self
, request_id
):
453 """Identifies the request with the specified id.
456 request_id: An integer request id; should be unique for each request.
459 A request object associated with the given id if found, or
462 found_request
= [x
for x
in self
._requests
if x
.id == request_id
]
464 return found_request
[0]
467 def GetFirstUnfulfilledRequest(self
, method
):
468 """Identifies the first unfulfilled request with the given method name.
470 An unfulfilled request is one for which all relevant reply messages have
471 not yet been received from the remote inspector.
474 method: The string method name of the request for which to search.
477 The first request object in the request list that is not yet fulfilled
478 and is also associated with the given method name, or
479 None if no such request object can be found.
481 for request
in self
._requests
:
482 if not request
.is_fulfilled
and request
.method
== method
:
486 def _GetLatestRequestOfType(self
, ref_req
, method
):
487 """Identifies the latest specified request before a reference request.
489 This function finds the latest request with the specified method that
490 occurs before the given reference request.
493 ref_req: A reference request from which to start looking.
494 method: The string method name of the request for which to search.
497 The latest _DevToolsSocketRequest object with the specified method,
498 if found, or None otherwise.
500 start_looking
= False
501 for request
in self
._requests
[::-1]:
502 if request
.id == ref_req
.id:
505 if request
.method
== method
:
509 def _FillInParams(self
, request
):
510 """Fills in parameters for requests as necessary before the request is sent.
513 request: The _DevToolsSocketRequest object associated with a request
514 message that is about to be sent.
516 if request
.method
== self
._agent
_name
+'.takeHeapSnapshot':
517 # We always want detailed v8 heap snapshot information.
518 request
.params
= {'detailed': True}
519 elif request
.method
== self
._agent
_name
+ '.getHeapSnapshot':
520 # To actually request the snapshot data from a previously-taken snapshot,
521 # we need to specify the unique uid of the snapshot we want.
522 # The relevant uid should be contained in the last
523 # 'Profiler.takeHeapSnapshot' request object.
524 last_req
= self
._GetLatestRequestOfType
(request
,
525 self
._agent
_name
+ '.takeHeapSnapshot')
526 if last_req
and 'uid' in last_req
.results
:
527 request
.params
= {'uid': last_req
.results
['uid']}
528 elif request
.method
== self
._agent
_name
+ '.getProfile':
529 # TODO(eustas): Remove this case after M27 is released.
530 last_req
= self
._GetLatestRequestOfType
(request
,
531 self
._agent
_name
+ '.takeHeapSnapshot')
532 if last_req
and 'uid' in last_req
.results
:
533 request
.params
= {'type': 'HEAP', 'uid': last_req
.results
['uid']}
536 def _IdentifyDevToolsSocketConnectionInfo(url
, tab_index
, tab_filter
):
537 """Identifies DevToolsSocket connection info from a remote Chrome instance.
540 url: The base URL to connent to.
541 tab_index: The integer index of the tab in the remote Chrome instance to
543 tab_filter: When specified, is run over tabs of the remote Chrome instance
544 to choose which one to connect to.
547 A dictionary containing the DevToolsSocket connection info:
555 RuntimeError: When DevToolsSocket connection info cannot be identified.
558 f
= urllib2
.urlopen(url
+ '/json')
560 logging
.debug(result
)
561 result
= simplejson
.loads(result
)
562 except urllib2
.URLError
, e
:
564 'Error accessing Chrome instance debugging port: ' + str(e
))
567 connect_to
= filter(tab_filter
, result
)[0]
569 if tab_index
>= len(result
):
571 'Specified tab index %d doesn\'t exist (%d tabs found)' %
572 (tab_index
, len(result
)))
573 connect_to
= result
[tab_index
]
575 logging
.debug(simplejson
.dumps(connect_to
))
577 if 'webSocketDebuggerUrl' not in connect_to
:
578 raise RuntimeError('No socket URL exists for the specified tab.')
580 socket_url
= connect_to
['webSocketDebuggerUrl']
581 parsed
= urlparse
.urlparse(socket_url
)
582 # On ChromeOS, the "ws://" scheme may not be recognized, leading to an
583 # incorrect netloc (and empty hostname and port attributes) in |parsed|.
584 # Change the scheme to "http://" to fix this.
585 if not parsed
.hostname
or not parsed
.port
:
586 socket_url
= 'http' + socket_url
[socket_url
.find(':'):]
587 parsed
= urlparse
.urlparse(socket_url
)
588 # Warning: |parsed.scheme| is incorrect after this point.
589 return ({'host': parsed
.hostname
,
591 'path': parsed
.path
})
594 class _RemoteInspectorDriverThread(threading
.Thread
):
595 """Drives the communication service with the remote inspector."""
599 threading
.Thread
.__init
__(self
)
602 """Drives the communication service with the remote inspector."""
604 while asyncore
.socket_map
:
605 asyncore
.loop(timeout
=1, count
=1, use_poll
=True)
606 except KeyboardInterrupt:
610 class _V8HeapSnapshotParser(object):
611 """Parses v8 heap snapshot data."""
612 _CHILD_TYPES
= ['context', 'element', 'property', 'internal', 'hidden',
614 _NODE_TYPES
= ['hidden', 'array', 'string', 'object', 'code', 'closure',
615 'regexp', 'number', 'native', 'synthetic']
618 def ParseSnapshotData(raw_data
):
619 """Parses raw v8 heap snapshot data and returns the summarized results.
621 The raw heap snapshot data is represented as a JSON object with the
622 following keys: 'snapshot', 'nodes', and 'strings'.
624 The 'snapshot' value provides the 'title' and 'uid' attributes for the
625 snapshot. For example:
626 { u'title': u'org.webkit.profiles.user-initiated.1', u'uid': 1}
628 The 'nodes' value is a list of node information from the v8 heap, with a
629 special first element that describes the node serialization layout (see
630 HeapSnapshotJSONSerializer::SerializeNodes). All other list elements
631 contain information about nodes in the v8 heap, according to the
632 serialization layout.
634 The 'strings' value is a list of strings, indexed by values in the 'nodes'
635 list to associate nodes with strings.
638 raw_data: A string representing the raw v8 heap snapshot data.
641 A dictionary containing the summarized v8 heap snapshot data:
643 'total_v8_node_count': integer, # Total number of nodes in the v8 heap.
644 'total_shallow_size': integer, # Total heap size, in bytes.
648 total_shallow_size
= 0
651 # TODO(dennisjeffrey): The following line might be slow, especially on
652 # ChromeOS. Investigate faster alternatives.
653 heap
= simplejson
.loads(raw_data
)
655 index
= 1 # Bypass the special first node list item.
656 node_list
= heap
['nodes']
657 while index
< len(node_list
):
658 node_type
= node_list
[index
]
659 node_name
= node_list
[index
+ 1]
660 node_id
= node_list
[index
+ 2]
661 node_self_size
= node_list
[index
+ 3]
662 node_retained_size
= node_list
[index
+ 4]
663 node_dominator
= node_list
[index
+ 5]
664 node_children_count
= node_list
[index
+ 6]
668 for i
in xrange(node_children_count
):
669 child_type
= node_list
[index
]
670 child_type_string
= _V8HeapSnapshotParser
._CHILD
_TYPES
[int(child_type
)]
671 child_name_index
= node_list
[index
+ 1]
672 child_to_node
= node_list
[index
+ 2]
676 'type': child_type_string
,
677 'name_or_index': child_name_index
,
678 'to_node': child_to_node
,
680 node_children
.append(child_info
)
682 # Get the constructor string for this node so nodes can be grouped by
684 # See HeapSnapshot.js: WebInspector.HeapSnapshotNode.prototype.
685 type_string
= _V8HeapSnapshotParser
._NODE
_TYPES
[int(node_type
)]
686 constructor_name
= None
687 if type_string
== 'hidden':
688 constructor_name
= '(system)'
689 elif type_string
== 'object':
690 constructor_name
= heap
['strings'][int(node_name
)]
691 elif type_string
== 'native':
692 pos
= heap
['strings'][int(node_name
)].find('/')
694 constructor_name
= heap
['strings'][int(node_name
)][:pos
].rstrip()
696 constructor_name
= heap
['strings'][int(node_name
)]
697 elif type_string
== 'code':
698 constructor_name
= '(compiled code)'
700 constructor_name
= '(' + type_string
+ ')'
704 'name': heap
['strings'][int(node_name
)],
706 'self_size': node_self_size
,
707 'retained_size': node_retained_size
,
708 'dominator': node_dominator
,
709 'children_count': node_children_count
,
710 'children': node_children
,
713 if constructor_name
not in constructors
:
714 constructors
[constructor_name
] = []
715 constructors
[constructor_name
].append(node_obj
)
717 total_node_count
+= 1
718 total_shallow_size
+= node_self_size
720 # TODO(dennisjeffrey): Have this function also return more detailed v8
721 # heap snapshot data when a need for it arises (e.g., using |constructors|).
723 result
['total_v8_node_count'] = total_node_count
724 result
['total_shallow_size'] = total_shallow_size
728 # TODO(dennisjeffrey): The "verbose" option used in this file should re-use
729 # pyauto's verbose flag.
730 class RemoteInspectorClient(object):
731 """Main class for interacting with Chrome's remote inspector.
733 Upon initialization, a socket connection to Chrome's remote inspector will
734 be established. Users of this class should call Stop() to close the
735 connection when it's no longer needed.
738 Stop: Close the connection to the remote inspector. Should be called when
739 a user is done using this module.
740 HeapSnapshot: Takes a v8 heap snapshot and returns the summarized data.
741 GetMemoryObjectCounts: Retrieves memory object count information.
742 CollectGarbage: Forces a garbage collection.
743 StartTimelineEventMonitoring: Starts monitoring for timeline events.
744 StopTimelineEventMonitoring: Stops monitoring for timeline events.
747 # TODO(dennisjeffrey): Allow a user to specify a window index too (not just a
748 # tab index), when running through PyAuto.
749 def __init__(self
, tab_index
=0, tab_filter
=None,
750 verbose
=False, show_socket_messages
=False,
751 url
='http://localhost:9222'):
755 tab_index: The integer index of the tab in the remote Chrome instance to
756 which to connect. Defaults to 0 (the first tab).
757 tab_filter: When specified, is run over tabs of the remote Chrome
758 instance to choose which one to connect to.
759 verbose: A boolean indicating whether or not to use verbose logging.
760 show_socket_messages: A boolean indicating whether or not to show the
761 socket messages sent/received when communicating with the remote
764 self
._tab
_index
= tab_index
765 self
._tab
_filter
= tab_filter
766 self
._verbose
= verbose
767 self
._show
_socket
_messages
= show_socket_messages
769 self
._timeline
_started
= False
771 logging
.basicConfig()
772 self
._logger
= logging
.getLogger('RemoteInspectorClient')
773 self
._logger
.setLevel([logging
.WARNING
, logging
.DEBUG
][verbose
])
775 # Creating _RemoteInspectorThread might raise an exception. This prevents an
776 # AttributeError in the destructor.
777 self
._remote
_inspector
_thread
= None
778 self
._remote
_inspector
_driver
_thread
= None
780 self
._version
= self
._GetVersion
(url
)
782 # TODO(loislo): Remove this hack after M28 is released.
783 self
._agent
_name
= 'Profiler'
784 if self
._IsBrowserDayNumberGreaterThan
(1470):
785 self
._agent
_name
= 'HeapProfiler'
787 # Start up a thread for long-term communication with the remote inspector.
788 self
._remote
_inspector
_thread
= _RemoteInspectorThread(
789 url
, tab_index
, tab_filter
, verbose
, show_socket_messages
,
791 self
._remote
_inspector
_thread
.start()
792 # At this point, a connection has already been made to the remote inspector.
794 # This thread calls asyncore.loop, which activates the channel service.
795 self
._remote
_inspector
_driver
_thread
= _RemoteInspectorDriverThread()
796 self
._remote
_inspector
_driver
_thread
.start()
799 """Called on destruction of this object."""
803 """Stop/close communication with the remote inspector."""
804 if self
._remote
_inspector
_thread
:
805 self
._remote
_inspector
_thread
.Kill()
806 self
._remote
_inspector
_thread
.join()
807 self
._remote
_inspector
_thread
= None
808 if self
._remote
_inspector
_driver
_thread
:
809 self
._remote
_inspector
_driver
_thread
.join()
810 self
._remote
_inspector
_driver
_thread
= None
812 def HeapSnapshot(self
, include_summary
=False):
813 """Takes a v8 heap snapshot.
816 A dictionary containing information for a single v8 heap
817 snapshot that was taken.
819 'url': string, # URL of the webpage that was snapshotted.
820 'raw_data': string, # The raw data as JSON string.
821 'total_v8_node_count': integer, # Total number of nodes in the v8 heap.
822 # Only if |include_summary| is True.
823 'total_heap_size': integer, # Total v8 heap size (number of bytes).
824 # Only if |include_summary| is True.
827 HEAP_SNAPSHOT_MESSAGES
= [
828 ('Page.getResourceTree', {}),
829 ('Debugger.enable', {}),
830 (self
._agent
_name
+ '.clearProfiles', {}),
831 (self
._agent
_name
+ '.takeHeapSnapshot', {}),
832 (self
._agent
_name
+ '.getHeapSnapshot', {}),
835 self
._current
_heap
_snapshot
= []
837 self
._collected
_heap
_snapshot
_data
= {}
839 done_condition
= threading
.Condition()
841 def HandleReply(reply_dict
):
842 """Processes a reply message received from the remote Chrome instance.
845 reply_dict: A dictionary object representing the reply message received
846 from the remote inspector.
848 if 'result' in reply_dict
:
849 # This is the result message associated with a previously-sent request.
850 request
= self
._remote
_inspector
_thread
.GetRequestWithId(
852 if 'frameTree' in reply_dict
['result']:
853 self
._url
= reply_dict
['result']['frameTree']['frame']['url']
854 elif request
.method
== self
._agent
_name
+ '.getHeapSnapshot':
855 # A heap snapshot has been completed. Analyze and output the data.
856 self
._logger
.debug('Heap snapshot taken: %s', self
._url
)
857 # TODO(dennisjeffrey): Parse the heap snapshot on-the-fly as the data
858 # is coming in over the wire, so we can avoid storing the entire
859 # snapshot string in memory.
860 raw_snapshot_data
= ''.join(self
._current
_heap
_snapshot
)
861 self
._collected
_heap
_snapshot
_data
= {
863 'raw_data': raw_snapshot_data
}
865 self
._logger
.debug('Now analyzing heap snapshot...')
866 parser
= _V8HeapSnapshotParser()
867 time_start
= time
.time()
868 self
._logger
.debug('Raw snapshot data size: %.2f MB',
869 len(raw_snapshot_data
) / (1024.0 * 1024.0))
870 result
= parser
.ParseSnapshotData(raw_snapshot_data
)
871 self
._logger
.debug('Time to parse data: %.2f sec',
872 time
.time() - time_start
)
873 count
= result
['total_v8_node_count']
874 self
._collected
_heap
_snapshot
_data
['total_v8_node_count'] = count
875 total_size
= result
['total_shallow_size']
876 self
._collected
_heap
_snapshot
_data
['total_heap_size'] = total_size
878 done_condition
.acquire()
879 done_condition
.notify()
880 done_condition
.release()
881 elif 'method' in reply_dict
:
882 # This is an auxiliary message sent from the remote Chrome instance.
883 if reply_dict
['method'] == self
._agent
_name
+ '.addProfileHeader':
885 self
._remote
_inspector
_thread
.GetFirstUnfulfilledRequest(
886 self
._agent
_name
+ '.takeHeapSnapshot'))
888 snapshot_req
.results
['uid'] = reply_dict
['params']['header']['uid']
889 elif reply_dict
['method'] == self
._agent
_name
+ '.addHeapSnapshotChunk':
890 self
._current
_heap
_snapshot
.append(reply_dict
['params']['chunk'])
892 # Tell the remote inspector to take a v8 heap snapshot, then wait until
893 # the snapshot information is available to return.
894 self
._remote
_inspector
_thread
.PerformAction(HEAP_SNAPSHOT_MESSAGES
,
897 done_condition
.acquire()
898 done_condition
.wait()
899 done_condition
.release()
901 return self
._collected
_heap
_snapshot
_data
903 def EvaluateJavaScript(self
, expression
):
904 """Evaluates a JavaScript expression and returns the result.
906 Sends a message containing the expression to the remote Chrome instance we
907 are connected to, and evaluates it in the context of the tab we are
908 connected to. Blocks until the result is available and returns it.
911 A dictionary representing the result.
913 EVALUATE_MESSAGES
= [
914 ('Runtime.evaluate', { 'expression': expression
,
915 'objectGroup': 'group',
916 'returnByValue': True }),
917 ('Runtime.releaseObjectGroup', { 'objectGroup': 'group' })
921 done_condition
= threading
.Condition()
923 def HandleReply(reply_dict
):
924 """Processes a reply message received from the remote Chrome instance.
927 reply_dict: A dictionary object representing the reply message received
928 from the remote Chrome instance.
930 if 'result' in reply_dict
and 'result' in reply_dict
['result']:
931 self
._result
= reply_dict
['result']['result']['value']
933 done_condition
.acquire()
934 done_condition
.notify()
935 done_condition
.release()
937 # Tell the remote inspector to evaluate the given expression, then wait
938 # until that information is available to return.
939 self
._remote
_inspector
_thread
.PerformAction(EVALUATE_MESSAGES
,
942 done_condition
.acquire()
943 done_condition
.wait()
944 done_condition
.release()
948 def GetMemoryObjectCounts(self
):
949 """Retrieves memory object count information.
952 A dictionary containing the memory object count information:
954 'DOMNodeCount': integer, # Total number of DOM nodes.
955 'EventListenerCount': integer, # Total number of event listeners.
958 MEMORY_COUNT_MESSAGES
= [
959 ('Memory.getDOMCounters', {})
962 self
._event
_listener
_count
= None
963 self
._dom
_node
_count
= None
965 done_condition
= threading
.Condition()
966 def HandleReply(reply_dict
):
967 """Processes a reply message received from the remote Chrome instance.
970 reply_dict: A dictionary object representing the reply message received
971 from the remote Chrome instance.
973 if 'result' in reply_dict
:
974 self
._event
_listener
_count
= reply_dict
['result']['jsEventListeners']
975 self
._dom
_node
_count
= reply_dict
['result']['nodes']
977 done_condition
.acquire()
978 done_condition
.notify()
979 done_condition
.release()
981 # Tell the remote inspector to collect memory count info, then wait until
982 # that information is available to return.
983 self
._remote
_inspector
_thread
.PerformAction(MEMORY_COUNT_MESSAGES
,
986 done_condition
.acquire()
987 done_condition
.wait()
988 done_condition
.release()
991 'DOMNodeCount': self
._dom
_node
_count
,
992 'EventListenerCount': self
._event
_listener
_count
,
995 def CollectGarbage(self
):
996 """Forces a garbage collection."""
997 COLLECT_GARBAGE_MESSAGES
= [
998 ('Profiler.collectGarbage', {})
1001 # Tell the remote inspector to do a garbage collect. We can return
1002 # immediately, since there is no result for which to wait.
1003 self
._remote
_inspector
_thread
.PerformAction(COLLECT_GARBAGE_MESSAGES
, None)
1005 def StartTimelineEventMonitoring(self
, event_callback
):
1006 """Starts timeline event monitoring.
1009 event_callback: A callable to invoke whenever a timeline event is observed
1010 from the remote inspector. The callable should take a single input,
1011 which is a dictionary containing the detailed information of a
1014 if self
._timeline
_started
:
1015 self
._logger
.warning('Timeline monitoring already started.')
1017 TIMELINE_MESSAGES
= [
1018 ('Timeline.start', {})
1021 self
._event
_callback
= event_callback
1023 done_condition
= threading
.Condition()
1024 def HandleReply(reply_dict
):
1025 """Processes a reply message received from the remote Chrome instance.
1028 reply_dict: A dictionary object representing the reply message received
1029 from the remote Chrome instance.
1031 if 'result' in reply_dict
:
1032 done_condition
.acquire()
1033 done_condition
.notify()
1034 done_condition
.release()
1035 if reply_dict
.get('method') == 'Timeline.eventRecorded':
1036 self
._event
_callback
(reply_dict
['params']['record'])
1038 # Tell the remote inspector to start the timeline.
1039 self
._timeline
_callback
= HandleReply
1040 self
._remote
_inspector
_thread
.AddMessageCallback(self
._timeline
_callback
)
1041 self
._remote
_inspector
_thread
.PerformAction(TIMELINE_MESSAGES
, None)
1043 done_condition
.acquire()
1044 done_condition
.wait()
1045 done_condition
.release()
1047 self
._timeline
_started
= True
1049 def StopTimelineEventMonitoring(self
):
1050 """Stops timeline event monitoring."""
1051 if not self
._timeline
_started
:
1052 self
._logger
.warning('Timeline monitoring already stopped.')
1054 TIMELINE_MESSAGES
= [
1055 ('Timeline.stop', {})
1058 done_condition
= threading
.Condition()
1059 def HandleReply(reply_dict
):
1060 """Processes a reply message received from the remote Chrome instance.
1063 reply_dict: A dictionary object representing the reply message received
1064 from the remote Chrome instance.
1066 if 'result' in reply_dict
:
1067 done_condition
.acquire()
1068 done_condition
.notify()
1069 done_condition
.release()
1071 # Tell the remote inspector to stop the timeline.
1072 self
._remote
_inspector
_thread
.RemoveMessageCallback(self
._timeline
_callback
)
1073 self
._remote
_inspector
_thread
.PerformAction(TIMELINE_MESSAGES
, HandleReply
)
1075 done_condition
.acquire()
1076 done_condition
.wait()
1077 done_condition
.release()
1079 self
._timeline
_started
= False
1081 def _ConvertByteCountToHumanReadableString(self
, num_bytes
):
1082 """Converts an integer number of bytes into a human-readable string.
1085 num_bytes: An integer number of bytes.
1088 A human-readable string representation of the given number of bytes.
1090 if num_bytes
< 1024:
1091 return '%d B' % num_bytes
1092 elif num_bytes
< 1048576:
1093 return '%.2f KB' % (num_bytes
/ 1024.0)
1095 return '%.2f MB' % (num_bytes
/ 1048576.0)
1098 def _GetVersion(endpoint
):
1099 """Fetches version information from a remote Chrome instance.
1102 endpoint: The base URL to connent to.
1105 A dictionary containing Browser and Content version information:
1121 RuntimeError: When Browser version info can't be fetched or parsed.
1124 f
= urllib2
.urlopen(endpoint
+ '/json/version')
1126 result
= simplejson
.loads(result
)
1127 except urllib2
.URLError
, e
:
1129 'Error accessing Chrome instance debugging port: ' + str(e
))
1131 if 'Browser' not in result
:
1132 raise RuntimeError('Browser version is not specified.')
1134 parsed
= re
.search('^Chrome\/(\d+).(\d+).(\d+).(\d+)', result
['Browser'])
1136 raise RuntimeError('Browser-Version cannot be parsed.')
1138 day
= int(parsed
.group(3))
1140 'major': int(parsed
.group(1)),
1141 'minor': int(parsed
.group(2)),
1143 'fix': int(parsed
.group(4)),
1146 raise RuntimeError('Browser-Version cannot be parsed.')
1148 if 'WebKit-Version' not in result
:
1149 raise RuntimeError('Content-Version is not specified.')
1151 parsed
= re
.search('^(\d+)\.(\d+)', result
['WebKit-Version'])
1153 raise RuntimeError('Content-Version cannot be parsed.')
1157 'name': 'Blink' if day
> 1464 else 'WebKit',
1158 'major': int(parsed
.group(1)),
1159 'minor': int(parsed
.group(2)),
1162 raise RuntimeError('WebKit-Version cannot be parsed.')
1165 'browser': browser_info
,
1166 'platform': platform_info
1169 def _IsContentVersionNotOlderThan(self
, major
, minor
):
1170 """Compares remote Browser Content version with specified one.
1173 major: Major Webkit version.
1174 minor: Minor Webkit version.
1177 True if remote Content version is same or newer than specified,
1181 RuntimeError: If remote Content version hasn't been fetched yet.
1183 if not hasattr(self
, '_version'):
1184 raise RuntimeError('Browser version has not been fetched yet.')
1185 version
= self
._version
['platform']
1187 if version
['major'] < major
:
1189 elif version
['major'] == major
and version
['minor'] < minor
:
1194 def _IsBrowserDayNumberGreaterThan(self
, day_number
):
1195 """Compares remote Chromium day number with specified one.
1198 day_number: Forth part of the chromium version.
1201 True if remote Chromium day number is same or newer than specified,
1205 RuntimeError: If remote Chromium version hasn't been fetched yet.
1207 if not hasattr(self
, '_version'):
1208 raise RuntimeError('Browser revision has not been fetched yet.')
1209 version
= self
._version
['browser']
1211 return version
['day'] > day_number