OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Performance snapshot utility for pyauto tests. | 6 """Performance snapshot utility for pyauto tests. |
7 | 7 |
8 Wrapper around Chrome DevTools (mimics the front-end) to collect profiling info | 8 Wrapper around Chrome DevTools (mimics the front-end) to collect profiling info |
9 associated with a Chrome tab. This script collects snapshots of the v8 | 9 associated with a Chrome tab. This script collects snapshots of the v8 |
10 (Javascript engine) heap associated with the Chrome tab. | 10 (Javascript engine) heap associated with the Chrome tab. |
(...skipping 19 matching lines...) Expand all Loading... | |
30 the summarized info for a single v8 heap snapshot. See the initialization | 30 the summarized info for a single v8 heap snapshot. See the initialization |
31 parameters for class PerformanceSnapshotter to control how snapshots are taken. | 31 parameters for class PerformanceSnapshotter to control how snapshots are taken. |
32 """ | 32 """ |
33 | 33 |
34 import asyncore | 34 import asyncore |
35 import datetime | 35 import datetime |
36 import logging | 36 import logging |
37 import optparse | 37 import optparse |
38 import simplejson | 38 import simplejson |
39 import socket | 39 import socket |
40 import sys | |
40 import threading | 41 import threading |
41 import time | 42 import time |
42 import urllib2 | 43 import urllib2 |
43 import urlparse | 44 import urlparse |
44 | 45 |
45 | 46 |
46 class _V8HeapSnapshotParser(object): | 47 class _V8HeapSnapshotParser(object): |
47 """Parses v8 heap snapshot data. | 48 """Parses v8 heap snapshot data. |
48 | 49 |
49 Public Methods: | 50 Public Methods: |
(...skipping 155 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
205 json_dict['method'] = self.method | 206 json_dict['method'] = self.method |
206 json_dict['id'] = self.id | 207 json_dict['id'] = self.id |
207 if self.params: | 208 if self.params: |
208 json_dict['params'] = self.params | 209 json_dict['params'] = self.params |
209 return simplejson.dumps(json_dict, separators=(',', ':')) | 210 return simplejson.dumps(json_dict, separators=(',', ':')) |
210 | 211 |
211 | 212 |
212 class _DevToolsSocketClient(asyncore.dispatcher): | 213 class _DevToolsSocketClient(asyncore.dispatcher): |
213 """Client that communicates with a remote Chrome instance via sockets. | 214 """Client that communicates with a remote Chrome instance via sockets. |
214 | 215 |
215 This class works in conjunction with the _PerformanceSnapshotterThread class | 216 This class works in conjunction with the _RemoteInspectorBaseThread class |
216 to communicate with a remote Chrome instance following the remote debugging | 217 to communicate with a remote Chrome instance following the remote debugging |
217 communication protocol in WebKit. This class performs the lower-level work | 218 communication protocol in WebKit. This class performs the lower-level work |
218 of socket communication. | 219 of socket communication. |
219 | 220 |
220 Public Methods: | 221 Public Methods: |
221 SendMessage: Causes a specified message to be sent to the remote Chrome | 222 SendMessage: Causes a specified message to be sent to the remote Chrome |
222 instance. | 223 instance. |
223 | 224 |
224 Public Attributes: | 225 Public Attributes: |
225 handshake_done: A boolean indicating whether or not the client has completed | 226 handshake_done: A boolean indicating whether or not the client has completed |
226 the required protocol handshake with the remote Chrome | 227 the required protocol handshake with the remote Chrome |
227 instance. | 228 instance. |
228 snapshotter: An instance of the _PerformanceSnapshotterThread class that is | 229 inspector_thread: An instance of the _RemoteInspectorBaseThread class that |
229 working together with this class to communicate with a remote | 230 is working together with this class to communicate with a |
230 Chrome instance. | 231 remote Chrome instance. |
231 """ | 232 """ |
232 def __init__(self, verbose, show_socket_messages, hostname, port, path): | 233 def __init__(self, verbose, show_socket_messages, hostname, port, path): |
233 """Initializes the DevToolsSocketClient. | 234 """Initializes the DevToolsSocketClient. |
234 | 235 |
235 Args: | 236 Args: |
236 verbose: A boolean indicating whether or not to use verbose logging. | 237 verbose: A boolean indicating whether or not to use verbose logging. |
237 show_socket_messages: A boolean indicating whether or not to show the | 238 show_socket_messages: A boolean indicating whether or not to show the |
238 socket messages sent/received when communicating | 239 socket messages sent/received when communicating |
239 with the remote Chrome instance. | 240 with the remote Chrome instance. |
240 hostname: The string hostname of the DevToolsSocket to which to connect. | 241 hostname: The string hostname of the DevToolsSocket to which to connect. |
241 port: The integer port number of the DevToolsSocket to which to connect. | 242 port: The integer port number of the DevToolsSocket to which to connect. |
242 path: The string path of the DevToolsSocket to which to connect. | 243 path: The string path of the DevToolsSocket to which to connect. |
243 """ | 244 """ |
244 asyncore.dispatcher.__init__(self) | 245 asyncore.dispatcher.__init__(self) |
245 | 246 |
246 self._logger = logging.getLogger('_DevToolsSocketClient') | 247 self._logger = logging.getLogger('_DevToolsSocketClient') |
247 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) | 248 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) |
248 | 249 |
249 self._show_socket_messages = show_socket_messages | 250 self._show_socket_messages = show_socket_messages |
250 | 251 |
251 self._read_buffer = '' | 252 self._read_buffer = '' |
252 self._write_buffer = '' | 253 self._write_buffer = '' |
253 | 254 |
255 self._lock = threading.Lock() | |
Nirnimesh
2011/12/13 21:21:59
use a better var name.
dennis_jeffrey
2011/12/14 22:38:48
Done.
| |
256 | |
254 self.handshake_done = False | 257 self.handshake_done = False |
255 self.snapshotter = None | 258 self.inspector_thread = None |
256 | 259 |
257 # Connect to the remote Chrome instance and initiate the protocol handshake. | 260 # Connect to the remote Chrome instance and initiate the protocol handshake. |
258 self.create_socket(socket.AF_INET, socket.SOCK_STREAM) | 261 self.create_socket(socket.AF_INET, socket.SOCK_STREAM) |
259 self.connect((hostname, port)) | 262 self.connect((hostname, port)) |
260 | 263 |
261 fields = [ | 264 fields = [ |
262 'Upgrade: WebSocket', | 265 'Upgrade: WebSocket', |
263 'Connection: Upgrade', | 266 'Connection: Upgrade', |
264 'Host: %s:%d' % (hostname, port), | 267 'Host: %s:%d' % (hostname, port), |
265 'Origin: http://%s:%d' % (hostname, port), | 268 'Origin: http://%s:%d' % (hostname, port), |
(...skipping 19 matching lines...) Expand all Loading... | |
285 """Causes a raw message to be sent to the remote Chrome instance. | 288 """Causes a raw message to be sent to the remote Chrome instance. |
286 | 289 |
287 Args: | 290 Args: |
288 msg: A raw string message to be sent. | 291 msg: A raw string message to be sent. |
289 """ | 292 """ |
290 self._write_buffer += msg | 293 self._write_buffer += msg |
291 self.handle_write() | 294 self.handle_write() |
292 | 295 |
293 def handle_write(self): | 296 def handle_write(self): |
294 """Called if a writable socket can be written; overridden from asyncore.""" | 297 """Called if a writable socket can be written; overridden from asyncore.""" |
298 self._lock.acquire() | |
295 if self._write_buffer: | 299 if self._write_buffer: |
296 sent = self.send(self._write_buffer) | 300 sent = self.send(self._write_buffer) |
297 if self._show_socket_messages: | 301 if self._show_socket_messages: |
298 msg_type = ['Handshake', 'Message'][self._write_buffer[0] == '\x00' and | 302 msg_type = ['Handshake', 'Message'][self._write_buffer[0] == '\x00' and |
299 self._write_buffer[-1] == '\xff'] | 303 self._write_buffer[-1] == '\xff'] |
300 msg = ('========================\n' | 304 msg = ('========================\n' |
301 'Sent %s:\n' | 305 'Sent %s:\n' |
302 '========================\n' | 306 '========================\n' |
303 '%s\n' | 307 '%s\n' |
304 '========================') % (msg_type, | 308 '========================') % (msg_type, |
305 self._write_buffer[:sent-1]) | 309 self._write_buffer[:sent-1]) |
306 print msg | 310 print msg |
307 self._write_buffer = self._write_buffer[sent:] | 311 self._write_buffer = self._write_buffer[sent:] |
312 self._lock.release() | |
308 | 313 |
309 def handle_read(self): | 314 def handle_read(self): |
310 """Called when a socket can be read; overridden from asyncore.""" | 315 """Called when a socket can be read; overridden from asyncore.""" |
316 self._lock.acquire() | |
311 if self.handshake_done: | 317 if self.handshake_done: |
312 # Process a message reply from the remote Chrome instance. | 318 # Process a message reply from the remote Chrome instance. |
313 self._read_buffer += self.recv(4096) | 319 self._read_buffer += self.recv(4096) |
314 pos = self._read_buffer.find('\xff') | 320 pos = self._read_buffer.find('\xff') |
315 while pos >= 0: | 321 while pos >= 0: |
316 pos += len('\xff') | 322 pos += len('\xff') |
317 data = self._read_buffer[:pos-len('\xff')] | 323 data = self._read_buffer[:pos-len('\xff')] |
318 pos2 = data.find('\x00') | 324 pos2 = data.find('\x00') |
319 if pos2 >= 0: | 325 if pos2 >= 0: |
320 data = data[pos2 + 1:] | 326 data = data[pos2 + 1:] |
321 self._read_buffer = self._read_buffer[pos:] | 327 self._read_buffer = self._read_buffer[pos:] |
322 if self._show_socket_messages: | 328 if self._show_socket_messages: |
323 msg = ('========================\n' | 329 msg = ('========================\n' |
324 'Received Message:\n' | 330 'Received Message:\n' |
325 '========================\n' | 331 '========================\n' |
326 '%s\n' | 332 '%s\n' |
327 '========================') % data | 333 '========================') % data |
328 print msg | 334 print msg |
329 if self.snapshotter: | 335 if self.inspector_thread: |
330 self.snapshotter.NotifyReply(data) | 336 self.inspector_thread.NotifyReply(data) |
331 pos = self._read_buffer.find('\xff') | 337 pos = self._read_buffer.find('\xff') |
332 else: | 338 else: |
333 # Process a handshake reply from the remote Chrome instance. | 339 # Process a handshake reply from the remote Chrome instance. |
334 self._read_buffer += self.recv(4096) | 340 self._read_buffer += self.recv(4096) |
335 pos = self._read_buffer.find('\r\n\r\n') | 341 pos = self._read_buffer.find('\r\n\r\n') |
336 if pos >= 0: | 342 if pos >= 0: |
337 pos += len('\r\n\r\n') | 343 pos += len('\r\n\r\n') |
338 data = self._read_buffer[:pos] | 344 data = self._read_buffer[:pos] |
339 self._read_buffer = self._read_buffer[pos:] | 345 self._read_buffer = self._read_buffer[pos:] |
340 self.handshake_done = True | 346 self.handshake_done = True |
341 if self._show_socket_messages: | 347 if self._show_socket_messages: |
342 msg = ('=========================\n' | 348 msg = ('=========================\n' |
343 'Received Handshake Reply:\n' | 349 'Received Handshake Reply:\n' |
344 '=========================\n' | 350 '=========================\n' |
345 '%s\n' | 351 '%s\n' |
346 '=========================') % data | 352 '=========================') % data |
347 print msg | 353 print msg |
354 self._lock.release() | |
348 | 355 |
349 def handle_close(self): | 356 def handle_close(self): |
350 """Called when the socket is closed; overridden from asyncore.""" | 357 """Called when the socket is closed; overridden from asyncore.""" |
351 self.close() | 358 self.close() |
352 | 359 |
353 def writable(self): | 360 def writable(self): |
354 """Determines if writes can occur for this socket; overridden from asyncore. | 361 """Determines if writes can occur for this socket; overridden from asyncore. |
355 | 362 |
356 Returns: | 363 Returns: |
357 True, if there is something to write to the socket, or | 364 True, if there is something to write to the socket, or |
358 False, otherwise. | 365 False, otherwise. |
359 """ | 366 """ |
360 return len(self._write_buffer) > 0 | 367 return len(self._write_buffer) > 0 |
361 | 368 |
362 def handle_expt(self): | 369 def handle_expt(self): |
363 """Called when out-of-band data exists; overridden from asyncore.""" | 370 """Called when out-of-band data exists; overridden from asyncore.""" |
364 self.handle_error() | 371 self.handle_error() |
365 | 372 |
366 def handle_error(self): | 373 def handle_error(self): |
367 """Called when an exception is raised; overridden from asyncore.""" | 374 """Called when an exception is raised; overridden from asyncore.""" |
368 self.close() | 375 self.close() |
369 self.snapshotter.NotifySocketClientException() | 376 self.inspector_thread.NotifySocketClientException() |
370 asyncore.dispatcher.handle_error(self) | 377 asyncore.dispatcher.handle_error(self) |
371 | 378 |
372 | 379 |
373 class _PerformanceSnapshotterThread(threading.Thread): | 380 class _RemoteInspectorBaseThread(threading.Thread): |
374 """Manages communication with a remote Chrome instance to take snapshots. | 381 """Manages communication using Chrome's remote inspector protocol. |
375 | 382 |
376 This class works in conjunction with the _DevToolsSocketClient class to | 383 This class works in conjunction with the _DevToolsSocketClient class to |
377 communicate with a remote Chrome instance following the remote debugging | 384 communicate with a remote Chrome instance following the remote inspector |
378 communication protocol in WebKit. This class performs the higher-level work | 385 communication protocol in WebKit. This class performs the higher-level work |
379 of managing request and reply messages, whereas _DevToolsSocketClient handles | 386 of managing request and reply messages, whereas _DevToolsSocketClient handles |
380 the lower-level work of socket communication. | 387 the lower-level work of socket communication. |
381 | 388 |
389 This base class should be subclassed for each different type of action that | |
390 needs to be performed using the remote inspector (e.g., take a v8 heap | |
391 snapshot, force a garbage collect): | |
392 | |
393 * Each subclass should override the run() method to customize the work done | |
394 by the thread, making sure to call self._client.close() when done. | |
395 * If overriding __init__ in a subclass, the base class __init__ must also be | |
396 invoked. | |
397 * The HandleReply() function should be overridden if special handling needs | |
398 to be performed using the reply messages received from the remote Chrome | |
399 instance. | |
400 | |
382 Public Methods: | 401 Public Methods: |
402 NotifySocketClientException: Notifies the current object that the | |
403 _DevToolsSocketClient encountered an exception. | |
404 Called by the _DevToolsSocketClient. | |
383 NotifyReply: Notifies the current object of a reply message that has been | 405 NotifyReply: Notifies the current object of a reply message that has been |
384 received from the remote Chrome instance (which would have been | 406 received from the remote Chrome instance (which would have been |
385 sent in response to an earlier request). Called by the | 407 sent in response to an earlier request). Called by the |
386 _DevToolsSocketClient. | 408 _DevToolsSocketClient. |
387 NotifySocketClientException: Notifies the current object that the | 409 HandleReply: Processes a reply message received from the remote Chrome |
388 _DevToolsSocketClient encountered an exception. | 410 instance. Should be overridden by a subclass if special |
389 Called by the _DevToolsSocketClient. | 411 result handling needs to be performed. |
390 run: Starts the thread of execution for this object. Invoked implicitly | 412 run: Starts the thread of execution for this object. Invoked implicitly |
391 by calling the start() method on this object. | 413 by calling the start() method on this object. Should be overridden |
414 by a subclass. | |
415 """ | |
416 def __init__(self, tab_index, verbose, show_socket_messages): | |
417 """Initializes a _RemoteInspectorBaseThread object. | |
Nirnimesh
2011/12/13 21:21:59
Keep it short: Initialize.
dennis_jeffrey
2011/12/14 22:38:48
Done - I made the same change where applicable els
| |
418 | |
419 Args: | |
420 tab_index: The integer index of the tab in the remote Chrome instance to | |
421 use for snapshotting. | |
422 verbose: A boolean indicating whether or not to use verbose logging. | |
Nirnimesh
2011/12/13 21:21:59
In future this should probably re-use pyauto's ver
dennis_jeffrey
2011/12/14 22:38:48
Good point - I added a TODO for that.
| |
423 show_socket_messages: A boolean indicating whether or not to show the | |
424 socket messages sent/received when communicating | |
425 with the remote Chrome instance. | |
426 """ | |
427 threading.Thread.__init__(self) | |
428 self._logger = logging.getLogger('_RemoteInspectorBaseThread') | |
429 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) | |
430 | |
431 self._killed = False | |
432 self._next_request_id = 1 | |
433 self._requests = [] | |
434 | |
435 # Create a DevToolsSocket client and wait for it to complete the remote | |
436 # debugging protocol handshake with the remote Chrome instance. | |
437 result = self._IdentifyDevToolsSocketConnectionInfo(tab_index) | |
438 self._client = _DevToolsSocketClient( | |
439 verbose, show_socket_messages, result['host'], result['port'], | |
440 result['path']) | |
441 self._client.inspector_thread = self | |
442 while asyncore.socket_map: | |
443 if self._client.handshake_done or self._killed: | |
444 break | |
445 asyncore.loop(timeout=1, count=1) | |
446 | |
447 def NotifySocketClientException(self): | |
448 """Notifies that the _DevToolsSocketClient encountered an exception.""" | |
449 self._killed = True | |
450 | |
451 def NotifyReply(self, msg): | |
452 """Notifies of a reply message received from the remote Chrome instance. | |
453 | |
454 Args: | |
455 msg: A string reply message received from the remote Chrome instance; | |
456 assumed to be a JSON message formatted according to the remote | |
457 debugging communication protocol in WebKit. | |
458 """ | |
459 reply_dict = simplejson.loads(msg) | |
460 if 'result' in reply_dict: | |
461 # This is the result message associated with a previously-sent request. | |
462 request = self._GetRequestWithId(reply_dict['id']) | |
463 if request: | |
464 request.is_complete = True | |
465 self.HandleReply(reply_dict) | |
466 | |
467 def HandleReply(self, reply_dict): | |
468 """Processes a reply message received from the remote Chrome instance. | |
469 | |
470 Override this function to specially handle reply messages from the remote | |
471 Chrome instance. | |
472 | |
473 Args: | |
474 reply_dict: A dictionary representing the reply message received from the | |
475 remote Chrome instance. | |
476 """ | |
477 pass | |
478 | |
479 def run(self): | |
480 """Start _PerformanceSnapshotterThread; overridden from threading.Thread. | |
481 | |
482 Should be overridden in a subclass. | |
483 """ | |
484 self._client.close() | |
485 | |
486 def _GetRequestWithId(self, request_id): | |
487 """Identifies the request with the specified id. | |
488 | |
489 Args: | |
490 request_id: An integer request id; should be unique for each request. | |
491 | |
492 Returns: | |
493 A request object associated with the given id if found, or | |
494 None otherwise. | |
495 """ | |
496 found_request = [x for x in self._requests if x.id == request_id] | |
497 if found_request: | |
498 return found_request[0] | |
499 return None | |
500 | |
501 def _GetFirstIncompleteRequest(self, method): | |
502 """Identifies the first incomplete request with the given method name. | |
503 | |
504 Args: | |
505 method: The string method name of the request for which to search. | |
506 | |
507 Returns: | |
508 The first request object in the request list that is not yet complete and | |
509 is also associated with the given method name, or | |
510 None if no such request object can be found. | |
511 """ | |
512 for request in self._requests: | |
513 if not request.is_complete and request.method == method: | |
514 return request | |
515 return None | |
516 | |
517 def _GetLatestRequestOfType(self, ref_req, method): | |
518 """Identifies the latest specified request before a reference request. | |
519 | |
520 This function finds the latest request with the specified method that | |
521 occurs before the given reference request. | |
522 | |
523 Returns: | |
524 The latest _DevToolsSocketRequest object with the specified method, | |
525 if found, or None otherwise. | |
526 """ | |
527 start_looking = False | |
528 for request in self._requests[::-1]: | |
529 if request.id == ref_req.id: | |
530 start_looking = True | |
531 elif start_looking: | |
532 if request.method == method: | |
533 return request | |
534 return None | |
535 | |
536 def _FillInParams(self, request): | |
537 """Fills in parameters for requests as necessary before the request is sent. | |
538 | |
539 Args: | |
540 request: The _DevToolsSocketRequest object associated with a request | |
541 message that is about to be sent. | |
542 """ | |
543 if request.method == 'Profiler.takeHeapSnapshot': | |
544 # We always want detailed v8 heap snapshot information. | |
545 request.params = {'detailed': True} | |
546 elif request.method == 'Profiler.getProfile': | |
547 # To actually request the snapshot data from a previously-taken snapshot, | |
548 # we need to specify the unique uid of the snapshot we want. | |
549 # The relevant uid should be contained in the last | |
550 # 'Profiler.takeHeapSnapshot' request object. | |
551 last_req = self._GetLatestRequestOfType(request, | |
552 'Profiler.takeHeapSnapshot') | |
553 if last_req and 'uid' in last_req.results: | |
554 request.params = {'type': 'HEAP', 'uid': last_req.results['uid']} | |
555 | |
556 @staticmethod | |
557 def _IdentifyDevToolsSocketConnectionInfo(tab_index): | |
558 """Identifies DevToolsSocket connection info from a remote Chrome instance. | |
559 | |
560 Args: | |
561 tab_index: The integer index of the tab in the remote Chrome instance to | |
562 which to connect. | |
563 | |
564 Returns: | |
565 A dictionary containing the DevToolsSocket connection info: | |
566 { | |
567 'host': string, | |
568 'port': integer, | |
569 'path': string, | |
570 } | |
571 | |
572 Raises: | |
573 RuntimeError: When DevToolsSocket connection info cannot be identified. | |
574 """ | |
575 try: | |
576 # TODO(dennisjeffrey): Do not assume port 9222. The port should be passed | |
577 # as input to this function. | |
578 f = urllib2.urlopen('http://localhost:9222/json') | |
579 result = f.read(); | |
580 result = simplejson.loads(result) | |
581 except urllib2.URLError, e: | |
582 raise RuntimeError( | |
583 'Error accessing Chrome instance debugging port: ' + str(e)) | |
584 | |
585 if tab_index >= len(result): | |
586 raise RuntimeError( | |
587 'Specified tab index %d doesn\'t exist (%d tabs found)' % | |
588 (tab_index, len(result))) | |
589 | |
590 if 'webSocketDebuggerUrl' not in result[tab_index]: | |
591 raise RuntimeError('No socket URL exists for the specified tab.') | |
592 | |
593 socket_url = result[tab_index]['webSocketDebuggerUrl'] | |
594 parsed = urlparse.urlparse(socket_url) | |
595 # On ChromeOS, the "ws://" scheme may not be recognized, leading to an | |
596 # incorrect netloc (and empty hostname and port attributes) in |parsed|. | |
597 # Change the scheme to "http://" to fix this. | |
598 if not parsed.hostname or not parsed.port: | |
599 socket_url = 'http' + socket_url[socket_url.find(':'):] | |
600 parsed = urlparse.urlparse(socket_url) | |
601 # Warning: |parsed.scheme| is incorrect after this point. | |
602 return ({'host': parsed.hostname, | |
603 'port': parsed.port, | |
604 'path': parsed.path}) | |
605 | |
606 | |
607 class _PerformanceSnapshotterThread(_RemoteInspectorBaseThread): | |
608 """Manages communication with a remote Chrome to take v8 heap snapshots. | |
392 | 609 |
393 Public Attributes: | 610 Public Attributes: |
394 collected_heap_snapshot_data: A list of dictionaries, where each dictionary | 611 collected_heap_snapshot_data: A list of dictionaries, where each dictionary |
395 contains the information for a taken snapshot. | 612 contains the information for a taken snapshot. |
396 """ | 613 """ |
397 _HEAP_SNAPSHOT_MESSAGES = [ | 614 _HEAP_SNAPSHOT_MESSAGES = [ |
398 'Page.getResourceTree', | 615 'Page.getResourceTree', |
399 'Debugger.enable', | 616 'Debugger.enable', |
400 'Profiler.clearProfiles', | 617 'Profiler.clearProfiles', |
401 'Profiler.takeHeapSnapshot', | 618 'Profiler.takeHeapSnapshot', |
(...skipping 14 matching lines...) Expand all Loading... | |
416 completed, before starting the next snapshot. | 633 completed, before starting the next snapshot. |
417 num_snapshots: An integer number of snapshots to take before terminating. | 634 num_snapshots: An integer number of snapshots to take before terminating. |
418 Use 0 to take snapshots indefinitely (you must manually | 635 Use 0 to take snapshots indefinitely (you must manually |
419 kill the script in this case). | 636 kill the script in this case). |
420 verbose: A boolean indicating whether or not to use verbose logging. | 637 verbose: A boolean indicating whether or not to use verbose logging. |
421 show_socket_messages: A boolean indicating whether or not to show the | 638 show_socket_messages: A boolean indicating whether or not to show the |
422 socket messages sent/received when communicating | 639 socket messages sent/received when communicating |
423 with the remote Chrome instance. | 640 with the remote Chrome instance. |
424 interactive_mode: A boolean indicating whether or not to take snapshots | 641 interactive_mode: A boolean indicating whether or not to take snapshots |
425 in interactive mode. | 642 in interactive mode. |
426 | |
427 Raises: | |
428 RuntimeError: When no proper connection can be made to a remote Chrome | |
429 instance. | |
430 """ | 643 """ |
431 threading.Thread.__init__(self) | 644 _RemoteInspectorBaseThread.__init__(self, tab_index, verbose, |
432 | 645 show_socket_messages) |
433 self._logger = logging.getLogger('_PerformanceSnapshotterThread') | |
434 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) | |
435 | 646 |
436 self._output_file = output_file | 647 self._output_file = output_file |
437 self._interval = interval | 648 self._interval = interval |
438 self._num_snapshots = num_snapshots | 649 self._num_snapshots = num_snapshots |
439 self._interactive_mode = interactive_mode | 650 self._interactive_mode = interactive_mode |
440 | 651 |
441 self._next_request_id = 1 | |
442 self._requests = [] | |
443 self._current_heap_snapshot = [] | 652 self._current_heap_snapshot = [] |
444 self._url = '' | 653 self._url = '' |
445 self.collected_heap_snapshot_data = [] | 654 self.collected_heap_snapshot_data = [] |
446 self.last_snapshot_start_time = 0 | 655 self.last_snapshot_start_time = 0 |
447 | 656 |
448 self._killed = False | 657 def HandleReply(self, reply_dict): |
449 | 658 """Processes a reply message received from the remote Chrome instance. |
450 # Create a DevToolsSocket client and wait for it to complete the remote | |
451 # debugging protocol handshake with the remote Chrome instance. | |
452 result = self._IdentifyDevToolsSocketConnectionInfo(tab_index) | |
453 self._client = _DevToolsSocketClient( | |
454 verbose, show_socket_messages, result['host'], result['port'], | |
455 result['path']) | |
456 self._client.snapshotter = self | |
457 while asyncore.socket_map: | |
458 if self._client.handshake_done or self._killed: | |
459 break | |
460 asyncore.loop(timeout=1, count=1) | |
461 | |
462 def NotifyReply(self, msg): | |
463 """Notifies of a reply message received from the remote Chrome instance. | |
464 | 659 |
465 Args: | 660 Args: |
466 msg: A string reply message received from the remote Chrome instance; | 661 reply_dict: A dictionary object representing the reply message received |
467 assumed to be a JSON message formatted according to the remote | 662 from the remote inspector. |
468 debugging communication protocol in WebKit. | |
469 """ | 663 """ |
470 reply_dict = simplejson.loads(msg) | |
471 if 'result' in reply_dict: | 664 if 'result' in reply_dict: |
472 # This is the result message associated with a previously-sent request. | 665 # This is the result message associated with a previously-sent request. |
473 request = self._GetRequestWithId(reply_dict['id']) | 666 request = self._GetRequestWithId(reply_dict['id']) |
474 if request: | |
475 request.is_complete = True | |
476 if 'frameTree' in reply_dict['result']: | 667 if 'frameTree' in reply_dict['result']: |
477 self._url = reply_dict['result']['frameTree']['frame']['url'] | 668 self._url = reply_dict['result']['frameTree']['frame']['url'] |
478 elif 'method' in reply_dict: | 669 elif 'method' in reply_dict: |
479 # This is an auxiliary message sent from the remote Chrome instance. | 670 # This is an auxiliary message sent from the remote Chrome instance. |
480 if reply_dict['method'] == 'Profiler.addProfileHeader': | 671 if reply_dict['method'] == 'Profiler.addProfileHeader': |
481 snapshot_req = self._GetFirstIncompleteRequest( | 672 snapshot_req = self._GetFirstIncompleteRequest( |
482 'Profiler.takeHeapSnapshot') | 673 'Profiler.takeHeapSnapshot') |
483 if snapshot_req: | 674 if snapshot_req: |
484 snapshot_req.results['uid'] = reply_dict['params']['header']['uid'] | 675 snapshot_req.results['uid'] = reply_dict['params']['header']['uid'] |
485 elif reply_dict['method'] == 'Profiler.addHeapSnapshotChunk': | 676 elif reply_dict['method'] == 'Profiler.addHeapSnapshotChunk': |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
517 f.write('\n') | 708 f.write('\n') |
518 now = datetime.datetime.now() | 709 now = datetime.datetime.now() |
519 f.write('[%s]\nSnapshot for: %s\n' % | 710 f.write('[%s]\nSnapshot for: %s\n' % |
520 (now.strftime('%Y-%B-%d %I:%M:%S %p'), self._url)) | 711 (now.strftime('%Y-%B-%d %I:%M:%S %p'), self._url)) |
521 f.write(' Total node count: %d\n' % num_nodes) | 712 f.write(' Total node count: %d\n' % num_nodes) |
522 f.write(' Total shallow size: %s\n' % total_size_str) | 713 f.write(' Total shallow size: %s\n' % total_size_str) |
523 f.close() | 714 f.close() |
524 self._logger.debug('Heap snapshot analysis complete (%s).', | 715 self._logger.debug('Heap snapshot analysis complete (%s).', |
525 total_size_str) | 716 total_size_str) |
526 | 717 |
527 def NotifySocketClientException(self): | |
528 """Notifies that the _DevToolsSocketClient encountered an exception.""" | |
529 self._killed = True | |
530 | |
531 @staticmethod | 718 @staticmethod |
532 def _ConvertBytesToHumanReadableString(num_bytes): | 719 def _ConvertBytesToHumanReadableString(num_bytes): |
533 """Converts an integer number of bytes into a human-readable string. | 720 """Converts an integer number of bytes into a human-readable string. |
534 | 721 |
535 Args: | 722 Args: |
536 num_bytes: An integer number of bytes. | 723 num_bytes: An integer number of bytes. |
537 | 724 |
538 Returns: | 725 Returns: |
539 A human-readable string representation of the given number of bytes. | 726 A human-readable string representation of the given number of bytes. |
540 """ | 727 """ |
541 if num_bytes < 1024: | 728 if num_bytes < 1024: |
542 return '%d B' % num_bytes | 729 return '%d B' % num_bytes |
543 elif num_bytes < 1048576: | 730 elif num_bytes < 1048576: |
544 return '%.2f KB' % (num_bytes / 1024.0) | 731 return '%.2f KB' % (num_bytes / 1024.0) |
545 else: | 732 else: |
546 return '%.2f MB' % (num_bytes / 1048576.0) | 733 return '%.2f MB' % (num_bytes / 1048576.0) |
547 | 734 |
548 def _IdentifyDevToolsSocketConnectionInfo(self, tab_index): | |
549 """Identifies DevToolsSocket connection info from a remote Chrome instance. | |
550 | |
551 Args: | |
552 tab_index: The integer index of the tab in the remote Chrome instance to | |
553 which to connect. | |
554 | |
555 Returns: | |
556 A dictionary containing the DevToolsSocket connection info: | |
557 { | |
558 'host': string, | |
559 'port': integer, | |
560 'path': string, | |
561 } | |
562 | |
563 Raises: | |
564 RuntimeError: When DevToolsSocket connection info cannot be identified. | |
565 """ | |
566 try: | |
567 # TODO(dennisjeffrey): Do not assume port 9222. The port should be passed | |
568 # as input to this function. | |
569 f = urllib2.urlopen('http://localhost:9222/json') | |
570 result = f.read(); | |
571 result = simplejson.loads(result) | |
572 except urllib2.URLError, e: | |
573 raise RuntimeError( | |
574 'Error accessing Chrome instance debugging port: ' + str(e)) | |
575 | |
576 if tab_index >= len(result): | |
577 raise RuntimeError( | |
578 'Specified tab index %d doesn\'t exist (%d tabs found)' % | |
579 (tab_index, len(result))) | |
580 | |
581 if 'webSocketDebuggerUrl' not in result[tab_index]: | |
582 raise RuntimeError('No socket URL exists for the specified tab.') | |
583 | |
584 socket_url = result[tab_index]['webSocketDebuggerUrl'] | |
585 parsed = urlparse.urlparse(socket_url) | |
586 # On ChromeOS, the "ws://" scheme may not be recognized, leading to an | |
587 # incorrect netloc (and empty hostname and port attributes) in |parsed|. | |
588 # Change the scheme to "http://" to fix this. | |
589 if not parsed.hostname or not parsed.port: | |
590 socket_url = 'http' + socket_url[socket_url.find(':'):] | |
591 parsed = urlparse.urlparse(socket_url) | |
592 # Warning: |parsed.scheme| is incorrect after this point. | |
593 return ({'host': parsed.hostname, | |
594 'port': parsed.port, | |
595 'path': parsed.path}) | |
596 | |
597 def _ResetRequests(self): | 735 def _ResetRequests(self): |
598 """Clears snapshot-related info in preparation for a new snapshot.""" | 736 """Clears snapshot-related info in preparation for a new snapshot.""" |
599 self._requests = [] | 737 self._requests = [] |
600 self._current_heap_snapshot = [] | 738 self._current_heap_snapshot = [] |
601 self._url = '' | 739 self._url = '' |
602 | 740 |
603 def _GetRequestWithId(self, request_id): | |
604 """Identifies the request with the specified id. | |
605 | |
606 Args: | |
607 request_id: An integer request id; should be unique for each request. | |
608 | |
609 Returns: | |
610 A request object associated with the given id if found, or | |
611 None otherwise. | |
612 """ | |
613 found_request = [x for x in self._requests if x.id == request_id] | |
614 if found_request: | |
615 return found_request[0] | |
616 return None | |
617 | |
618 def _GetFirstIncompleteRequest(self, method): | |
619 """Identifies the first incomplete request with the given method name. | |
620 | |
621 Args: | |
622 method: The string method name of the request for which to search. | |
623 | |
624 Returns: | |
625 The first request object in the request list that is not yet complete and | |
626 is also associated with the given method name, or | |
627 None if no such request object can be found. | |
628 """ | |
629 for request in self._requests: | |
630 if not request.is_complete and request.method == method: | |
631 return request | |
632 return None | |
633 | |
634 def _GetLatestRequestOfType(self, ref_req, method): | |
635 """Identifies the latest specified request before a reference request. | |
636 | |
637 This function finds the latest request with the specified method that | |
638 occurs before the given reference request. | |
639 | |
640 Returns: | |
641 The latest request object with the specified method, if found, or | |
642 None otherwise. | |
643 """ | |
644 start_looking = False | |
645 for request in self._requests[::-1]: | |
646 if request.id == ref_req.id: | |
647 start_looking = True | |
648 elif start_looking: | |
649 if request.method == method: | |
650 return request | |
651 return None | |
652 | |
653 def _FillInParams(self, request): | |
654 """Fills in parameters for requests as necessary before the request is sent. | |
655 | |
656 Args: | |
657 request: The request object associated with a request message that is | |
658 about to be sent. | |
659 """ | |
660 if request.method == 'Profiler.takeHeapSnapshot': | |
661 # We always want detailed heap snapshot information. | |
662 request.params = {'detailed': True} | |
663 elif request.method == 'Profiler.getProfile': | |
664 # To actually request the snapshot data from a previously-taken snapshot, | |
665 # we need to specify the unique uid of the snapshot we want. | |
666 # The relevant uid should be contained in the last | |
667 # 'Profiler.takeHeapSnapshot' request object. | |
668 last_req = self._GetLatestRequestOfType(request, | |
669 'Profiler.takeHeapSnapshot') | |
670 if last_req and 'uid' in last_req.results: | |
671 request.params = {'type': 'HEAP', 'uid': last_req.results['uid']} | |
672 | |
673 def _TakeHeapSnapshot(self): | 741 def _TakeHeapSnapshot(self): |
674 """Takes a heap snapshot by communicating with _DevToolsSocketClient. | 742 """Takes a heap snapshot by communicating with _DevToolsSocketClient. |
675 | 743 |
676 Returns: | 744 Returns: |
677 A boolean indicating whether the heap snapshot was taken successfully. | 745 A boolean indicating whether the heap snapshot was taken successfully. |
678 This can be False if the current thread is killed before the snapshot | 746 This can be False if the current thread is killed before the snapshot |
679 is finished being taken. | 747 is finished being taken. |
680 """ | 748 """ |
681 if self._killed: | 749 if self._killed: |
682 return False | 750 return False |
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
733 return | 801 return |
734 self._logger.debug('Completed snapshot %d of %d.', snapshot_num + 1, | 802 self._logger.debug('Completed snapshot %d of %d.', snapshot_num + 1, |
735 self._num_snapshots) | 803 self._num_snapshots) |
736 if snapshot_num + 1 < self._num_snapshots: | 804 if snapshot_num + 1 < self._num_snapshots: |
737 self._logger.debug('Waiting %d seconds...', self._interval) | 805 self._logger.debug('Waiting %d seconds...', self._interval) |
738 time.sleep(self._interval) | 806 time.sleep(self._interval) |
739 self._client.close() | 807 self._client.close() |
740 self._logger.debug('Snapshotter thread finished.') | 808 self._logger.debug('Snapshotter thread finished.') |
741 | 809 |
742 | 810 |
811 class _GarbageCollectThread(_RemoteInspectorBaseThread): | |
812 """Manages communication with a remote Chrome to force a garbage collect.""" | |
813 | |
814 _COLLECT_GARBAGE_MESSAGES = [ | |
815 'Profiler.collectGarbage', | |
816 ] | |
817 | |
818 def run(self): | |
819 """Start _GarbageCollectThread; overridden from threading.Thread.""" | |
820 if self._killed: | |
821 return | |
822 | |
823 # Prepare the request list. | |
824 for message in self._COLLECT_GARBAGE_MESSAGES: | |
825 self._requests.append( | |
826 _DevToolsSocketRequest(message, self._next_request_id)) | |
827 self._next_request_id += 1 | |
828 | |
829 # Send out each request. Wait until each request is complete before sending | |
830 # the next request. | |
831 for request in self._requests: | |
832 self._FillInParams(request) | |
833 self._client.SendMessage(str(request)) | |
834 while not request.is_complete: | |
835 if self._killed: | |
836 return | |
837 time.sleep(0.1) | |
838 return | |
839 | |
840 | |
743 class PerformanceSnapshotter(object): | 841 class PerformanceSnapshotter(object): |
744 """Main class for taking v8 heap snapshots. | 842 """Main class for taking v8 heap snapshots. |
745 | 843 |
746 Public Methods: | 844 Public Methods: |
747 HeapSnapshot: Begins taking heap snapshots according to the initialization | 845 HeapSnapshot: Begins taking heap snapshots according to the initialization |
748 parameters for the current object. | 846 parameters for the current object. |
847 GarbageCollect: Forces a garbage collection. | |
749 SetInteractiveMode: Sets the current object to take snapshots in interactive | 848 SetInteractiveMode: Sets the current object to take snapshots in interactive |
750 mode. Only used by the main() function in this script | 849 mode. Only used by the main() function in this script |
751 when the 'interactive mode' command-line flag is set. | 850 when the 'interactive mode' command-line flag is set. |
752 """ | 851 """ |
753 DEFAULT_SNAPSHOT_INTERVAL = 30 | 852 DEFAULT_SNAPSHOT_INTERVAL = 30 |
754 | 853 |
755 # TODO(dennisjeffrey): Allow a user to specify a window index too (not just a | 854 # TODO(dennisjeffrey): Allow a user to specify a window index too (not just a |
756 # tab index), when running through PyAuto. | 855 # tab index), when running through PyAuto. |
757 def __init__( | 856 def __init__( |
758 self, tab_index=0, output_file=None, interval=DEFAULT_SNAPSHOT_INTERVAL, | 857 self, tab_index=0, output_file=None, interval=DEFAULT_SNAPSHOT_INTERVAL, |
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
809 if not snapshotter_thread.is_alive(): | 908 if not snapshotter_thread.is_alive(): |
810 break | 909 break |
811 asyncore.loop(timeout=1, count=1) | 910 asyncore.loop(timeout=1, count=1) |
812 except KeyboardInterrupt: | 911 except KeyboardInterrupt: |
813 pass | 912 pass |
814 self._logger.debug('Waiting for snapshotter thread to die...') | 913 self._logger.debug('Waiting for snapshotter thread to die...') |
815 snapshotter_thread.join() | 914 snapshotter_thread.join() |
816 self._logger.debug('Done taking snapshots.') | 915 self._logger.debug('Done taking snapshots.') |
817 return snapshotter_thread.collected_heap_snapshot_data | 916 return snapshotter_thread.collected_heap_snapshot_data |
818 | 917 |
918 def GarbageCollect(self): | |
919 """Forces a garbage collection.""" | |
920 gc_thread = _GarbageCollectThread(self._tab_index, self._verbose, | |
921 self._show_socket_messages) | |
922 gc_thread.start() | |
923 try: | |
924 while asyncore.socket_map: | |
925 if not gc_thread.is_alive(): | |
926 break | |
927 asyncore.loop(timeout=1, count=1) | |
928 except KeyboardInterrupt: | |
929 pass | |
930 gc_thread.join() | |
931 | |
819 def SetInteractiveMode(self): | 932 def SetInteractiveMode(self): |
820 """Sets the current object to take snapshots in interactive mode.""" | 933 """Sets the current object to take snapshots in interactive mode.""" |
821 self._interactive_mode = True | 934 self._interactive_mode = True |
822 | 935 |
823 | 936 |
824 def main(): | 937 def main(): |
825 """Main function to enable running this script from the command line.""" | 938 """Main function to enable running this script from the command line.""" |
826 # Process command-line arguments. | 939 # Process command-line arguments. |
827 parser = optparse.OptionParser() | 940 parser = optparse.OptionParser() |
828 parser.add_option( | 941 parser.add_option( |
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
867 | 980 |
868 if options.interactive_mode: | 981 if options.interactive_mode: |
869 snapshotter.SetInteractiveMode() | 982 snapshotter.SetInteractiveMode() |
870 | 983 |
871 snapshotter.HeapSnapshot() | 984 snapshotter.HeapSnapshot() |
872 return 0 | 985 return 0 |
873 | 986 |
874 | 987 |
875 if __name__ == '__main__': | 988 if __name__ == '__main__': |
876 sys.exit(main()) | 989 sys.exit(main()) |
OLD | NEW |