| #!/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() |