blob: 12ebb663b983d73366066a5e09938d02de89b439 [file] [log] [blame]
#! /usr/bin/python
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Loads a URL on an Android device, logging all the requests made to do it
to a JSON file using DevTools.
"""
import contextlib
import httplib
import json
import logging
import optparse
import os
import sys
_SRC_DIR = os.path.abspath(os.path.join(
os.path.dirname(__file__), '..', '..', '..'))
sys.path.append(os.path.join(_SRC_DIR, 'third_party', 'catapult', 'devil'))
from devil.android import device_utils
sys.path.append(os.path.join(_SRC_DIR, 'build', 'android'))
import devil_chromium
sys.path.append(os.path.join(_SRC_DIR, 'tools', 'perf'))
from chrome_telemetry_build import chromium_config
sys.path.append(chromium_config.GetTelemetryDir())
from telemetry.internal.backends.chrome_inspector import inspector_websocket
from telemetry.internal.backends.chrome_inspector import websocket
sys.path.append(os.path.join(_SRC_DIR, 'tools', 'chrome_proxy'))
from common import inspector_network
import device_setup
class AndroidRequestsLogger(object):
"""Logs all the requests made to load a page on a device."""
def __init__(self, device):
"""If device is None, we connect to a local chrome session."""
self.device = device
self._please_stop = False
self._main_frame_id = None
self._tracing_data = []
def _PageDataReceived(self, msg):
"""Called when a Page event is received.
Records the main frame, and stops the recording once it has finished
loading.
Args:
msg: (dict) Message sent by DevTools.
"""
if 'params' not in msg:
return
params = msg['params']
method = msg.get('method', None)
if method == 'Page.frameStartedLoading' and self._main_frame_id is None:
self._main_frame_id = params['frameId']
elif (method == 'Page.frameStoppedLoading'
and params['frameId'] == self._main_frame_id):
self._please_stop = True
def _TracingDataReceived(self, msg):
self._tracing_data.append(msg)
def _LogPageLoadInternal(self, url, clear_cache):
"""Returns the collection of requests made to load a given URL.
Assumes that DevTools is available on http://localhost:DEVTOOLS_PORT.
Args:
url: URL to load.
clear_cache: Whether to clear the HTTP cache.
Returns:
[inspector_network.InspectorNetworkResponseData, ...]
"""
self._main_frame_id = None
self._please_stop = False
r = httplib.HTTPConnection(
device_setup.DEVTOOLS_HOSTNAME, device_setup.DEVTOOLS_PORT)
r.request('GET', '/json')
response = r.getresponse()
if response.status != 200:
logging.error('Cannot connect to the remote target.')
return None
json_response = json.loads(response.read())
r.close()
websocket_url = json_response[0]['webSocketDebuggerUrl']
ws = inspector_websocket.InspectorWebsocket()
ws.Connect(websocket_url)
inspector = inspector_network.InspectorNetwork(ws)
if clear_cache:
inspector.ClearCache()
ws.SyncRequest({'method': 'Page.enable'})
ws.RegisterDomain('Page', self._PageDataReceived)
inspector.StartMonitoringNetwork()
ws.SendAndIgnoreResponse({'method': 'Page.navigate',
'params': {'url': url}})
while not self._please_stop:
try:
ws.DispatchNotifications()
except websocket.WebSocketTimeoutException as e:
logging.warning('Exception: ' + str(e))
break
if not self._please_stop:
logging.warning('Finished with timeout instead of page load')
inspector.StopMonitoringNetwork()
return inspector.GetResponseData()
def _LogTracingInternal(self, url):
self._main_frame_id = None
self._please_stop = False
r = httplib.HTTPConnection('localhost', device_setup.DEVTOOLS_PORT)
r.request('GET', '/json')
response = r.getresponse()
if response.status != 200:
logging.error('Cannot connect to the remote target.')
return None
json_response = json.loads(response.read())
r.close()
websocket_url = json_response[0]['webSocketDebuggerUrl']
ws = inspector_websocket.InspectorWebsocket()
ws.Connect(websocket_url)
ws.RegisterDomain('Tracing', self._TracingDataReceived)
logging.warning('Tracing.start: ' +
str(ws.SyncRequest({'method': 'Tracing.start',
'options': 'zork'})))
ws.SendAndIgnoreResponse({'method': 'Page.navigate',
'params': {'url': url}})
while not self._please_stop:
try:
ws.DispatchNotifications()
except websocket.WebSocketTimeoutException:
break
if not self._please_stop:
logging.warning('Finished with timeout instead of page load')
return {'events': self._tracing_data,
'end': ws.SyncRequest({'method': 'Tracing.end'})}
def LogPageLoad(self, url, clear_cache, package):
"""Returns the collection of requests made to load a given URL on a device.
Args:
url: (str) URL to load on the device.
clear_cache: (bool) Whether to clear the HTTP cache.
Returns:
See _LogPageLoadInternal().
"""
return device_setup.SetUpAndExecute(
self.device, package,
lambda: self._LogPageLoadInternal(url, clear_cache))
def LogTracing(self, url):
"""Log tracing events from a load of the given URL.
TODO(mattcary): This doesn't work. It would be best to log tracing
simultaneously with network requests, but as that wasn't working the tracing
logging was broken out separately. It still doesn't work...
"""
return device_setup.SetUpAndExecute(
self.device, 'chrome', lambda: self._LogTracingInternal(url))
def _ResponseDataToJson(data):
"""Converts a list of inspector_network.InspectorNetworkResponseData to JSON.
Args:
data: as returned by _LogPageLoad()
Returns:
A JSON file with the following format:
[request1, request2, ...], and a request is:
{'status': str, 'headers': dict, 'request_headers': dict,
'timestamp': double, 'timing': dict, 'url': str,
'served_from_cache': bool, 'initiator': str})
"""
result = []
for r in data:
result.append({'status': r.status,
'headers': r.headers,
'request_headers': r.request_headers,
'timestamp': r.timestamp,
'timing': r.timing,
'url': r.url,
'served_from_cache': r.served_from_cache,
'initiator': r.initiator})
return json.dumps(result)
def _CreateOptionParser():
"""Returns the option parser for this tool."""
parser = optparse.OptionParser(description='Starts a browser on an Android '
'device, gathers the requests made to load a '
'page and dumps it to a JSON file.')
parser.add_option('--url', help='URL to load.',
default='https://www.google.com', metavar='URL')
parser.add_option('--output', help='Output file.', default='result.json')
parser.add_option('--no-clear-cache', help=('Do not clear the HTTP cache '
'before loading the URL.'),
default=True, action='store_false', dest='clear_cache')
parser.add_option('--package', help='Package info for chrome build. '
'See build/android/pylib/constants.',
default='chrome')
parser.add_option('--local', action='store_true', default=False,
help='Connect to local chrome session rather than android.')
return parser
def main():
logging.basicConfig(level=logging.WARNING)
parser = _CreateOptionParser()
options, _ = parser.parse_args()
devil_chromium.Initialize()
if options.local:
device = None
else:
devices = device_utils.DeviceUtils.HealthyDevices()
device = devices[0]
request_logger = AndroidRequestsLogger(device)
response_data = request_logger.LogPageLoad(
options.url, options.clear_cache, options.package)
json_data = _ResponseDataToJson(response_data)
with open(options.output, 'w') as f:
f.write(json_data)
if __name__ == '__main__':
main()