OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 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 """Chrome remote inspector utility for pyauto tests. | 6 """Chrome remote inspector utility for pyauto tests. |
7 | 7 |
8 This script provides a python interface that acts as a front-end for Chrome's | 8 This script provides a python interface that acts as a front-end for Chrome's |
9 remote inspector module, communicating via sockets to interact with Chrome in | 9 remote inspector module, communicating via sockets to interact with Chrome in |
10 the same way that the Developer Tools does. This -- in theory -- should allow | 10 the same way that the Developer Tools does. This -- in theory -- should allow |
(...skipping 263 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
274 class _RemoteInspectorThread(threading.Thread): | 274 class _RemoteInspectorThread(threading.Thread): |
275 """Manages communication using Chrome's remote inspector protocol. | 275 """Manages communication using Chrome's remote inspector protocol. |
276 | 276 |
277 This class works in conjunction with the _DevToolsSocketClient class to | 277 This class works in conjunction with the _DevToolsSocketClient class to |
278 communicate with a remote Chrome instance following the remote inspector | 278 communicate with a remote Chrome instance following the remote inspector |
279 communication protocol in WebKit. This class performs the higher-level work | 279 communication protocol in WebKit. This class performs the higher-level work |
280 of managing request and reply messages, whereas _DevToolsSocketClient handles | 280 of managing request and reply messages, whereas _DevToolsSocketClient handles |
281 the lower-level work of socket communication. | 281 the lower-level work of socket communication. |
282 """ | 282 """ |
283 | 283 |
284 def __init__(self, url, tab_index, tab_filter, verbose, show_socket_messages): | 284 def __init__(self, url, tab_index, tab_filter, verbose, show_socket_messages, |
285 agent_name): | |
285 """Initialize. | 286 """Initialize. |
286 | 287 |
287 Args: | 288 Args: |
288 url: The base URL to connent to. | 289 url: The base URL to connent to. |
289 tab_index: The integer index of the tab in the remote Chrome instance to | 290 tab_index: The integer index of the tab in the remote Chrome instance to |
290 use for snapshotting. | 291 use for snapshotting. |
291 tab_filter: When specified, is run over tabs of the remote Chrome | 292 tab_filter: When specified, is run over tabs of the remote Chrome |
292 instances to choose which one to connect to. | 293 instances to choose which one to connect to. |
293 verbose: A boolean indicating whether or not to use verbose logging. | 294 verbose: A boolean indicating whether or not to use verbose logging. |
294 show_socket_messages: A boolean indicating whether or not to show the | 295 show_socket_messages: A boolean indicating whether or not to show the |
295 socket messages sent/received when communicating with the remote | 296 socket messages sent/received when communicating with the remote |
296 Chrome instance. | 297 Chrome instance. |
297 """ | 298 """ |
298 threading.Thread.__init__(self) | 299 threading.Thread.__init__(self) |
299 self._logger = logging.getLogger('_RemoteInspectorThread') | 300 self._logger = logging.getLogger('_RemoteInspectorThread') |
300 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) | 301 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) |
301 | 302 |
302 self._killed = False | 303 self._killed = False |
303 self._requests = [] | 304 self._requests = [] |
304 self._action_queue = [] | 305 self._action_queue = [] |
305 self._action_queue_condition = threading.Condition() | 306 self._action_queue_condition = threading.Condition() |
306 self._action_specific_callback = None # Callback only for current action. | 307 self._action_specific_callback = None # Callback only for current action. |
307 self._action_specific_callback_lock = threading.Lock() | 308 self._action_specific_callback_lock = threading.Lock() |
308 self._general_callbacks = [] # General callbacks that can be long-lived. | 309 self._general_callbacks = [] # General callbacks that can be long-lived. |
309 self._general_callbacks_lock = threading.Lock() | 310 self._general_callbacks_lock = threading.Lock() |
310 self._condition_to_wait = None | 311 self._condition_to_wait = None |
312 self._agent_name = agent_name | |
311 | 313 |
312 # Create a DevToolsSocket client and wait for it to complete the remote | 314 # Create a DevToolsSocket client and wait for it to complete the remote |
313 # debugging protocol handshake with the remote Chrome instance. | 315 # debugging protocol handshake with the remote Chrome instance. |
314 result = self._IdentifyDevToolsSocketConnectionInfo( | 316 result = self._IdentifyDevToolsSocketConnectionInfo( |
315 url, tab_index, tab_filter) | 317 url, tab_index, tab_filter) |
316 self._client = _DevToolsSocketClient( | 318 self._client = _DevToolsSocketClient( |
317 verbose, show_socket_messages, result['host'], result['port'], | 319 verbose, show_socket_messages, result['host'], result['port'], |
318 result['path']) | 320 result['path']) |
319 self._client.inspector_thread = self | 321 self._client.inspector_thread = self |
320 while asyncore.socket_map: | 322 while asyncore.socket_map: |
(...skipping 183 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
504 return request | 506 return request |
505 return None | 507 return None |
506 | 508 |
507 def _FillInParams(self, request): | 509 def _FillInParams(self, request): |
508 """Fills in parameters for requests as necessary before the request is sent. | 510 """Fills in parameters for requests as necessary before the request is sent. |
509 | 511 |
510 Args: | 512 Args: |
511 request: The _DevToolsSocketRequest object associated with a request | 513 request: The _DevToolsSocketRequest object associated with a request |
512 message that is about to be sent. | 514 message that is about to be sent. |
513 """ | 515 """ |
514 if request.method == 'Profiler.takeHeapSnapshot': | 516 if request.method == self._agent_name +'.takeHeapSnapshot': |
515 # We always want detailed v8 heap snapshot information. | 517 # We always want detailed v8 heap snapshot information. |
516 request.params = {'detailed': True} | 518 request.params = {'detailed': True} |
517 elif request.method == 'Profiler.getHeapSnapshot': | 519 elif request.method == self._agent_name + '.getHeapSnapshot': |
518 # To actually request the snapshot data from a previously-taken snapshot, | 520 # To actually request the snapshot data from a previously-taken snapshot, |
519 # we need to specify the unique uid of the snapshot we want. | 521 # we need to specify the unique uid of the snapshot we want. |
520 # The relevant uid should be contained in the last | 522 # The relevant uid should be contained in the last |
521 # 'Profiler.takeHeapSnapshot' request object. | 523 # 'Profiler.takeHeapSnapshot' request object. |
522 last_req = self._GetLatestRequestOfType(request, | 524 last_req = self._GetLatestRequestOfType(request, |
523 'Profiler.takeHeapSnapshot') | 525 self._agent_name + '.takeHeapSnapshot') |
524 if last_req and 'uid' in last_req.results: | 526 if last_req and 'uid' in last_req.results: |
525 request.params = {'uid': last_req.results['uid']} | 527 request.params = {'uid': last_req.results['uid']} |
526 elif request.method == 'Profiler.getProfile': | 528 elif request.method == self._agent_name + '.getProfile': |
527 # TODO(eustas): Remove this case after M27 is released. | 529 # TODO(eustas): Remove this case after M27 is released. |
528 last_req = self._GetLatestRequestOfType(request, | 530 last_req = self._GetLatestRequestOfType(request, |
529 'Profiler.takeHeapSnapshot') | 531 self._agent_name + '.takeHeapSnapshot') |
530 if last_req and 'uid' in last_req.results: | 532 if last_req and 'uid' in last_req.results: |
531 request.params = {'type': 'HEAP', 'uid': last_req.results['uid']} | 533 request.params = {'type': 'HEAP', 'uid': last_req.results['uid']} |
532 | 534 |
533 @staticmethod | 535 @staticmethod |
534 def _IdentifyDevToolsSocketConnectionInfo(url, tab_index, tab_filter): | 536 def _IdentifyDevToolsSocketConnectionInfo(url, tab_index, tab_filter): |
535 """Identifies DevToolsSocket connection info from a remote Chrome instance. | 537 """Identifies DevToolsSocket connection info from a remote Chrome instance. |
536 | 538 |
537 Args: | 539 Args: |
538 url: The base URL to connent to. | 540 url: The base URL to connent to. |
539 tab_index: The integer index of the tab in the remote Chrome instance to | 541 tab_index: The integer index of the tab in the remote Chrome instance to |
(...skipping 309 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
849 | 851 |
850 # Creating _RemoteInspectorThread might raise an exception. This prevents an | 852 # Creating _RemoteInspectorThread might raise an exception. This prevents an |
851 # AttributeError in the destructor. | 853 # AttributeError in the destructor. |
852 self._remote_inspector_thread = None | 854 self._remote_inspector_thread = None |
853 self._remote_inspector_driver_thread = None | 855 self._remote_inspector_driver_thread = None |
854 | 856 |
855 # TODO(dennisjeffrey): Do not assume port 9222. The port should be passed | 857 # TODO(dennisjeffrey): Do not assume port 9222. The port should be passed |
856 # as input to this function. | 858 # as input to this function. |
857 url = 'http://localhost:9222' | 859 url = 'http://localhost:9222' |
858 | 860 |
859 self._webkit_version = self._GetWebkitVersion(url) | 861 self._version = self._GetVersion(url) |
862 | |
863 # TODO(loislo): Remove this hack after M28 is released. | |
864 self._agent_name = 'Profiler' | |
865 if self._IsBrowserDayNumberMoreThan(1470): | |
866 self._agent_name = 'HeapProfiler' | |
860 | 867 |
861 # Start up a thread for long-term communication with the remote inspector. | 868 # Start up a thread for long-term communication with the remote inspector. |
862 self._remote_inspector_thread = _RemoteInspectorThread( | 869 self._remote_inspector_thread = _RemoteInspectorThread( |
863 url, tab_index, tab_filter, verbose, show_socket_messages) | 870 url, tab_index, tab_filter, verbose, show_socket_messages, |
871 self._agent_name) | |
864 self._remote_inspector_thread.start() | 872 self._remote_inspector_thread.start() |
865 # At this point, a connection has already been made to the remote inspector. | 873 # At this point, a connection has already been made to the remote inspector. |
866 | 874 |
867 # This thread calls asyncore.loop, which activates the channel service. | 875 # This thread calls asyncore.loop, which activates the channel service. |
868 self._remote_inspector_driver_thread = _RemoteInspectorDriverThread() | 876 self._remote_inspector_driver_thread = _RemoteInspectorDriverThread() |
869 self._remote_inspector_driver_thread.start() | 877 self._remote_inspector_driver_thread.start() |
870 | 878 |
871 def __del__(self): | 879 def __del__(self): |
872 """Called on destruction of this object.""" | 880 """Called on destruction of this object.""" |
873 self.Stop() | 881 self.Stop() |
(...skipping 17 matching lines...) Expand all Loading... | |
891 { | 899 { |
892 'url': string, # URL of the webpage that was snapshotted. | 900 'url': string, # URL of the webpage that was snapshotted. |
893 'raw_data': string, # The raw data as JSON string. | 901 'raw_data': string, # The raw data as JSON string. |
894 'total_v8_node_count': integer, # Total number of nodes in the v8 heap. | 902 'total_v8_node_count': integer, # Total number of nodes in the v8 heap. |
895 # Only if |include_summary| is True. | 903 # Only if |include_summary| is True. |
896 'total_heap_size': integer, # Total v8 heap size (number of bytes). | 904 'total_heap_size': integer, # Total v8 heap size (number of bytes). |
897 # Only if |include_summary| is True. | 905 # Only if |include_summary| is True. |
898 } | 906 } |
899 """ | 907 """ |
900 # TODO(eustas): Remove this hack after M27 is released. | 908 # TODO(eustas): Remove this hack after M27 is released. |
901 if self._IsWebkitVersionNotOlderThan(537, 27): | 909 if self._IsPlatformVersionNotOlderThan(537, 27): |
902 get_heap_snapshot_method = 'Profiler.getHeapSnapshot' | 910 get_heap_snapshot_method = '.getHeapSnapshot' |
903 else: | 911 else: |
904 get_heap_snapshot_method = 'Profiler.getProfile' | 912 get_heap_snapshot_method = '.getProfile' |
905 | 913 |
906 HEAP_SNAPSHOT_MESSAGES = [ | 914 HEAP_SNAPSHOT_MESSAGES = [ |
907 ('Page.getResourceTree', {}), | 915 ('Page.getResourceTree', {}), |
908 ('Debugger.enable', {}), | 916 ('Debugger.enable', {}), |
909 ('Profiler.clearProfiles', {}), | 917 (self._agent_name + '.clearProfiles', {}), |
910 ('Profiler.takeHeapSnapshot', {}), | 918 (self._agent_name + '.takeHeapSnapshot', {}), |
911 (get_heap_snapshot_method, {}), | 919 (self._agent_name + get_heap_snapshot_method, {}), |
912 ] | 920 ] |
913 | 921 |
914 self._current_heap_snapshot = [] | 922 self._current_heap_snapshot = [] |
915 self._url = '' | 923 self._url = '' |
916 self._collected_heap_snapshot_data = {} | 924 self._collected_heap_snapshot_data = {} |
917 | 925 |
918 done_condition = threading.Condition() | 926 done_condition = threading.Condition() |
919 | 927 |
920 def HandleReply(reply_dict): | 928 def HandleReply(reply_dict): |
921 """Processes a reply message received from the remote Chrome instance. | 929 """Processes a reply message received from the remote Chrome instance. |
922 | 930 |
923 Args: | 931 Args: |
924 reply_dict: A dictionary object representing the reply message received | 932 reply_dict: A dictionary object representing the reply message received |
925 from the remote inspector. | 933 from the remote inspector. |
926 """ | 934 """ |
927 if 'result' in reply_dict: | 935 if 'result' in reply_dict: |
928 # This is the result message associated with a previously-sent request. | 936 # This is the result message associated with a previously-sent request. |
929 request = self._remote_inspector_thread.GetRequestWithId( | 937 request = self._remote_inspector_thread.GetRequestWithId( |
930 reply_dict['id']) | 938 reply_dict['id']) |
931 if 'frameTree' in reply_dict['result']: | 939 if 'frameTree' in reply_dict['result']: |
932 self._url = reply_dict['result']['frameTree']['frame']['url'] | 940 self._url = reply_dict['result']['frameTree']['frame']['url'] |
933 elif 'method' in reply_dict: | 941 elif 'method' in reply_dict: |
934 # This is an auxiliary message sent from the remote Chrome instance. | 942 # This is an auxiliary message sent from the remote Chrome instance. |
935 if reply_dict['method'] == 'Profiler.addProfileHeader': | 943 if reply_dict['method'] == self._agent_name + '.addProfileHeader': |
936 snapshot_req = ( | 944 snapshot_req = ( |
937 self._remote_inspector_thread.GetFirstUnfulfilledRequest( | 945 self._remote_inspector_thread.GetFirstUnfulfilledRequest( |
938 'Profiler.takeHeapSnapshot')) | 946 self._agent_name + '.takeHeapSnapshot')) |
939 if snapshot_req: | 947 if snapshot_req: |
940 snapshot_req.results['uid'] = reply_dict['params']['header']['uid'] | 948 snapshot_req.results['uid'] = reply_dict['params']['header']['uid'] |
941 elif reply_dict['method'] == 'Profiler.addHeapSnapshotChunk': | 949 elif reply_dict['method'] == self._agent_name + '.addHeapSnapshotChunk': |
942 self._current_heap_snapshot.append(reply_dict['params']['chunk']) | 950 self._current_heap_snapshot.append(reply_dict['params']['chunk']) |
943 elif reply_dict['method'] == 'Profiler.finishHeapSnapshot': | 951 elif reply_dict['method'] == self._agent_name + '.finishHeapSnapshot': |
944 # A heap snapshot has been completed. Analyze and output the data. | 952 # A heap snapshot has been completed. Analyze and output the data. |
945 self._logger.debug('Heap snapshot taken: %s', self._url) | 953 self._logger.debug('Heap snapshot taken: %s', self._url) |
946 # TODO(dennisjeffrey): Parse the heap snapshot on-the-fly as the data | 954 # TODO(dennisjeffrey): Parse the heap snapshot on-the-fly as the data |
947 # is coming in over the wire, so we can avoid storing the entire | 955 # is coming in over the wire, so we can avoid storing the entire |
948 # snapshot string in memory. | 956 # snapshot string in memory. |
949 raw_snapshot_data = ''.join(self._current_heap_snapshot) | 957 raw_snapshot_data = ''.join(self._current_heap_snapshot) |
950 self._collected_heap_snapshot_data = { | 958 self._collected_heap_snapshot_data = { |
951 'url': self._url, | 959 'url': self._url, |
952 'raw_data': raw_snapshot_data} | 960 'raw_data': raw_snapshot_data} |
953 if include_summary: | 961 if include_summary: |
(...skipping 320 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1274 A human-readable string representation of the given number of bytes. | 1282 A human-readable string representation of the given number of bytes. |
1275 """ | 1283 """ |
1276 if num_bytes < 1024: | 1284 if num_bytes < 1024: |
1277 return '%d B' % num_bytes | 1285 return '%d B' % num_bytes |
1278 elif num_bytes < 1048576: | 1286 elif num_bytes < 1048576: |
1279 return '%.2f KB' % (num_bytes / 1024.0) | 1287 return '%.2f KB' % (num_bytes / 1024.0) |
1280 else: | 1288 else: |
1281 return '%.2f MB' % (num_bytes / 1048576.0) | 1289 return '%.2f MB' % (num_bytes / 1048576.0) |
1282 | 1290 |
1283 @staticmethod | 1291 @staticmethod |
1284 def _GetWebkitVersion(endpoint): | 1292 def _GetVersion(endpoint): |
1285 """Fetches Webkit version information from a remote Chrome instance. | 1293 """Fetches version information from a remote Chrome instance. |
1286 | 1294 |
1287 Args: | 1295 Args: |
1288 endpoint: The base URL to connent to. | 1296 endpoint: The base URL to connent to. |
1289 | 1297 |
1290 Returns: | 1298 Returns: |
1291 A dictionary containing Webkit version information: | 1299 A dictionary containing Brownser and Content version information: |
dennis_jeffrey
2013/04/10 17:21:34
Brownser --> Browser
| |
1292 { | 1300 { |
1293 'major': integer, | 1301 'Browser': { |
1294 'minor': integer, | 1302 'major': integer, |
1303 'minor': integer, | |
1304 'fix': integer, | |
1305 'day': integer | |
1306 }, | |
1307 'Content': { | |
1308 'name': string, | |
1309 'major': integer, | |
1310 'minor': integer | |
1311 } | |
1295 } | 1312 } |
1296 | 1313 |
1297 Raises: | 1314 Raises: |
1298 RuntimeError: When Webkit version info can't be fetched or parsed. | 1315 RuntimeError: When Browser version info can't be fetched or parsed. |
1299 """ | 1316 """ |
1300 try: | 1317 try: |
1301 f = urllib2.urlopen(endpoint + '/json/version') | 1318 f = urllib2.urlopen(endpoint + '/json/version') |
1302 result = f.read(); | 1319 result = f.read(); |
1303 result = simplejson.loads(result) | 1320 result = simplejson.loads(result) |
1304 except urllib2.URLError, e: | 1321 except urllib2.URLError, e: |
1305 raise RuntimeError( | 1322 raise RuntimeError( |
1306 'Error accessing Chrome instance debugging port: ' + str(e)) | 1323 'Error accessing Chrome instance debugging port: ' + str(e)) |
1307 | 1324 |
1325 if 'Browser' not in result: | |
1326 raise RuntimeError('Browser version is not specified.') | |
1327 | |
1328 parsed = re.search('^Chrome\/(\d+).(\d+).(\d+).(\d+)', result['Browser']) | |
1329 if parsed is None: | |
1330 raise RuntimeError('Browser-Version cannot be parsed.') | |
1331 try: | |
1332 day = int(parsed.group(3)) | |
1333 browser_info = { | |
1334 'major': int(parsed.group(1)), | |
1335 'minor': int(parsed.group(2)), | |
1336 'day': day, | |
1337 'fix': int(parsed.group(4)), | |
1338 } | |
1339 except ValueError: | |
1340 raise RuntimeError('Browser-Version cannot be parsed.') | |
1341 | |
1308 if 'WebKit-Version' not in result: | 1342 if 'WebKit-Version' not in result: |
1309 raise RuntimeError('WebKit-Version is not specified.') | 1343 raise RuntimeError('Content-Version is not specified.') |
1310 | 1344 |
1311 parsed = re.search('^(\d+)\.(\d+)', result['WebKit-Version']) | 1345 parsed = re.search('^(\d+)\.(\d+)', result['WebKit-Version']) |
1312 if parsed is None: | 1346 if parsed is None: |
1313 raise RuntimeError('WebKit-Version cannot be parsed.') | 1347 raise RuntimeError('Content-Version cannot be parsed.') |
1314 | 1348 |
1315 try: | 1349 try: |
1316 info = { | 1350 platform_info = { |
1351 'name': 'Blink' if day > 1464 else 'WebKit', | |
1317 'major': int(parsed.group(1)), | 1352 'major': int(parsed.group(1)), |
1318 'minor': int(parsed.group(2)), | 1353 'minor': int(parsed.group(2)), |
1319 } | 1354 } |
1320 except ValueError: | 1355 except ValueError: |
1321 raise RuntimeError('WebKit-Version cannot be parsed.') | 1356 raise RuntimeError('WebKit-Version cannot be parsed.') |
1322 | 1357 |
1323 return info | 1358 return { |
1359 'browser': browser_info, | |
1360 'platform': platform_info | |
1361 } | |
1324 | 1362 |
1325 def _IsWebkitVersionNotOlderThan(self, major, minor): | 1363 def _IsContentVersionNotOlderThan(self, major, minor): |
1326 """Compares remote Webkit version with specified one. | 1364 """Compares remote Browser Content version with specified one. |
1327 | 1365 |
1328 Args: | 1366 Args: |
1329 major: Major Webkit version. | 1367 major: Major Webkit version. |
1330 minor: Minor Webkit version. | 1368 minor: Minor Webkit version. |
1331 | 1369 |
1332 Returns: | 1370 Returns: |
1333 True if remote Webkit version is same or newer than specified, | 1371 True if remote Content version is same or newer than specified, |
1334 False otherwise. | 1372 False otherwise. |
1335 | 1373 |
1336 Raises: | 1374 Raises: |
1337 RuntimeError: If remote Webkit version hasn't been fetched yet. | 1375 RuntimeError: If remote Content version hasn't been fetched yet. |
1338 """ | 1376 """ |
1339 if not hasattr(self, '_webkit_version'): | 1377 if not hasattr(self, '_version'): |
1340 raise RuntimeError('WebKit version has not been fetched yet.') | 1378 raise RuntimeError('Browser version has not been fetched yet.') |
1341 version = self._webkit_version | 1379 version = self._version['platform'] |
1342 | 1380 |
1343 if version['major'] < major: | 1381 if version['major'] < major: |
1344 return False | 1382 return False |
1345 elif version['major'] == major and version['minor'] < minor: | 1383 elif version['major'] == major and version['minor'] < minor: |
1346 return False | 1384 return False |
1347 else: | 1385 else: |
1348 return True | 1386 return True |
1387 | |
1388 def _IsBrowserDayNumberMoreThan(self, day_number): | |
1389 """Compares remote Chromium day number with specified one. | |
1390 | |
1391 Args: | |
1392 day_number: Forth part of the chromium version. | |
1393 | |
1394 Returns: | |
1395 True if remote Chromium day number is same or newer than specified, | |
1396 False otherwise. | |
1397 | |
1398 Raises: | |
1399 RuntimeError: If remote Chromium version hasn't been fetched yet. | |
1400 """ | |
1401 if not hasattr(self, '_version'): | |
1402 raise RuntimeError('Browser revision has not been fetched yet.') | |
1403 version = self._version['browser'] | |
1404 | |
1405 return version['day'] > day_number | |
OLD | NEW |