blob: 5f15fe2320eb4fd01b8de8e07aa957ae00474e8e [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.
import argparse
import cgi
import json
import logging
import os
import subprocess
import sys
import tempfile
import time
_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
from devil.android.sdk import intent
sys.path.append(os.path.join(_SRC_DIR, 'build', 'android'))
import devil_chromium
from pylib import constants
import content_classification_lens
import device_setup
import loading_model
import loading_trace
import trace_recorder
# TODO(mattcary): logging.info isn't that useful, as the whole (tools) world
# uses logging info; we need to introduce logging modules to get finer-grained
# output. For now we just do logging.warning.
# TODO(mattcary): probably we want this piped in through a flag.
CHROME = constants.PACKAGE_INFO['chrome']
def _LoadPage(device, url):
"""Load a page on chrome on our device.
Args:
device: an AdbWrapper for the device on which to load the page.
url: url as a string to load.
"""
load_intent = intent.Intent(
package=CHROME.package, activity=CHROME.activity, data=url)
logging.warning('Loading ' + url)
device.StartActivity(load_intent, blocking=True)
def _WriteJson(output, json_data):
"""Write JSON data in a nice way.
Args:
output: a file object
json_data: JSON data as a dict.
"""
json.dump(json_data, output, sort_keys=True, indent=2)
def _GetPrefetchHtml(graph, name=None):
"""Generate prefetch page for the resources in resource graph.
Args:
graph: a ResourceGraph.
name: optional string used in the generated page.
Returns:
HTML as a string containing all the link rel=prefetch directives necessary
for prefetching the given ResourceGraph.
"""
if name:
title = 'Prefetch for ' + cgi.escape(name)
else:
title = 'Generated prefetch page'
output = []
output.append("""<!DOCTYPE html>
<html>
<head>
<title>%s</title>
""" % title)
for info in graph.ResourceInfo():
output.append('<link rel="prefetch" href="%s">\n' % info.Url())
output.append("""</head>
<body>%s</body>
</html>
""" % title)
return '\n'.join(output)
def _LogRequests(url, clear_cache=True, local=False):
"""Log requests for a web page.
Args:
url: url to log as string.
clear_cache: optional flag to clear the cache.
local: log from local (desktop) chrome session.
Returns:
JSON dict of logged information (ie, a dict that describes JSON).
"""
device = device_setup.GetFirstDevice() if not local else None
with device_setup.DeviceConnection(device) as connection:
trace = trace_recorder.MonitorUrl(connection, url, clear_cache=clear_cache)
return trace.ToJsonDict()
def _FullFetch(url, json_output, prefetch, local, prefetch_delay_seconds):
"""Do a full fetch with optional prefetching."""
if not url.startswith('http'):
url = 'http://' + url
logging.warning('Cold fetch')
cold_data = _LogRequests(url, local=local)
assert cold_data, 'Cold fetch failed to produce data. Check your phone.'
if prefetch:
assert not local
logging.warning('Generating prefetch')
prefetch_html = _GetPrefetchHtml(
loading_model.ResourceGraph(cold_data), name=url)
tmp = tempfile.NamedTemporaryFile()
tmp.write(prefetch_html)
tmp.flush()
# We hope that the tmpfile name is unique enough for the device.
target = os.path.join('/sdcard/Download', os.path.basename(tmp.name))
device = device_setup.GetFirstDevice()
device.adb.Push(tmp.name, target)
logging.warning('Pushed prefetch %s to device at %s' % (tmp.name, target))
_LoadPage(device, 'file://' + target)
time.sleep(prefetch_delay_seconds)
logging.warning('Warm fetch')
warm_data = _LogRequests(url, clear_cache=False)
with open(json_output, 'w') as f:
_WriteJson(f, warm_data)
logging.warning('Wrote ' + json_output)
with open(json_output + '.cold', 'w') as f:
_WriteJson(f, cold_data)
logging.warning('Wrote ' + json_output + '.cold')
else:
with open(json_output, 'w') as f:
_WriteJson(f, cold_data)
logging.warning('Wrote ' + json_output)
# TODO(mattcary): it would be nice to refactor so the --noads flag gets dealt
# with here.
def _ProcessRequests(filename, ad_rules_filename='',
tracking_rules_filename=''):
with open(filename) as f:
trace = loading_trace.LoadingTrace.FromJsonDict(json.load(f))
content_lens = (
content_classification_lens.ContentClassificationLens.WithRulesFiles(
trace, ad_rules_filename, tracking_rules_filename))
return loading_model.ResourceGraph(trace, content_lens)
def InvalidCommand(cmd):
sys.exit('Invalid command "%s"\nChoices are: %s' %
(cmd, ' '.join(COMMAND_MAP.keys())))
def DoCost(arg_str):
parser = argparse.ArgumentParser(description='Tabulates cost')
parser.add_argument('request_json')
parser.add_argument('--parameter', nargs='*', default=[])
parser.add_argument('--path', action='store_true')
parser.add_argument('--noads', action='store_true')
args = parser.parse_args(arg_str)
graph = _ProcessRequests(args.request_json)
for p in args.parameter:
graph.Set(**{p: True})
path_list = []
if args.noads:
graph.Set(node_filter=graph.FilterAds)
print 'Graph cost: ' + str(graph.Cost(path_list))
if args.path:
for p in path_list:
print ' ' + p.ShortName()
def DoPng(arg_str):
parser = argparse.ArgumentParser(
description='Generates a PNG from a trace')
parser.add_argument('request_json')
parser.add_argument('png_output', nargs='?')
parser.add_argument('--eog', action='store_true')
parser.add_argument('--highlight')
parser.add_argument('--noads', action='store_true')
parser.add_argument('--ad_rules', default='')
parser.add_argument('--tracking_rules', default='')
args = parser.parse_args(arg_str)
graph = _ProcessRequests(
args.request_json, args.ad_rules, args.tracking_rules)
if args.noads:
graph.Set(node_filter=graph.FilterAds)
tmp = tempfile.NamedTemporaryFile()
graph.MakeGraphviz(
tmp,
highlight=args.highlight.split(',') if args.highlight else None)
tmp.flush()
png_output = args.png_output
if not png_output:
if args.request_json.endswith('.json'):
png_output = args.request_json[:args.request_json.rfind('.json')] + '.png'
else:
png_output = args.request_json + '.png'
subprocess.check_call(['dot', '-Tpng', tmp.name, '-o', png_output])
logging.warning('Wrote ' + png_output)
if args.eog:
subprocess.Popen(['eog', png_output])
tmp.close()
def DoCompare(arg_str):
parser = argparse.ArgumentParser(description='Compares two traces')
parser.add_argument('g1_json')
parser.add_argument('g2_json')
args = parser.parse_args(arg_str)
g1 = _ProcessRequests(args.g1_json)
g2 = _ProcessRequests(args.g2_json)
discrepancies = loading_model.ResourceGraph.CheckImageLoadConsistency(g1, g2)
if discrepancies:
print '%d discrepancies' % len(discrepancies)
print '\n'.join([str(r) for r in discrepancies])
else:
print 'Consistent!'
def DoPrefetchSetup(arg_str):
parser = argparse.ArgumentParser(description='Sets up prefetch')
parser.add_argument('request_json')
parser.add_argument('target_html')
parser.add_argument('--upload', action='store_true')
args = parser.parse_args(arg_str)
graph = _ProcessRequests(args.request_json)
with open(args.target_html, 'w') as html:
html.write(_GetPrefetchHtml(
graph, name=os.path.basename(args.request_json)))
if args.upload:
device = device_setup.GetFirstDevice()
destination = os.path.join('/sdcard/Download',
os.path.basename(args.target_html))
device.adb.Push(args.target_html, destination)
logging.warning(
'Pushed %s to device at %s' % (args.target_html, destination))
def DoLogRequests(arg_str):
parser = argparse.ArgumentParser(description='Logs requests of a load')
parser.add_argument('--url', required=True)
parser.add_argument('--output', required=True)
parser.add_argument('--prefetch', action='store_true')
parser.add_argument('--prefetch_delay_seconds', type=int, default=5)
parser.add_argument('--local', action='store_true')
args = parser.parse_args(arg_str)
_FullFetch(url=args.url,
json_output=args.output,
prefetch=args.prefetch,
prefetch_delay_seconds=args.prefetch_delay_seconds,
local=args.local)
def DoFetch(arg_str):
parser = argparse.ArgumentParser(description='Fetches SITE into DIR with '
'standard naming that can be processed by '
'./cost_to_csv.py. Both warm and cold '
'fetches are done. SITE can be a full url '
'but the filename may be strange so better '
'to just use a site (ie, domain).')
# Arguments are flags as it's easy to get the wrong order of site vs dir.
parser.add_argument('--site', required=True)
parser.add_argument('--dir', required=True)
parser.add_argument('--prefetch_delay_seconds', type=int, default=5)
args = parser.parse_args(arg_str)
if not os.path.exists(args.dir):
os.makedirs(args.dir)
_FullFetch(url=args.site,
json_output=os.path.join(args.dir, args.site + '.json'),
prefetch=True,
prefetch_delay_seconds=args.prefetch_delay_seconds,
local=False)
def DoLongPole(arg_str):
parser = argparse.ArgumentParser(description='Calculates long pole')
parser.add_argument('request_json')
parser.add_argument('--noads', action='store_true')
args = parser.parse_args(arg_str)
graph = _ProcessRequests(args.request_json)
if args.noads:
graph.Set(node_filter=graph.FilterAds)
path_list = []
cost = graph.Cost(path_list=path_list)
print '%s (%s)' % (path_list[-1], cost)
def DoNodeCost(arg_str):
parser = argparse.ArgumentParser(description='Calculates node cost')
parser.add_argument('request_json')
parser.add_argument('--noads', action='store_true')
args = parser.parse_args(arg_str)
graph = _ProcessRequests(args.request_json)
if args.noads:
graph.Set(node_filter=graph.FilterAds)
print sum((n.NodeCost() for n in graph.Nodes()))
COMMAND_MAP = {
'cost': DoCost,
'png': DoPng,
'compare': DoCompare,
'prefetch_setup': DoPrefetchSetup,
'log_requests': DoLogRequests,
'longpole': DoLongPole,
'nodecost': DoNodeCost,
'fetch': DoFetch,
}
def main():
logging.basicConfig(level=logging.WARNING)
parser = argparse.ArgumentParser(description='Analyzes loading')
parser.add_argument('command', help=' '.join(COMMAND_MAP.keys()))
parser.add_argument('rest', nargs=argparse.REMAINDER)
args = parser.parse_args()
devil_chromium.Initialize()
COMMAND_MAP.get(args.command,
lambda _: InvalidCommand(args.command))(args.rest)
if __name__ == '__main__':
main()