Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(457)

Side by Side Diff: chrome/test/pyautolib/remote_inspector_client.py

Issue 222873002: Remove pyauto tests. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src/
Patch Set: sync Created 6 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « chrome/test/pyautolib/remote_host.py ('k') | chrome/test/pyautolib/timer_queue.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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
OLDNEW
« no previous file with comments | « chrome/test/pyautolib/remote_host.py ('k') | chrome/test/pyautolib/timer_queue.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698