- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / test / pyautolib / remote_inspector_client.py
1 #!/usr/bin/env python
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.
5
6 """Chrome remote inspector utility for pyauto tests.
7
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
13 script.
14
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.
17
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.
23
24 For example, to take v8 heap snapshots from a pyauto test:
25
26 import remote_inspector_client
27 my_client = remote_inspector_client.RemoteInspectorClient()
28 snapshot_info = my_client.HeapSnapshot(include_summary=True)
29 // Do some stuff...
30 new_snapshot_info = my_client.HeapSnapshot(include_summary=True)
31 my_client.Stop()
32
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
36 for it arises.
37 """
38
39 import asyncore
40 import datetime
41 import logging
42 import optparse
43 import pprint
44 import re
45 import simplejson
46 import socket
47 import sys
48 import threading
49 import time
50 import urllib2
51 import urlparse
52
53
54 class _DevToolsSocketRequest(object):
55   """A representation of a single DevToolsSocket request.
56
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.
63
64   Public Attributes:
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
74         be fulfilled.
75   """
76
77   def __init__(self, method, params, message_id):
78     """Initialize.
79
80     Args:
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.
84     """
85     self.method = method
86     self.id = message_id
87     self.params = params
88     self.results = {}
89     self.is_fulfilled = False
90     self.is_fulfilled_condition = threading.Condition()
91
92   def __repr__(self):
93     json_dict = {}
94     json_dict['method'] = self.method
95     json_dict['id'] = self.id
96     if self.params:
97       json_dict['params'] = self.params
98     return simplejson.dumps(json_dict, separators=(',', ':'))
99
100
101 class _DevToolsSocketClient(asyncore.dispatcher):
102   """Client that communicates with a remote Chrome instance via sockets.
103
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.
108
109   Public Attributes:
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
114         instance.
115   """
116
117   def __init__(self, verbose, show_socket_messages, hostname, port, path):
118     """Initialize.
119
120     Args:
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
124           Chrome instance.
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.
128     """
129     asyncore.dispatcher.__init__(self)
130
131     self._logger = logging.getLogger('_DevToolsSocketClient')
132     self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose])
133
134     self._show_socket_messages = show_socket_messages
135
136     self._read_buffer = ''
137     self._write_buffer = ''
138
139     self._socket_buffer_lock = threading.Lock()
140
141     self.handshake_done = False
142     self.inspector_thread = None
143
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))
147
148     fields = [
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',
155     ]
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'))
159
160   def SendMessage(self, msg):
161     """Causes a request message to be sent to the remote Chrome instance.
162
163     Args:
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.
166     """
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')
170
171   def _Write(self, msg):
172     """Causes a raw message to be sent to the remote Chrome instance.
173
174     Args:
175       msg: A raw string message to be sent.
176     """
177     self._write_buffer += msg
178     self.handle_write()
179
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'
189                'Sent %s:\n'
190                '========================\n'
191                '%s\n'
192                '========================') % (msg_type,
193                                               self._write_buffer[:sent-1])
194         print msg
195       self._write_buffer = self._write_buffer[sent:]
196     self._socket_buffer_lock.release()
197
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')
205       while pos >= 0:
206         pos += len('\xff')
207         data = self._read_buffer[:pos-len('\xff')]
208         pos2 = data.find('\x00')
209         if pos2 >= 0:
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'
216                  '%s\n'
217                  '========================') % data
218           print msg
219         if self.inspector_thread:
220           self.inspector_thread.NotifyReply(data)
221         pos = self._read_buffer.find('\xff')
222     else:
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')
226       if pos >= 0:
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'
235                  '%s\n'
236                  '=========================') % data
237           print msg
238     self._socket_buffer_lock.release()
239
240   def handle_close(self):
241     """Called when the socket is closed; overridden from asyncore."""
242     if self._show_socket_messages:
243       msg = ('=========================\n'
244              'Socket closed.\n'
245              '=========================')
246       print msg
247     self.close()
248
249   def writable(self):
250     """Determines if writes can occur for this socket; overridden from asyncore.
251
252     Returns:
253       True, if there is something to write to the socket, or
254       False, otherwise.
255     """
256     return len(self._write_buffer) > 0
257
258   def handle_expt(self):
259     """Called when out-of-band data exists; overridden from asyncore."""
260     self.handle_error()
261
262   def handle_error(self):
263     """Called when an exception is raised; overridden from asyncore."""
264     if self._show_socket_messages:
265       msg = ('=========================\n'
266              'Socket error.\n'
267              '=========================')
268       print msg
269     self.close()
270     self.inspector_thread.ClientSocketExceptionOccurred()
271     asyncore.dispatcher.handle_error(self)
272
273
274 class _RemoteInspectorThread(threading.Thread):
275   """Manages communication using Chrome's remote inspector protocol.
276
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.
282   """
283
284   def __init__(self, url, tab_index, tab_filter, verbose, show_socket_messages,
285                agent_name):
286     """Initialize.
287
288     Args:
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
297           Chrome instance.
298     """
299     threading.Thread.__init__(self)
300     self._logger = logging.getLogger('_RemoteInspectorThread')
301     self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose])
302
303     self._killed = False
304     self._requests = []
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
313
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'],
320         result['path'])
321     self._client.inspector_thread = self
322     while asyncore.socket_map:
323       if self._client.handshake_done or self._killed:
324         break
325       asyncore.loop(timeout=1, count=1, use_poll=True)
326
327   def ClientSocketExceptionOccurred(self):
328     """Notifies that the _DevToolsSocketClient encountered an exception."""
329     self.Kill()
330
331   def NotifyReply(self, msg):
332     """Notifies of a reply message received from the remote Chrome instance.
333
334     Args:
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.
338     """
339     reply_dict = simplejson.loads(msg)
340
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()
346
347     self._general_callbacks_lock.acquire()
348     if self._general_callbacks:
349       for callback in self._general_callbacks:
350         callback(reply_dict)
351     self._general_callbacks_lock.release()
352
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'])
356       if request:
357         request.is_fulfilled_condition.acquire()
358         request.is_fulfilled_condition.notify()
359         request.is_fulfilled_condition.release()
360
361   def run(self):
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()
371
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))
376
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))
382
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()
387
388           if self._killed:
389             self._client.close()
390             return
391
392         # Clean up so things are ready for the next request.
393         self._requests = []
394
395         self._action_specific_callback_lock.acquire()
396         self._action_specific_callback = None
397         self._action_specific_callback_lock.release()
398
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()
403     self._client.close()
404
405   def Kill(self):
406     """Notify this thread that it should stop executing."""
407     self._killed = True
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()
413
414   def PerformAction(self, request_messages, reply_message_callback):
415     """Notify this thread of an action to perform using the remote inspector.
416
417     Args:
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.
424     """
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()
429
430   def AddMessageCallback(self, callback):
431     """Add a callback to invoke for messages received from the remote inspector.
432
433     Args:
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.
437     """
438     self._general_callbacks_lock.acquire()
439     self._general_callbacks.append(callback)
440     self._general_callbacks_lock.release()
441
442   def RemoveMessageCallback(self, callback):
443     """Remove a callback from the set of those to invoke for messages received.
444
445     Args:
446       callback: A callable to remove from consideration.
447     """
448     self._general_callbacks_lock.acquire()
449     self._general_callbacks.remove(callback)
450     self._general_callbacks_lock.release()
451
452   def GetRequestWithId(self, request_id):
453     """Identifies the request with the specified id.
454
455     Args:
456       request_id: An integer request id; should be unique for each request.
457
458     Returns:
459       A request object associated with the given id if found, or
460       None otherwise.
461     """
462     found_request = [x for x in self._requests if x.id == request_id]
463     if found_request:
464       return found_request[0]
465     return None
466
467   def GetFirstUnfulfilledRequest(self, method):
468     """Identifies the first unfulfilled request with the given method name.
469
470     An unfulfilled request is one for which all relevant reply messages have
471     not yet been received from the remote inspector.
472
473     Args:
474       method: The string method name of the request for which to search.
475
476     Returns:
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.
480     """
481     for request in self._requests:
482       if not request.is_fulfilled and request.method == method:
483         return request
484     return None
485
486   def _GetLatestRequestOfType(self, ref_req, method):
487     """Identifies the latest specified request before a reference request.
488
489     This function finds the latest request with the specified method that
490     occurs before the given reference request.
491
492     Args:
493       ref_req: A reference request from which to start looking.
494       method: The string method name of the request for which to search.
495
496     Returns:
497       The latest _DevToolsSocketRequest object with the specified method,
498       if found, or None otherwise.
499     """
500     start_looking = False
501     for request in self._requests[::-1]:
502       if request.id == ref_req.id:
503         start_looking = True
504       elif start_looking:
505         if request.method == method:
506           return request
507     return None
508
509   def _FillInParams(self, request):
510     """Fills in parameters for requests as necessary before the request is sent.
511
512     Args:
513       request: The _DevToolsSocketRequest object associated with a request
514                message that is about to be sent.
515     """
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']}
534
535   @staticmethod
536   def _IdentifyDevToolsSocketConnectionInfo(url, tab_index, tab_filter):
537     """Identifies DevToolsSocket connection info from a remote Chrome instance.
538
539     Args:
540       url: The base URL to connent to.
541       tab_index: The integer index of the tab in the remote Chrome instance to
542           which to connect.
543       tab_filter: When specified, is run over tabs of the remote Chrome instance
544           to choose which one to connect to.
545
546     Returns:
547       A dictionary containing the DevToolsSocket connection info:
548       {
549         'host': string,
550         'port': integer,
551         'path': string,
552       }
553
554     Raises:
555       RuntimeError: When DevToolsSocket connection info cannot be identified.
556     """
557     try:
558       f = urllib2.urlopen(url + '/json')
559       result = f.read()
560       logging.debug(result)
561       result = simplejson.loads(result)
562     except urllib2.URLError, e:
563       raise RuntimeError(
564           'Error accessing Chrome instance debugging port: ' + str(e))
565
566     if tab_filter:
567       connect_to = filter(tab_filter, result)[0]
568     else:
569       if tab_index >= len(result):
570         raise RuntimeError(
571             'Specified tab index %d doesn\'t exist (%d tabs found)' %
572             (tab_index, len(result)))
573       connect_to = result[tab_index]
574
575     logging.debug(simplejson.dumps(connect_to))
576
577     if 'webSocketDebuggerUrl' not in connect_to:
578       raise RuntimeError('No socket URL exists for the specified tab.')
579
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,
590              'port': parsed.port,
591              'path': parsed.path})
592
593
594 class _RemoteInspectorDriverThread(threading.Thread):
595   """Drives the communication service with the remote inspector."""
596
597   def __init__(self):
598     """Initialize."""
599     threading.Thread.__init__(self)
600
601   def run(self):
602     """Drives the communication service with the remote inspector."""
603     try:
604       while asyncore.socket_map:
605         asyncore.loop(timeout=1, count=1, use_poll=True)
606     except KeyboardInterrupt:
607       pass
608
609
610 class _V8HeapSnapshotParser(object):
611   """Parses v8 heap snapshot data."""
612   _CHILD_TYPES = ['context', 'element', 'property', 'internal', 'hidden',
613                   'shortcut', 'weak']
614   _NODE_TYPES = ['hidden', 'array', 'string', 'object', 'code', 'closure',
615                  'regexp', 'number', 'native', 'synthetic']
616
617   @staticmethod
618   def ParseSnapshotData(raw_data):
619     """Parses raw v8 heap snapshot data and returns the summarized results.
620
621     The raw heap snapshot data is represented as a JSON object with the
622     following keys: 'snapshot', 'nodes', and 'strings'.
623
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}
627
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.
633
634     The 'strings' value is a list of strings, indexed by values in the 'nodes'
635     list to associate nodes with strings.
636
637     Args:
638       raw_data: A string representing the raw v8 heap snapshot data.
639
640     Returns:
641       A dictionary containing the summarized v8 heap snapshot data:
642       {
643         'total_v8_node_count': integer,  # Total number of nodes in the v8 heap.
644         'total_shallow_size': integer, # Total heap size, in bytes.
645       }
646     """
647     total_node_count = 0
648     total_shallow_size = 0
649     constructors = {}
650
651     # TODO(dennisjeffrey): The following line might be slow, especially on
652     # ChromeOS.  Investigate faster alternatives.
653     heap = simplejson.loads(raw_data)
654
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]
665       index += 7
666
667       node_children = []
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]
673         index += 3
674
675         child_info = {
676           'type': child_type_string,
677           'name_or_index': child_name_index,
678           'to_node': child_to_node,
679         }
680         node_children.append(child_info)
681
682       # Get the constructor string for this node so nodes can be grouped by
683       # constructor.
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('/')
693         if pos >= 0:
694           constructor_name = heap['strings'][int(node_name)][:pos].rstrip()
695         else:
696           constructor_name = heap['strings'][int(node_name)]
697       elif type_string == 'code':
698         constructor_name = '(compiled code)'
699       else:
700         constructor_name = '(' + type_string + ')'
701
702       node_obj = {
703         'type': type_string,
704         'name': heap['strings'][int(node_name)],
705         'id': node_id,
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,
711       }
712
713       if constructor_name not in constructors:
714         constructors[constructor_name] = []
715       constructors[constructor_name].append(node_obj)
716
717       total_node_count += 1
718       total_shallow_size += node_self_size
719
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|).
722     result = {}
723     result['total_v8_node_count'] = total_node_count
724     result['total_shallow_size'] = total_shallow_size
725     return result
726
727
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.
732
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.
736
737   Public Methods:
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.
745   """
746
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'):
752     """Initialize.
753
754     Args:
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
762           Chrome instance.
763     """
764     self._tab_index = tab_index
765     self._tab_filter = tab_filter
766     self._verbose = verbose
767     self._show_socket_messages = show_socket_messages
768
769     self._timeline_started = False
770
771     logging.basicConfig()
772     self._logger = logging.getLogger('RemoteInspectorClient')
773     self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose])
774
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
779
780     self._version = self._GetVersion(url)
781
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'
786
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,
790         self._agent_name)
791     self._remote_inspector_thread.start()
792     # At this point, a connection has already been made to the remote inspector.
793
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()
797
798   def __del__(self):
799     """Called on destruction of this object."""
800     self.Stop()
801
802   def Stop(self):
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
811
812   def HeapSnapshot(self, include_summary=False):
813     """Takes a v8 heap snapshot.
814
815     Returns:
816       A dictionary containing information for a single v8 heap
817       snapshot that was taken.
818       {
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.
825       }
826     """
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', {}),
833     ]
834
835     self._current_heap_snapshot = []
836     self._url = ''
837     self._collected_heap_snapshot_data = {}
838
839     done_condition = threading.Condition()
840
841     def HandleReply(reply_dict):
842       """Processes a reply message received from the remote Chrome instance.
843
844       Args:
845         reply_dict: A dictionary object representing the reply message received
846                      from the remote inspector.
847       """
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(
851             reply_dict['id'])
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 = {
862               'url': self._url,
863               'raw_data': raw_snapshot_data}
864           if include_summary:
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
877
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':
884           snapshot_req = (
885               self._remote_inspector_thread.GetFirstUnfulfilledRequest(
886                   self._agent_name + '.takeHeapSnapshot'))
887           if snapshot_req:
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'])
891
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,
895                                                 HandleReply)
896
897     done_condition.acquire()
898     done_condition.wait()
899     done_condition.release()
900
901     return self._collected_heap_snapshot_data
902
903   def EvaluateJavaScript(self, expression):
904     """Evaluates a JavaScript expression and returns the result.
905
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.
909
910     Returns:
911       A dictionary representing the result.
912     """
913     EVALUATE_MESSAGES = [
914       ('Runtime.evaluate', { 'expression': expression,
915                              'objectGroup': 'group',
916                              'returnByValue': True }),
917       ('Runtime.releaseObjectGroup', { 'objectGroup': 'group' })
918     ]
919
920     self._result = None
921     done_condition = threading.Condition()
922
923     def HandleReply(reply_dict):
924       """Processes a reply message received from the remote Chrome instance.
925
926       Args:
927         reply_dict: A dictionary object representing the reply message received
928                     from the remote Chrome instance.
929       """
930       if 'result' in reply_dict and 'result' in reply_dict['result']:
931         self._result = reply_dict['result']['result']['value']
932
933         done_condition.acquire()
934         done_condition.notify()
935         done_condition.release()
936
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,
940                                                 HandleReply)
941
942     done_condition.acquire()
943     done_condition.wait()
944     done_condition.release()
945
946     return self._result
947
948   def GetMemoryObjectCounts(self):
949     """Retrieves memory object count information.
950
951     Returns:
952       A dictionary containing the memory object count information:
953       {
954         'DOMNodeCount': integer,  # Total number of DOM nodes.
955         'EventListenerCount': integer,  # Total number of event listeners.
956       }
957     """
958     MEMORY_COUNT_MESSAGES = [
959       ('Memory.getDOMCounters', {})
960     ]
961
962     self._event_listener_count = None
963     self._dom_node_count = None
964
965     done_condition = threading.Condition()
966     def HandleReply(reply_dict):
967       """Processes a reply message received from the remote Chrome instance.
968
969       Args:
970         reply_dict: A dictionary object representing the reply message received
971                     from the remote Chrome instance.
972       """
973       if 'result' in reply_dict:
974         self._event_listener_count = reply_dict['result']['jsEventListeners']
975         self._dom_node_count = reply_dict['result']['nodes']
976
977         done_condition.acquire()
978         done_condition.notify()
979         done_condition.release()
980
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,
984                                                 HandleReply)
985
986     done_condition.acquire()
987     done_condition.wait()
988     done_condition.release()
989
990     return {
991       'DOMNodeCount': self._dom_node_count,
992       'EventListenerCount': self._event_listener_count,
993     }
994
995   def CollectGarbage(self):
996     """Forces a garbage collection."""
997     COLLECT_GARBAGE_MESSAGES = [
998       ('Profiler.collectGarbage', {})
999     ]
1000
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)
1004
1005   def StartTimelineEventMonitoring(self, event_callback):
1006     """Starts timeline event monitoring.
1007
1008     Args:
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
1012           timeline event.
1013     """
1014     if self._timeline_started:
1015       self._logger.warning('Timeline monitoring already started.')
1016       return
1017     TIMELINE_MESSAGES = [
1018       ('Timeline.start', {})
1019     ]
1020
1021     self._event_callback = event_callback
1022
1023     done_condition = threading.Condition()
1024     def HandleReply(reply_dict):
1025       """Processes a reply message received from the remote Chrome instance.
1026
1027       Args:
1028         reply_dict: A dictionary object representing the reply message received
1029                     from the remote Chrome instance.
1030       """
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'])
1037
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)
1042
1043     done_condition.acquire()
1044     done_condition.wait()
1045     done_condition.release()
1046
1047     self._timeline_started = True
1048
1049   def StopTimelineEventMonitoring(self):
1050     """Stops timeline event monitoring."""
1051     if not self._timeline_started:
1052       self._logger.warning('Timeline monitoring already stopped.')
1053       return
1054     TIMELINE_MESSAGES = [
1055       ('Timeline.stop', {})
1056     ]
1057
1058     done_condition = threading.Condition()
1059     def HandleReply(reply_dict):
1060       """Processes a reply message received from the remote Chrome instance.
1061
1062       Args:
1063         reply_dict: A dictionary object representing the reply message received
1064                     from the remote Chrome instance.
1065       """
1066       if 'result' in reply_dict:
1067         done_condition.acquire()
1068         done_condition.notify()
1069         done_condition.release()
1070
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)
1074
1075     done_condition.acquire()
1076     done_condition.wait()
1077     done_condition.release()
1078
1079     self._timeline_started = False
1080
1081   def _ConvertByteCountToHumanReadableString(self, num_bytes):
1082     """Converts an integer number of bytes into a human-readable string.
1083
1084     Args:
1085       num_bytes: An integer number of bytes.
1086
1087     Returns:
1088       A human-readable string representation of the given number of bytes.
1089     """
1090     if num_bytes < 1024:
1091       return '%d B' % num_bytes
1092     elif num_bytes < 1048576:
1093       return '%.2f KB' % (num_bytes / 1024.0)
1094     else:
1095       return '%.2f MB' % (num_bytes / 1048576.0)
1096
1097   @staticmethod
1098   def _GetVersion(endpoint):
1099     """Fetches version information from a remote Chrome instance.
1100
1101     Args:
1102       endpoint: The base URL to connent to.
1103
1104     Returns:
1105       A dictionary containing Browser and Content version information:
1106       {
1107         'Browser': {
1108           'major': integer,
1109           'minor': integer,
1110           'fix': integer,
1111           'day': integer
1112         },
1113         'Content': {
1114           'name': string,
1115           'major': integer,
1116           'minor': integer
1117         }
1118       }
1119
1120     Raises:
1121       RuntimeError: When Browser version info can't be fetched or parsed.
1122     """
1123     try:
1124       f = urllib2.urlopen(endpoint + '/json/version')
1125       result = f.read();
1126       result = simplejson.loads(result)
1127     except urllib2.URLError, e:
1128       raise RuntimeError(
1129           'Error accessing Chrome instance debugging port: ' + str(e))
1130
1131     if 'Browser' not in result:
1132       raise RuntimeError('Browser version is not specified.')
1133
1134     parsed = re.search('^Chrome\/(\d+).(\d+).(\d+).(\d+)', result['Browser'])
1135     if parsed is None:
1136       raise RuntimeError('Browser-Version cannot be parsed.')
1137     try:
1138       day = int(parsed.group(3))
1139       browser_info = {
1140         'major': int(parsed.group(1)),
1141         'minor': int(parsed.group(2)),
1142         'day': day,
1143         'fix': int(parsed.group(4)),
1144       }
1145     except ValueError:
1146       raise RuntimeError('Browser-Version cannot be parsed.')
1147
1148     if 'WebKit-Version' not in result:
1149       raise RuntimeError('Content-Version is not specified.')
1150
1151     parsed = re.search('^(\d+)\.(\d+)', result['WebKit-Version'])
1152     if parsed is None:
1153       raise RuntimeError('Content-Version cannot be parsed.')
1154
1155     try:
1156       platform_info = {
1157         'name': 'Blink' if day > 1464 else 'WebKit',
1158         'major': int(parsed.group(1)),
1159         'minor': int(parsed.group(2)),
1160       }
1161     except ValueError:
1162       raise RuntimeError('WebKit-Version cannot be parsed.')
1163
1164     return {
1165       'browser': browser_info,
1166       'platform': platform_info
1167     }
1168
1169   def _IsContentVersionNotOlderThan(self, major, minor):
1170     """Compares remote Browser Content version with specified one.
1171
1172     Args:
1173       major: Major Webkit version.
1174       minor: Minor Webkit version.
1175
1176     Returns:
1177       True if remote Content version is same or newer than specified,
1178       False otherwise.
1179
1180     Raises:
1181       RuntimeError: If remote Content version hasn't been fetched yet.
1182     """
1183     if not hasattr(self, '_version'):
1184       raise RuntimeError('Browser version has not been fetched yet.')
1185     version = self._version['platform']
1186
1187     if version['major'] < major:
1188       return False
1189     elif version['major'] == major and version['minor'] < minor:
1190       return False
1191     else:
1192       return True
1193
1194   def _IsBrowserDayNumberGreaterThan(self, day_number):
1195     """Compares remote Chromium day number with specified one.
1196
1197     Args:
1198       day_number: Forth part of the chromium version.
1199
1200     Returns:
1201       True if remote Chromium day number is same or newer than specified,
1202       False otherwise.
1203
1204     Raises:
1205       RuntimeError: If remote Chromium version hasn't been fetched yet.
1206     """
1207     if not hasattr(self, '_version'):
1208       raise RuntimeError('Browser revision has not been fetched yet.')
1209     version = self._version['browser']
1210
1211     return version['day'] > day_number