blob: 8a9135bb3deabcd84d7a522d00ffd6194a0c4fcb [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright 2019, The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Runner of one test given a setting.
Run app and gather the measurement in a certain configuration.
Print the result to stdout.
See --help for more details.
Sample usage:
$> ./python run_app_with_prefetch.py -p com.android.settings -a
com.android.settings.Settings -r fadvise -i input
"""
import argparse
import os
import sys
import time
from typing import List, Tuple, Optional, NamedTuple
# local imports
import lib.adb_utils as adb_utils
# global variables
DIR = os.path.abspath(os.path.dirname(__file__))
IORAP_COMMON_BASH_SCRIPT = os.path.realpath(os.path.join(DIR,
'../iorap/common'))
APP_STARTUP_COMMON_BASH_SCRIPT = os.path.realpath(os.path.join(DIR,
'lib/common'))
sys.path.append(os.path.dirname(DIR))
import lib.print_utils as print_utils
import lib.cmd_utils as cmd_utils
import iorap.lib.iorapd_utils as iorapd_utils
RunCommandArgs = NamedTuple('RunCommandArgs',
[('package', str),
('readahead', str),
('activity', Optional[str]),
('compiler_filter', Optional[str]),
('timeout', Optional[int]),
('debug', bool),
('simulate', bool),
('input', Optional[str])])
def parse_options(argv: List[str] = None):
"""Parses command line arguments and return an argparse Namespace object."""
parser = argparse.ArgumentParser(
description='Run an Android application once and measure startup time.'
)
required_named = parser.add_argument_group('required named arguments')
required_named.add_argument('-p', '--package', action='store', dest='package',
help='package of the application', required=True)
# optional arguments
# use a group here to get the required arguments to appear 'above' the
# optional arguments in help.
optional_named = parser.add_argument_group('optional named arguments')
optional_named.add_argument('-a', '--activity', action='store',
dest='activity',
help='launch activity of the application')
optional_named.add_argument('-s', '--simulate', dest='simulate',
action='store_true',
help='simulate the process without executing '
'any shell commands')
optional_named.add_argument('-d', '--debug', dest='debug',
action='store_true',
help='Add extra debugging output')
optional_named.add_argument('-i', '--input', action='store', dest='input',
help='perfetto trace file protobuf',
default='TraceFile.pb')
optional_named.add_argument('-r', '--readahead', action='store',
dest='readahead',
help='which readahead mode to use',
default='cold',
choices=('warm', 'cold', 'mlock', 'fadvise'))
optional_named.add_argument('-t', '--timeout', dest='timeout', action='store',
type=int,
help='Timeout after this many seconds when '
'executing a single run.',
default=10)
optional_named.add_argument('--compiler-filter', dest='compiler_filter',
action='store',
help='Which compiler filter to use.',
default=None)
return parser.parse_args(argv)
def validate_options(args: argparse.Namespace) -> Tuple[bool, RunCommandArgs]:
"""Validates the activity and trace file if needed.
Returns:
A bool indicates whether the activity is valid and trace file exists if
necessary.
"""
needs_trace_file = (args.readahead != 'cold' and args.readahead != 'warm')
if needs_trace_file and (args.input is None or
not os.path.exists(args.input)):
print_utils.error_print('--input not specified!')
return False, args
if args.simulate:
args = args._replace(activity='act')
if not args.activity:
_, activity = cmd_utils.run_shell_func(IORAP_COMMON_BASH_SCRIPT,
'get_activity_name',
[args.package])
args = args._replace(activity=activity)
if not args.activity:
print_utils.error_print('Activity name could not be found, '
'invalid package name?!')
return False, args
# Install necessary trace file. This must be after the activity checking.
if needs_trace_file:
passed = iorapd_utils.iorapd_compiler_install_trace_file(
args.package, args.activity, args.input)
if not cmd_utils.SIMULATE and not passed:
print_utils.error_print('Failed to install compiled TraceFile.pb for '
'"{}/{}"'.
format(args.package, args.activity))
return False, args
return True, args
def set_up_adb_env():
"""Sets up adb environment."""
adb_utils.root()
adb_utils.disable_selinux()
time.sleep(1)
def configure_compiler_filter(compiler_filter: str, package: str,
activity: str) -> bool:
"""Configures compiler filter (e.g. speed).
Returns:
A bool indicates whether configure of compiler filer succeeds or not.
"""
if not compiler_filter:
print_utils.debug_print('No --compiler-filter specified, don\'t'
' need to force it.')
return True
passed, current_compiler_filter_info = \
cmd_utils.run_shell_command(
'{} --package {}'.format(os.path.join(DIR, 'query_compiler_filter.py'),
package))
if passed != 0:
return passed
# TODO: call query_compiler_filter directly as a python function instead of
# these shell calls.
current_compiler_filter, current_reason, current_isa = \
current_compiler_filter_info.split(' ')
print_utils.debug_print('Compiler Filter={} Reason={} Isa={}'.format(
current_compiler_filter, current_reason, current_isa))
# Don't trust reasons that aren't 'unknown' because that means
# we didn't manually force the compilation filter.
# (e.g. if any automatic system-triggered compilations are not unknown).
if current_reason != 'unknown' or current_compiler_filter != compiler_filter:
passed, _ = adb_utils.run_shell_command('{}/force_compiler_filter '
'--compiler-filter "{}" '
'--package "{}"'
' --activity "{}'.
format(DIR, compiler_filter,
package, activity))
else:
adb_utils.debug_print('Queried compiler-filter matched requested '
'compiler-filter, skip forcing.')
passed = False
return passed
def parse_metrics_output(input: str,
simulate: bool = False) -> List[Tuple[str, str, str]]:
"""Parses ouput of app startup to metrics and corresponding values.
It converts 'a=b\nc=d\ne=f\n...' into '[(a,b,''),(c,d,''),(e,f,'')]'
Returns:
A list of tuples that including metric name, metric value and rest info.
"""
all_metrics = []
for line in input.split('\n'):
if not line:
continue
splits = line.split('=')
if len(splits) < 2:
print_utils.error_print('Bad line "{}"'.format(line))
continue
metric_name = splits[0]
metric_value = splits[1]
rest = splits[2] if len(splits) > 2 else ''
if rest:
print_utils.error_print('Corrupt line "{}"'.format(line))
print_utils.debug_print('metric: "{metric_name}", '
'value: "{metric_value}" '.
format(metric_name=metric_name,
metric_value=metric_value))
all_metrics.append((metric_name, metric_value))
return all_metrics
def _parse_total_time(am_start_output: str) -> Optional[str]:
"""Parses the total time from 'adb shell am start pkg' output.
Returns:
the total time of app startup.
"""
for line in am_start_output.split('\n'):
if 'TotalTime:' in line:
return line[len('TotalTime:'):].strip()
return None
def blocking_parse_all_metrics(am_start_output: str, package: str,
pre_launch_timestamp: str,
timeout: int) -> str:
"""Parses the metric after app startup by reading from logcat in a blocking
manner until all metrics have been found".
Returns:
the total time and displayed time of app startup.
For example: "TotalTime=123\nDisplayedTime=121
"""
total_time = _parse_total_time(am_start_output)
displayed_time = adb_utils.blocking_wait_for_logcat_displayed_time(
pre_launch_timestamp, package, timeout)
return 'TotalTime={}\nDisplayedTime={}'.format(total_time, displayed_time)
def run(readahead: str,
package: str,
activity: str,
timeout: int,
simulate: bool,
debug: bool) -> List[Tuple[str, str]]:
"""Runs app startup test.
Returns:
A list of tuples that including metric name, metric value and rest info.
"""
print_utils.debug_print('==========================================')
print_utils.debug_print('===== START =====')
print_utils.debug_print('==========================================')
# Kill any existing process of this app
adb_utils.pkill(package)
if readahead != 'warm':
print_utils.debug_print('Drop caches for non-warm start.')
# Drop all caches to get cold starts.
adb_utils.vm_drop_cache()
if readahead != 'warm' and readahead != 'cold':
iorapd_utils.enable_iorapd_readahead()
print_utils.debug_print('Running with timeout {}'.format(timeout))
pre_launch_timestamp = adb_utils.logcat_save_timestamp()
passed, output = cmd_utils.run_shell_command('timeout {timeout} '
'"{DIR}/launch_application" '
'"{package}" '
'"{activity}"'
.format(timeout=timeout,
DIR=DIR,
package=package,
activity=activity))
if not passed and not simulate:
return None
if simulate:
results = [('TotalTime', '123')]
else:
output = blocking_parse_all_metrics(output,
package,
pre_launch_timestamp,
timeout)
results = parse_metrics_output(output, simulate)
passed = perform_post_launch_cleanup(
readahead, package, activity, timeout, debug, pre_launch_timestamp)
if not passed and not simulate:
print_utils.error_print('Cannot perform post launch cleanup!')
return None
adb_utils.pkill(package)
return results
def perform_post_launch_cleanup(readahead: str,
package: str,
activity: str,
timeout: int,
debug: bool,
logcat_timestamp: str) -> bool:
"""Performs cleanup at the end of each loop iteration.
Returns:
A bool indicates whether the cleanup succeeds or not.
"""
if readahead != 'warm' and readahead != 'cold':
passed = iorapd_utils.wait_for_iorapd_finish(package,
activity,
timeout,
debug,
logcat_timestamp)
if not passed:
return passed
return iorapd_utils.disable_iorapd_readahead()
# Don't need to do anything for warm or cold.
return True
def run_test(args: RunCommandArgs) -> List[Tuple[str, str]]:
"""Runs one test using given options.
Returns:
A list of tuples that including metric name, metric value.
"""
print_utils.DEBUG = args.debug
cmd_utils.SIMULATE = args.simulate
passed, args = validate_options(args)
if not passed:
return None
set_up_adb_env()
# Ensure the APK is currently compiled with whatever we passed in
# via --compiler-filter.
# No-op if this option was not passed in.
if not configure_compiler_filter(args.compiler_filter, args.package,
args.activity):
return None
return run(args.readahead, args.package, args.activity, args.timeout,
args.simulate, args.debug)
def get_args_from_opts(opts: argparse.Namespace) -> RunCommandArgs:
kwargs = {}
for field in RunCommandArgs._fields:
kwargs[field] = getattr(opts, field)
return RunCommandArgs(**kwargs)
def main():
opts = parse_options()
args = get_args_from_opts(opts)
result = run_test(args)
if result is None:
return 1
print(result)
return 0
if __name__ == '__main__':
sys.exit(main())