Chromium Code Reviews| 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 |