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

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

Issue 8885025: Pyauto remote inspector module now provides garbage collect functionality. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Addressed review comments. Created 9 years 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 | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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 130 matching lines...) Expand 10 before | Expand all | Expand 10 after
180 method: The string method name associated with this request. 181 method: The string method name associated with this request.
181 id: A unique integer id associated with this request. 182 id: A unique integer id associated with this request.
182 params: A dictionary of input parameters associated with this request. 183 params: A dictionary of input parameters associated with this request.
183 results: A dictionary of relevant results obtained from the remote Chrome 184 results: A dictionary of relevant results obtained from the remote Chrome
184 instance that are associated with this request. 185 instance that are associated with this request.
185 is_complete: A boolean indicating whether or not this request has been sent 186 is_complete: A boolean indicating whether or not this request has been sent
186 and all relevant results for it have been obtained (i.e., this 187 and all relevant results for it have been obtained (i.e., this
187 value is True only if all results for this request are known). 188 value is True only if all results for this request are known).
188 """ 189 """
189 def __init__(self, method, message_id): 190 def __init__(self, method, message_id):
190 """Initializes a DevToolsSocket request. 191 """Initialize.
191 192
192 Args: 193 Args:
193 method: The string method name for this request. 194 method: The string method name for this request.
194 message_id: An integer id for this request, which is assumed to be unique 195 message_id: An integer id for this request, which is assumed to be unique
195 from among all requests. 196 from among all requests.
196 """ 197 """
197 self.method = method 198 self.method = method
198 self.id = message_id 199 self.id = message_id
199 self.params = {} 200 self.params = {}
200 self.results = {} 201 self.results = {}
201 self.is_complete = False 202 self.is_complete = False
202 203
203 def __repr__(self): 204 def __repr__(self):
204 json_dict = {} 205 json_dict = {}
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 """Initialize.
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._socket_buffer_lock = threading.Lock()
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
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._socket_buffer_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._socket_buffer_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._socket_buffer_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._socket_buffer_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 """Initialize.
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.
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',
402 'Profiler.getProfile', 619 'Profiler.getProfile',
403 ] 620 ]
404 621
405 def __init__( 622 def __init__(
406 self, tab_index, output_file, interval, num_snapshots, verbose, 623 self, tab_index, output_file, interval, num_snapshots, verbose,
407 show_socket_messages, interactive_mode): 624 show_socket_messages, interactive_mode):
408 """Initializes a _PerformanceSnapshotterThread object. 625 """Initialize.
409 626
410 Args: 627 Args:
411 tab_index: The integer index of the tab in the remote Chrome instance to 628 tab_index: The integer index of the tab in the remote Chrome instance to
412 use for snapshotting. 629 use for snapshotting.
413 output_file: The string name of an output file in which to write results. 630 output_file: The string name of an output file in which to write results.
414 Use None to refrain from writing results to an output file. 631 Use None to refrain from writing results to an output file.
415 interval: An integer number of seconds to wait after a snapshot is 632 interval: An integer number of seconds to wait after a snapshot is
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
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
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 self._client.close()
839 return
840
841
842 # TODO(dennisjeffrey): The "verbose" option used in this file should re-use
843 # pyauto's verbose flag.
743 class PerformanceSnapshotter(object): 844 class PerformanceSnapshotter(object):
744 """Main class for taking v8 heap snapshots. 845 """Main class for taking v8 heap snapshots.
745 846
746 Public Methods: 847 Public Methods:
747 HeapSnapshot: Begins taking heap snapshots according to the initialization 848 HeapSnapshot: Begins taking heap snapshots according to the initialization
748 parameters for the current object. 849 parameters for the current object.
850 GarbageCollect: Forces a garbage collection.
749 SetInteractiveMode: Sets the current object to take snapshots in interactive 851 SetInteractiveMode: Sets the current object to take snapshots in interactive
750 mode. Only used by the main() function in this script 852 mode. Only used by the main() function in this script
751 when the 'interactive mode' command-line flag is set. 853 when the 'interactive mode' command-line flag is set.
752 """ 854 """
753 DEFAULT_SNAPSHOT_INTERVAL = 30 855 DEFAULT_SNAPSHOT_INTERVAL = 30
754 856
755 # TODO(dennisjeffrey): Allow a user to specify a window index too (not just a 857 # TODO(dennisjeffrey): Allow a user to specify a window index too (not just a
756 # tab index), when running through PyAuto. 858 # tab index), when running through PyAuto.
757 def __init__( 859 def __init__(
758 self, tab_index=0, output_file=None, interval=DEFAULT_SNAPSHOT_INTERVAL, 860 self, tab_index=0, output_file=None, interval=DEFAULT_SNAPSHOT_INTERVAL,
759 num_snapshots=1, verbose=False, show_socket_messages=False): 861 num_snapshots=1, verbose=False, show_socket_messages=False):
760 """Initializes a PerformanceSnapshotter object. 862 """Initialize.
761 863
762 Args: 864 Args:
763 tab_index: The integer index of the tab in the remote Chrome instance to 865 tab_index: The integer index of the tab in the remote Chrome instance to
764 use for snapshotting. Defaults to 0 (the first tab). 866 use for snapshotting. Defaults to 0 (the first tab).
765 output_file: The string name of an output file in which to write results. 867 output_file: The string name of an output file in which to write results.
766 Use None to refrain from writing results to an output file. 868 Use None to refrain from writing results to an output file.
767 interval: An integer number of seconds to wait after a snapshot is 869 interval: An integer number of seconds to wait after a snapshot is
768 completed, before starting the next snapshot. 870 completed, before starting the next snapshot.
769 num_snapshots: An integer number of snapshots to take before terminating. 871 num_snapshots: An integer number of snapshots to take before terminating.
770 Use 0 to take snapshots indefinitely (you must manually 872 Use 0 to take snapshots indefinitely (you must manually
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after
809 if not snapshotter_thread.is_alive(): 911 if not snapshotter_thread.is_alive():
810 break 912 break
811 asyncore.loop(timeout=1, count=1) 913 asyncore.loop(timeout=1, count=1)
812 except KeyboardInterrupt: 914 except KeyboardInterrupt:
813 pass 915 pass
814 self._logger.debug('Waiting for snapshotter thread to die...') 916 self._logger.debug('Waiting for snapshotter thread to die...')
815 snapshotter_thread.join() 917 snapshotter_thread.join()
816 self._logger.debug('Done taking snapshots.') 918 self._logger.debug('Done taking snapshots.')
817 return snapshotter_thread.collected_heap_snapshot_data 919 return snapshotter_thread.collected_heap_snapshot_data
818 920
921 def GarbageCollect(self):
922 """Forces a garbage collection."""
923 gc_thread = _GarbageCollectThread(self._tab_index, self._verbose,
924 self._show_socket_messages)
925 gc_thread.start()
926 try:
927 while asyncore.socket_map:
928 if not gc_thread.is_alive():
929 break
930 asyncore.loop(timeout=1, count=1)
931 except KeyboardInterrupt:
932 pass
933 gc_thread.join()
934
819 def SetInteractiveMode(self): 935 def SetInteractiveMode(self):
820 """Sets the current object to take snapshots in interactive mode.""" 936 """Sets the current object to take snapshots in interactive mode."""
821 self._interactive_mode = True 937 self._interactive_mode = True
822 938
823 939
824 def main(): 940 def main():
825 """Main function to enable running this script from the command line.""" 941 """Main function to enable running this script from the command line."""
826 # Process command-line arguments. 942 # Process command-line arguments.
827 parser = optparse.OptionParser() 943 parser = optparse.OptionParser()
828 parser.add_option( 944 parser.add_option(
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after
867 983
868 if options.interactive_mode: 984 if options.interactive_mode:
869 snapshotter.SetInteractiveMode() 985 snapshotter.SetInteractiveMode()
870 986
871 snapshotter.HeapSnapshot() 987 snapshotter.HeapSnapshot()
872 return 0 988 return 0
873 989
874 990
875 if __name__ == '__main__': 991 if __name__ == '__main__':
876 sys.exit(main()) 992 sys.exit(main())
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698