blob: b5a3afe9aa7e56dcdf3f09e7a4273d061f2000c8 [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.
"""Loops Custom Tabs tests and outputs the results into a CSV file."""
import collections
import contextlib
import logging
import optparse
import os
import random
import re
import sys
import time
_SRC_PATH = os.path.abspath(os.path.join(
os.path.dirname(__file__), os.pardir, os.pardir, os.pardir, os.pardir))
sys.path.append(os.path.join(_SRC_PATH, 'third_party', 'catapult', 'devil'))
from devil.android import device_errors
from devil.android import device_utils
from devil.android import flag_changer
from devil.android.perf import cache_control
from devil.android.sdk import intent
sys.path.append(os.path.join(_SRC_PATH, 'build', 'android'))
import devil_chromium
import chrome_setup
# Local build of Chrome (not Chromium).
_CHROME_PACKAGE = 'com.google.android.apps.chrome'
_COMMAND_LINE_FILE = 'chrome-command-line'
_TEST_APP_PACKAGE_NAME = 'org.chromium.customtabs.test'
_INVALID_VALUE = -1
def RunOnce(device, url, speculated_url, parallel_url, warmup,
skip_launcher_activity, speculation_mode, delay_to_may_launch_url,
delay_to_launch_url, cold, chrome_args, reset_chrome_state):
"""Runs a test on a device once.
Args:
device: (DeviceUtils) device to run the tests on.
url: (str) URL to load. End of the redirect chain when using a
parallel request.
speculated_url: (str) Speculated URL.
parallel_url: ([str]) URL to load in parallel, typically
the start of the redirect chain.
warmup: (bool) Whether to call warmup.
skip_launcher_activity: (bool) Whether to skip the launcher activity.
speculation_mode: (str) Speculation Mode.
delay_to_may_launch_url: (int) Delay to mayLaunchUrl() in ms.
delay_to_launch_url: (int) Delay to launchUrl() in ms.
cold: (bool) Whether the page cache should be dropped.
chrome_args: ([str]) List of arguments to pass to Chrome.
reset_chrome_state: (bool) Whether to reset the Chrome local state before
the run.
Returns:
The output line (str), like this (one line only):
<warmup>,<prerender_mode>,<delay_to_may_launch_url>,<delay_to_launch>,
<intent_sent_ms>,<page_load_started_ms>,<page_load_finished_ms>,
<first_contentful_paint>
or None on error.
"""
if not device.HasRoot():
device.EnableRoot()
timeout_s = 20
logcat_timeout = int(timeout_s + delay_to_may_launch_url / 1000.
+ delay_to_launch_url / 1000.) + 3;
with flag_changer.CustomCommandLineFlags(
device, _COMMAND_LINE_FILE, chrome_args):
launch_intent = intent.Intent(
action='android.intent.action.MAIN',
package=_TEST_APP_PACKAGE_NAME,
activity='org.chromium.customtabs.test.MainActivity',
extras={'url': str(url),
'speculated_url': str(speculated_url),
'parallel_url': str (parallel_url),
'warmup': warmup,
'skip_launcher_activity': skip_launcher_activity,
'speculation_mode': str(speculation_mode),
'delay_to_may_launch_url': delay_to_may_launch_url,
'delay_to_launch_url': delay_to_launch_url,
'timeout': timeout_s})
result_line_re = re.compile(r'CUSTOMTABSBENCH.*: (.*)')
logcat_monitor = device.GetLogcatMonitor(clear=True)
logcat_monitor.Start()
device.ForceStop(_CHROME_PACKAGE)
device.ForceStop(_TEST_APP_PACKAGE_NAME)
if reset_chrome_state:
chrome_setup.ResetChromeLocalState(device, _CHROME_PACKAGE)
if cold:
cache_control.CacheControl(device).DropRamCaches()
device.StartActivity(launch_intent, blocking=True)
match = None
try:
match = logcat_monitor.WaitFor(result_line_re, timeout=logcat_timeout)
except device_errors.CommandTimeoutError as _:
logging.warning('Timeout waiting for the result line')
logcat_monitor.Stop()
logcat_monitor.Close()
return match.group(1) if match is not None else None
RESULT_FIELDS = ('warmup', 'skip_launcher_activity', 'speculation_mode',
'delay_to_may_launch_url', 'delay_to_launch_url', 'commit',
'plt', 'first_contentful_paint')
Result = collections.namedtuple('Result', RESULT_FIELDS)
def ParseResult(result_line):
"""Parses a result line, and returns it.
Args:
result_line: (str) A result line, as returned by RunOnce().
Returns:
An instance of Result.
"""
tokens = result_line.strip().split(',')
assert len(tokens) == 9
intent_sent_timestamp = int(tokens[5])
return Result(int(tokens[0]), int(tokens[1]), tokens[2], int(tokens[3]),
int(tokens[4]),
max(_INVALID_VALUE, int(tokens[6]) - intent_sent_timestamp),
max(_INVALID_VALUE, int(tokens[7]) - intent_sent_timestamp),
max(_INVALID_VALUE, int(tokens[8]) - intent_sent_timestamp))
def LoopOnDevice(device, configs, output_filename, once=False,
should_stop=None):
"""Loops the tests on a device.
Args:
device: (DeviceUtils) device to run the tests on.
configs: ([dict])
output_filename: (str) Output filename. '-' for stdout.
once: (bool) Run only once.
should_stop: (threading.Event or None) When the event is set, stop looping.
"""
to_stdout = output_filename == '-'
out = sys.stdout if to_stdout else open(output_filename, 'a')
try:
while should_stop is None or not should_stop.is_set():
config = configs[random.randint(0, len(configs) - 1)]
chrome_args = chrome_setup.CHROME_ARGS
if config['speculation_mode'] == 'no_state_prefetch':
# NoStatePrefetch is enabled through an experiment.
chrome_args.extend([
'--force-fieldtrials=trial/group',
'--force-fieldtrial-params=trial.group:mode/no_state_prefetch',
'--enable-features=NoStatePrefetch<trial'])
elif config['speculation_mode'] == 'speculative_prefetch':
# Speculative Prefetch is enabled through an experiment.
chrome_args.extend([
'--force-fieldtrials=trial/group',
'--force-fieldtrial-params=trial.group:mode/external-prefetching',
'--enable-features=SpeculativeResourcePrefetching<trial'])
result = RunOnce(device,
config['url'],
config['speculated_url'],
config['parallel_url'],
config['warmup'], config['skip_launcher_activity'],
config['speculation_mode'],
config['delay_to_may_launch_url'],
config['delay_to_launch_url'], config['cold'],
chrome_args, reset_chrome_state=True)
if result is not None:
out.write(result + '\n')
out.flush()
if once:
return
if should_stop is not None:
should_stop.wait(10.)
else:
time.sleep(10)
finally:
if not to_stdout:
out.close()
def ProcessOutput(filename):
"""Reads an output file, and returns a processed numpy array.
Args:
filename: (str) file to process.
Returns:
A numpy structured array.
"""
import numpy as np
entries = []
with open(filename, 'r') as f:
lines = f.readlines()
entries = [ParseResult(line) for line in lines]
result = np.array(entries,
dtype=[('warmup', np.int32),
('skip_launcher_activity', np.int32),
('speculation_mode', str),
('delay_to_may_launch_url', np.int32),
('delay_to_launch_url', np.int32),
('commit', np.int32), ('plt', np.int32),
('first_contentful_paint', np.int32)])
return result
def _CreateOptionParser():
parser = optparse.OptionParser(description='Loops Custom Tabs tests on a '
'device, and outputs the navigation timings '
'in a CSV file.')
parser.add_option('--device', help='Device ID')
parser.add_option('--speculated_url',
help='URL to call mayLaunchUrl() with.',)
parser.add_option('--url', help='URL to navigate to.',
default='https://www.android.com')
parser.add_option('--parallel_url', help='URL to navigate to.in parallel, '
'e.g. the start of the redirect chain.')
parser.add_option('--warmup', help='Call warmup.', default=False,
action='store_true')
parser.add_option('--skip_launcher_activity',
help='Skip ChromeLauncherActivity.', default=False,
action='store_true')
parser.add_option('--speculation_mode', default='prerender',
help='The speculation mode (prerender, '
'speculative_prefetch or no_state_prefetch).',
choices=['disabled', 'prerender', 'hidden_tab'])
parser.add_option('--delay_to_may_launch_url',
help='Delay before calling mayLaunchUrl() in ms.',
type='int', default=1000)
parser.add_option('--delay_to_launch_url',
help='Delay before calling launchUrl() in ms.',
type='int', default=-1)
parser.add_option('--cold', help='Purge the page cache before each run.',
default=False, action='store_true')
parser.add_option('--output_file', help='Output file (append). "-" for '
'stdout (this is the default)', default='-')
parser.add_option('--once', help='Run only one iteration.',
action='store_true', default=False)
return parser
def main():
parser = _CreateOptionParser()
options, _ = parser.parse_args()
devil_chromium.Initialize()
devices = device_utils.DeviceUtils.HealthyDevices()
device = devices[0]
if len(devices) != 1 and options.device is None:
logging.error('Several devices attached, must specify one with --device.')
sys.exit(0)
if options.device is not None:
matching_devices = [d for d in devices if str(d) == options.device]
if not matching_devices:
logging.error('Device not found.')
sys.exit(0)
device = matching_devices[0]
config = {
'url': options.url,
'skip_launcher_activity': options.skip_launcher_activity,
'speculated_url': options.speculated_url or options.url,
'parallel_url': options.parallel_url,
'warmup': options.warmup,
'speculation_mode': options.speculation_mode,
'delay_to_may_launch_url': options.delay_to_may_launch_url,
'delay_to_launch_url': options.delay_to_launch_url,
'cold': options.cold,
}
LoopOnDevice(device, [config], options.output_file, once=options.once)
if __name__ == '__main__':
main()