blob: c7970f5c4a678562ba80be13a93738189d44d4ed [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
from pathlib import Path
# 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'))
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
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(opts: argparse.Namespace) -> bool:
"""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 = (opts.readahead != 'cold' and opts.readahead != 'warm')
if needs_trace_file and (opts.input is None or
not os.path.exists(opts.input)):
print_utils.error_print('--input not specified!')
return False
# Install necessary trace file.
if needs_trace_file:
passed = iorapd_utils.iorapd_compiler_install_trace_file(
opts.package, opts.activity, opts.input)
if not cmd_utils.SIMULATE and not passed:
print_utils.error_print('Failed to install compiled TraceFile.pb for '
'"{}/{}"'.
format(opts.package, opts.activity))
return False
if opts.activity is not None:
return True
_, opts.activity = cmd_utils.run_shell_func(IORAP_COMMON_BASH_SCRIPT,
'get_activity_name',
[opts.package])
if not opts.activity:
print_utils.error_print('Activity name could not be found, '
'invalid package name?!')
return False
return True
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.
"""
if simulate:
return [('TotalTime', '123')]
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 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('==========================================')
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()
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}" | '
'"{DIR}/parse_metrics" '
'--package {package} '
'--activity {activity} '
'--timestamp "{timestamp}"'
.format(timeout=timeout,
DIR=DIR,
package=package,
activity=activity,
timestamp=pre_launch_timestamp))
if not output and not simulate:
return None
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':
return iorapd_utils.wait_for_iorapd_finish(package,
activity,
timeout,
debug,
logcat_timestamp)
return passed
# Don't need to do anything for warm or cold.
return True
def run_test(opts: argparse.Namespace) -> List[Tuple[str, str]]:
"""Runs one test using given options.
Returns:
A list of tuples that including metric name, metric value and anything left.
"""
print_utils.DEBUG = opts.debug
cmd_utils.SIMULATE = opts.simulate
passed = validate_options(opts)
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(opts.compiler_filter, opts.package,
opts.activity):
return None
return run(opts.readahead, opts.package, opts.activity, opts.timeout,
opts.simulate, opts.debug)
def main():
args = parse_options()
result = run_test(args)
if result is None:
return 1
print(result)
return 0
if __name__ == '__main__':
sys.exit(main())