diff options
author | Yan Wang <yawanng@google.com> | 2019-07-31 01:55:33 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2019-07-31 01:55:33 +0000 |
commit | 313328dcde5c40fa1a2af413bb66f4a03ce3068c (patch) | |
tree | 3477d28fc47e75f0880d4f97f8c919625d44d3a2 /startop | |
parent | 875161c6baa730e554d5f8db7ea73da71e236669 (diff) | |
parent | 06f54882c06da4b3090f2edfe321c07b1a7ec2a6 (diff) |
Merge "startop: Refactor app running."
Diffstat (limited to 'startop')
-rwxr-xr-x | startop/scripts/app_startup/app_startup_runner.py | 21 | ||||
-rw-r--r-- | startop/scripts/app_startup/lib/app_runner.py | 265 | ||||
-rw-r--r-- | startop/scripts/app_startup/lib/app_runner_test.py | 104 | ||||
-rw-r--r-- | startop/scripts/app_startup/run_app_with_prefetch.py | 407 | ||||
-rw-r--r-- | startop/scripts/app_startup/run_app_with_prefetch_test.py | 201 |
5 files changed, 616 insertions, 382 deletions
diff --git a/startop/scripts/app_startup/app_startup_runner.py b/startop/scripts/app_startup/app_startup_runner.py index 7cba7805903d..7b3bf3387452 100755 --- a/startop/scripts/app_startup/app_startup_runner.py +++ b/startop/scripts/app_startup/app_startup_runner.py @@ -33,12 +33,12 @@ import os import sys import tempfile from typing import Any, Callable, Iterable, List, NamedTuple, TextIO, Tuple, \ - TypeVar, Union + TypeVar, Union, Optional # local import DIR = os.path.abspath(os.path.dirname(__file__)) sys.path.append(os.path.dirname(DIR)) -import app_startup.run_app_with_prefetch as run_app_with_prefetch +from app_startup.run_app_with_prefetch import PrefetchAppRunner import app_startup.lib.args_utils as args_utils from app_startup.lib.data_frame import DataFrame import lib.cmd_utils as cmd_utils @@ -62,6 +62,16 @@ _COLLECTOR_TIMEOUT_MULTIPLIER = 10 # take the regular --timeout and multiply _UNLOCK_SCREEN_SCRIPT = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'unlock_screen') +RunCommandArgs = NamedTuple('RunCommandArgs', + [('package', str), + ('readahead', str), + ('activity', Optional[str]), + ('compiler_filter', Optional[str]), + ('timeout', Optional[int]), + ('debug', bool), + ('simulate', bool), + ('input', Optional[str])]) + # This must be the only mutable global variable. All other global variables are constants to avoid magic literals. _debug = False # See -d/--debug flag. _DEBUG_FORCE = None # Ignore -d/--debug if this is not none. @@ -207,8 +217,7 @@ def parse_run_script_csv_file(csv_file: TextIO) -> DataFrame: return DataFrame(d) def execute_run_combos( - grouped_run_combos: Iterable[Tuple[CollectorPackageInfo, Iterable[ - run_app_with_prefetch.RunCommandArgs]]], + grouped_run_combos: Iterable[Tuple[CollectorPackageInfo, Iterable[RunCommandArgs]]], simulate: bool, inodes_path: str, timeout: int): @@ -229,7 +238,7 @@ def execute_run_combos( combos = combos._replace(input=collector_tmp_output_file.name) print_utils.debug_print(combos) - output = run_app_with_prefetch.run_test(combos) + output = PrefetchAppRunner(**combos._asdict()).run() yield DataFrame(dict((x, [y]) for x, y in output)) if output else None @@ -307,7 +316,7 @@ def main(): output_file = opts.output and open(opts.output, 'w') or sys.stdout combos = lambda: args_utils.generate_run_combinations( - run_app_with_prefetch.RunCommandArgs, + RunCommandArgs, coerce_to_list(vars(opts)), opts.loop_count) print_utils.debug_print_gen("run combinations: ", combos()) diff --git a/startop/scripts/app_startup/lib/app_runner.py b/startop/scripts/app_startup/lib/app_runner.py new file mode 100644 index 000000000000..a8afd6a22e38 --- /dev/null +++ b/startop/scripts/app_startup/lib/app_runner.py @@ -0,0 +1,265 @@ +# 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. + +"""Class to run an app.""" +import os +import sys +from typing import Optional, List, Tuple + +# local import +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname( + os.path.abspath(__file__))))) + +import app_startup.lib.adb_utils as adb_utils +import lib.cmd_utils as cmd_utils +import lib.print_utils as print_utils + +class AppRunnerListener(object): + """Interface for lisenter of AppRunner. """ + + def preprocess(self) -> None: + """Preprocess callback to initialized before the app is running. """ + pass + + def postprocess(self, pre_launch_timestamp: str) -> None: + """Postprocess callback to cleanup after the app is running. + + param: + 'pre_launch_timestamp': indicates the timestamp when the app is + launching.. """ + pass + + def metrics_selector(self, am_start_output: str, + pre_launch_timestamp: str) -> None: + """A metrics selection callback that waits for the desired metrics to + show up in logcat. + params: + 'am_start_output': indicates the output of app startup. + 'pre_launch_timestamp': indicates the timestamp when the app is + launching. + returns: + a string in the format of "<metric>=<value>\n<metric>=<value>\n..." + for further parsing. For example "TotalTime=123\nDisplayedTime=121". + """ + pass + +class AppRunner(object): + """ Class to run an app. """ + # static variables + DIR = os.path.abspath(os.path.dirname(__file__)) + APP_STARTUP_DIR = os.path.dirname(DIR) + IORAP_COMMON_BASH_SCRIPT = os.path.realpath(os.path.join(DIR, + '../../iorap/common')) + DEFAULT_TIMEOUT = 30 + + def __init__(self, + package: str, + activity: Optional[str], + compiler_filter: Optional[str], + timeout: Optional[int], + simulate: bool): + self.package = package + self.simulate = simulate + + # If the argument activity is None, try to set it. + self.activity = activity + if self.simulate: + self.activity = 'act' + if self.activity is None: + self.activity = AppRunner.get_activity(self.package) + + self.compiler_filter = compiler_filter + self.timeout = timeout if timeout else AppRunner.DEFAULT_TIMEOUT + + self.listeners = [] + + def add_callbacks(self, listener: AppRunnerListener): + self.listeners.append(listener) + + def remove_callbacks(self, listener: AppRunnerListener): + self.listeners.remove(listener) + + @staticmethod + def get_activity(package: str) -> str: + """ Tries to set the activity based on the package. """ + passed, activity = cmd_utils.run_shell_func( + AppRunner.IORAP_COMMON_BASH_SCRIPT, + 'get_activity_name', + [package]) + + if not passed or not activity: + raise ValueError( + 'Activity name could not be found, invalid package name?!') + + return activity + + def configure_compiler_filter(self) -> bool: + """Configures compiler filter (e.g. speed). + + Returns: + A bool indicates whether configure of compiler filer succeeds or not. + """ + if not self.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(AppRunner.APP_STARTUP_DIR, + 'query_compiler_filter.py'), + self.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 != self.compiler_filter: + passed, _ = adb_utils.run_shell_command('{}/force_compiler_filter ' + '--compiler-filter "{}" ' + '--package "{}"' + ' --activity "{}'. + format(AppRunner.APP_STARTUP_DIR, + self.compiler_filter, + self.package, + self.activity)) + else: + adb_utils.debug_print('Queried compiler-filter matched requested ' + 'compiler-filter, skip forcing.') + passed = False + return passed + + def run(self) -> Optional[List[Tuple[str]]]: + """Runs an app. + + Returns: + A list of (metric, value) tuples. + """ + print_utils.debug_print('==========================================') + print_utils.debug_print('===== START =====') + print_utils.debug_print('==========================================') + # Run the preprocess. + for listener in self.listeners: + listener.preprocess() + + # 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 self.configure_compiler_filter(): + print_utils.error_print('Compiler filter configuration failed!') + return None + + pre_launch_timestamp = adb_utils.logcat_save_timestamp() + # Launch the app. + results = self.launch_app(pre_launch_timestamp) + + # Run the postprocess. + for listener in self.listeners: + listener.postprocess(pre_launch_timestamp) + + return results + + def launch_app(self, pre_launch_timestamp: str) -> Optional[List[Tuple[str]]]: + """ Launches the app. + + Returns: + A list of (metric, value) tuples. + """ + print_utils.debug_print('Running with timeout {}'.format(self.timeout)) + + passed, am_start_output = cmd_utils.run_shell_command('timeout {timeout} ' + '"{DIR}/launch_application" ' + '"{package}" ' + '"{activity}"'. + format(timeout=self.timeout, + DIR=AppRunner.APP_STARTUP_DIR, + package=self.package, + activity=self.activity)) + if not passed and not self.simulate: + return None + + return self.wait_for_app_finish(pre_launch_timestamp, am_start_output) + + def wait_for_app_finish(self, + pre_launch_timestamp: str, + am_start_output: str) -> Optional[List[Tuple[str]]]: + """ Wait for app finish and all metrics are shown in logcat. + + Returns: + A list of (metric, value) tuples. + """ + if self.simulate: + return [('TotalTime', '123')] + + ret = [] + for listener in self.listeners: + output = listener.metrics_selector(am_start_output, + pre_launch_timestamp) + ret = ret + AppRunner.parse_metrics_output(output) + + return ret + + @staticmethod + def parse_metrics_output(input: str) -> List[ + Tuple[str, str, str]]: + """Parses output 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 + + @staticmethod + 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 + diff --git a/startop/scripts/app_startup/lib/app_runner_test.py b/startop/scripts/app_startup/lib/app_runner_test.py new file mode 100644 index 000000000000..33d233b03aab --- /dev/null +++ b/startop/scripts/app_startup/lib/app_runner_test.py @@ -0,0 +1,104 @@ +# 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. +# + +"""Unit tests for the AppRunner.""" +import os +import sys +from pathlib import Path + +from app_runner import AppRunner, AppRunnerListener +from mock import Mock, call, patch + +# The path is "frameworks/base/startop/scripts/" +sys.path.append(Path(os.path.realpath(__file__)).parents[2]) +import lib.cmd_utils as cmd_utils + +class AppRunnerTestListener(AppRunnerListener): + def preprocess(self) -> None: + cmd_utils.run_shell_command('pre'), + + def postprocess(self, pre_launch_timestamp: str) -> None: + cmd_utils.run_shell_command('post'), + + def metrics_selector(self, am_start_output: str, + pre_launch_timestamp: str) -> None: + return 'TotalTime=123\n' + +RUNNER = AppRunner(package='music', + activity='MainActivity', + compiler_filter='speed', + timeout=None, + simulate=False) + + + +def test_configure_compiler_filter(): + with patch('lib.cmd_utils.run_shell_command', + new_callable=Mock) as mock_run_shell_command: + mock_run_shell_command.return_value = (True, 'speed arm64 kUpToDate') + + RUNNER.configure_compiler_filter() + + calls = [call(os.path.realpath( + os.path.join(RUNNER.DIR, + '../query_compiler_filter.py')) + ' --package music')] + mock_run_shell_command.assert_has_calls(calls) + +def test_parse_metrics_output(): + input = 'a1=b1\nc1=d1\ne1=f1' + ret = RUNNER.parse_metrics_output(input) + + assert ret == [('a1', 'b1'), ('c1', 'd1'), ('e1', 'f1')] + +def _mocked_run_shell_command(*args, **kwargs): + if args[0] == 'adb shell "date -u +\'%Y-%m-%d %H:%M:%S.%N\'"': + return (True, "2019-07-02 23:20:06.972674825") + elif args[0] == 'adb shell ps | grep "music" | awk \'{print $2;}\'': + return (True, '9999') + else: + return (True, 'a1=b1\nc1=d1=d2\ne1=f1') + +@patch('app_startup.lib.adb_utils.blocking_wait_for_logcat_displayed_time') +@patch('lib.cmd_utils.run_shell_command') +def test_run(mock_run_shell_command, + mock_blocking_wait_for_logcat_displayed_time): + mock_run_shell_command.side_effect = _mocked_run_shell_command + mock_blocking_wait_for_logcat_displayed_time.return_value = 123 + + test_listener = AppRunnerTestListener() + RUNNER.add_callbacks(test_listener) + + result = RUNNER.run() + + RUNNER.remove_callbacks(test_listener) + + calls = [call('pre'), + call(os.path.realpath( + os.path.join(RUNNER.DIR, + '../query_compiler_filter.py')) + + ' --package music'), + call('adb shell "date -u +\'%Y-%m-%d %H:%M:%S.%N\'"'), + call( + 'timeout {timeout} "{DIR}/launch_application" "{package}" "{activity}"' + .format(timeout=30, + DIR=os.path.realpath(os.path.dirname(RUNNER.DIR)), + package='music', + activity='MainActivity', + timestamp='2019-07-02 23:20:06.972674825')), + call('post') + ] + mock_run_shell_command.assert_has_calls(calls) + assert result == [('TotalTime', '123')] + assert len(RUNNER.listeners) == 0
\ No newline at end of file diff --git a/startop/scripts/app_startup/run_app_with_prefetch.py b/startop/scripts/app_startup/run_app_with_prefetch.py index 464742d16d13..2f1eff2c41f6 100644 --- a/startop/scripts/app_startup/run_app_with_prefetch.py +++ b/startop/scripts/app_startup/run_app_with_prefetch.py @@ -30,32 +30,146 @@ import argparse import os import sys import time -from typing import List, Tuple, Optional, NamedTuple +from typing import List, Tuple, Optional # local imports import lib.adb_utils as adb_utils +from lib.app_runner import AppRunner, AppRunnerListener # 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])]) +class PrefetchAppRunner(AppRunnerListener): + def __init__(self, + package: str, + activity: Optional[str], + readahead: str, + compiler_filter: Optional[str], + timeout: Optional[int], + simulate: bool, + debug: bool, + input:Optional[str], + **kwargs): + self.app_runner = AppRunner(package, + activity, + compiler_filter, + timeout, + simulate) + self.app_runner.add_callbacks(self) + + self.simulate = simulate + self.readahead = readahead + self.debug = debug + self.input = input + print_utils.DEBUG = self.debug + cmd_utils.SIMULATE = self.simulate + + + def run(self) -> Optional[List[Tuple[str]]]: + """Runs an app. + + Returns: + A list of (metric, value) tuples. + """ + return self.app_runner.run() + + def preprocess(self): + passed = self.validate_options() + if not passed: + return + + # Sets up adb environment. + adb_utils.root() + adb_utils.disable_selinux() + time.sleep(1) + + # Kill any existing process of this app + adb_utils.pkill(self.app_runner.package) + + if self.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 self.readahead != 'warm' and self.readahead != 'cold': + iorapd_utils.enable_iorapd_readahead() + + def postprocess(self, pre_launch_timestamp: str): + passed = self._perform_post_launch_cleanup(pre_launch_timestamp) + if not passed and not self.app_runner.simulate: + print_utils.error_print('Cannot perform post launch cleanup!') + return None + + # Kill any existing process of this app + adb_utils.pkill(self.app_runner.package) + + def _perform_post_launch_cleanup(self, logcat_timestamp: str) -> bool: + """Performs cleanup at the end of each loop iteration. + + Returns: + A bool indicates whether the cleanup succeeds or not. + """ + if self.readahead != 'warm' and self.readahead != 'cold': + passed = iorapd_utils.wait_for_iorapd_finish(self.app_runner.package, + self.app_runner.activity, + self.app_runner.timeout, + self.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 metrics_selector(self, am_start_output: str, + pre_launch_timestamp: str) -> 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 = AppRunner.parse_total_time(am_start_output) + displayed_time = adb_utils.blocking_wait_for_logcat_displayed_time( + pre_launch_timestamp, self.app_runner.package, self.app_runner.timeout) + + return 'TotalTime={}\nDisplayedTime={}'.format(total_time, displayed_time) + + def validate_options(self) -> bool: + """Validates the activity and trace file if needed. + + Returns: + A bool indicates whether the activity is valid. + """ + needs_trace_file = self.readahead != 'cold' and self.readahead != 'warm' + if needs_trace_file and (self.input is None or + not os.path.exists(self.input)): + print_utils.error_print('--input not specified!') + return False + + # Install necessary trace file. This must be after the activity checking. + if needs_trace_file: + passed = iorapd_utils.iorapd_compiler_install_trace_file( + self.app_runner.package, self.app_runner.activity, self.input) + if not cmd_utils.SIMULATE and not passed: + print_utils.error_print('Failed to install compiled TraceFile.pb for ' + '"{}/{}"'. + format(self.app_runner.package, + self.app_runner.activity)) + return False + + return True + + def parse_options(argv: List[str] = None): """Parses command line arguments and return an argparse Namespace object.""" @@ -101,271 +215,10 @@ def parse_options(argv: List[str] = None): return parser.parse_args(argv) -def validate_options(args: RunCommandArgs) -> 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) + runner = PrefetchAppRunner(**vars(opts)) + result = runner.run() if result is None: return 1 diff --git a/startop/scripts/app_startup/run_app_with_prefetch_test.py b/startop/scripts/app_startup/run_app_with_prefetch_test.py index 8536ce5f917e..8a588e4463e9 100644 --- a/startop/scripts/app_startup/run_app_with_prefetch_test.py +++ b/startop/scripts/app_startup/run_app_with_prefetch_test.py @@ -33,17 +33,18 @@ import io import os import shlex import sys +import tempfile # global imports from contextlib import contextmanager # pip imports import pytest # local imports -import run_app_with_prefetch as run -from mock import Mock, call, patch +import run_app_with_prefetch as runner +from mock import call, patch, Mock sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - +from app_startup.lib.app_runner import AppRunner # # Argument Parsing Helpers # @@ -85,7 +86,7 @@ def parse_args(args): :return: dictionary of parsed key/values """ # "-a b -c d" => ['-a', 'b', '-c', 'd'] - return vars(run.parse_options(shlex.split(args))) + return vars(runner.parse_options(shlex.split(args))) def default_dict_for_parsed_args(**kwargs): """Combines it with all of the "optional" parameters' default values.""" @@ -154,130 +155,132 @@ def test_argparse(): def test_main(): args = '--package com.fake.package --activity act -s' - opts = run.parse_options(shlex.split(args)) - - args = run.get_args_from_opts(opts) - result = run.run_test(args) + opts = runner.parse_options(shlex.split(args)) + result = runner.PrefetchAppRunner(**vars(opts)).run() assert result == [('TotalTime', '123')] -def test_set_up_adb_env(): +def _mocked_run_shell_command(*args, **kwargs): + if args[0] == 'adb shell ps | grep "music" | awk \'{print $2;}\'': + return (True, '9999') + else: + return (True, '') + +def test_preprocess_no_cache_drop(): with patch('lib.cmd_utils.run_shell_command', new_callable=Mock) as mock_run_shell_command: - mock_run_shell_command.return_value = (True, '') - run.set_up_adb_env() + mock_run_shell_command.side_effect = _mocked_run_shell_command + prefetch_app_runner = runner.PrefetchAppRunner(package='music', + activity='MainActivity', + readahead='warm', + compiler_filter=None, + timeout=None, + simulate=False, + debug=False, + input=None) + + prefetch_app_runner.preprocess() calls = [call('adb root'), call('adb shell "getenforce"'), call('adb shell "setenforce 0"'), call('adb shell "stop"'), call('adb shell "start"'), - call('adb wait-for-device')] + call('adb wait-for-device'), + call('adb shell ps | grep "music" | awk \'{print $2;}\''), + call('adb shell "kill 9999"')] mock_run_shell_command.assert_has_calls(calls) -def test_set_up_adb_env_with_permissive(): +def test_preprocess_with_cache_drop(): with patch('lib.cmd_utils.run_shell_command', new_callable=Mock) as mock_run_shell_command: - mock_run_shell_command.return_value = (True, 'Permissive') - run.set_up_adb_env() + mock_run_shell_command.side_effect = _mocked_run_shell_command + prefetch_app_runner = runner.PrefetchAppRunner(package='music', + activity='MainActivity', + readahead='cold', + compiler_filter=None, + timeout=None, + simulate=False, + debug=False, + input=None) + + prefetch_app_runner.preprocess() - calls = [call('adb root'), call('adb shell "getenforce"')] + calls = [call('adb root'), + call('adb shell "getenforce"'), + call('adb shell "setenforce 0"'), + call('adb shell "stop"'), + call('adb shell "start"'), + call('adb wait-for-device'), + call('adb shell ps | grep "music" | awk \'{print $2;}\''), + call('adb shell "kill 9999"'), + call('adb shell "echo 3 > /proc/sys/vm/drop_caches"')] mock_run_shell_command.assert_has_calls(calls) -def test_configure_compiler_filter(): +def test_preprocess_with_cache_drop_and_iorapd_enabled(): with patch('lib.cmd_utils.run_shell_command', new_callable=Mock) as mock_run_shell_command: - mock_run_shell_command.return_value = (True, 'speed arm64 kUpToDate') - run.configure_compiler_filter('speed', 'music', 'MainActivity') - - calls = [call(os.path.join(run.DIR, 'query_compiler_filter.py') + - ' --package music')] - mock_run_shell_command.assert_has_calls(calls) - -def test_parse_metrics_output(): - input = 'a1=b1\nc1=d1\ne1=f1' - ret = run.parse_metrics_output(input) - - assert ret == [('a1', 'b1'), ('c1', 'd1'), ('e1', 'f1')] - -def _mocked_run_shell_command(*args, **kwargs): - if args[0] == 'adb shell "date -u +\'%Y-%m-%d %H:%M:%S.%N\'"': - return (True, "2019-07-02 23:20:06.972674825") - elif args[0] == 'adb shell ps | grep "music" | awk \'{print $2;}\'': - return (True, '9999') - else: - return (True, 'a1=b1\nc1=d1=d2\ne1=f1') - -@patch('lib.adb_utils.blocking_wait_for_logcat_displayed_time') -@patch('lib.cmd_utils.run_shell_command') -def test_run_no_vm_cache_drop(mock_run_shell_command, - mock_blocking_wait_for_logcat_displayed_time): - mock_run_shell_command.side_effect = _mocked_run_shell_command - mock_blocking_wait_for_logcat_displayed_time.return_value = 123 - - run.run('warm', - 'music', - 'MainActivity', - timeout=10, - simulate=False, - debug=False) - - calls = [call('adb shell ps | grep "music" | awk \'{print $2;}\''), - call('adb shell "kill 9999"'), - call('adb shell "date -u +\'%Y-%m-%d %H:%M:%S.%N\'"'), - call( - 'timeout {timeout} "{DIR}/launch_application" "{package}" "{activity}"' - .format(timeout=10, - DIR=run.DIR, - package='music', - activity='MainActivity', - timestamp='2019-07-02 23:20:06.972674825')), - call('adb shell ps | grep "music" | awk \'{print $2;}\''), - call('adb shell "kill 9999"')] - mock_run_shell_command.assert_has_calls(calls) + mock_run_shell_command.side_effect = _mocked_run_shell_command + + with tempfile.NamedTemporaryFile() as input: + prefetch_app_runner = runner.PrefetchAppRunner(package='music', + activity='MainActivity', + readahead='fadvise', + compiler_filter=None, + timeout=None, + simulate=False, + debug=False, + input=input.name) + + prefetch_app_runner.preprocess() + + calls = [call('adb root'), + call('adb shell "getenforce"'), + call('adb shell "setenforce 0"'), + call('adb shell "stop"'), + call('adb shell "start"'), + call('adb wait-for-device'), + call( + 'adb shell ps | grep "music" | awk \'{print $2;}\''), + call('adb shell "kill 9999"'), + call('adb shell "echo 3 > /proc/sys/vm/drop_caches"'), + call('bash -c "source {}; iorapd_readahead_enable"'. + format(AppRunner.IORAP_COMMON_BASH_SCRIPT))] + mock_run_shell_command.assert_has_calls(calls) @patch('lib.adb_utils.blocking_wait_for_logcat_displayed_time') @patch('lib.cmd_utils.run_shell_command') -def test_run_with_vm_cache_drop_and_post_launch_cleanup( +def test_postprocess_with_launch_cleanup( mock_run_shell_command, mock_blocking_wait_for_logcat_displayed_time): mock_run_shell_command.side_effect = _mocked_run_shell_command mock_blocking_wait_for_logcat_displayed_time.return_value = 123 - run.run('fadvise', - 'music', - 'MainActivity', - timeout=10, - simulate=False, - debug=False) - - calls = [call('adb shell ps | grep "music" | awk \'{print $2;}\''), - call('adb shell "kill 9999"'), - call('adb shell "echo 3 > /proc/sys/vm/drop_caches"'), - call('bash -c "source {}; iorapd_readahead_enable"'. - format(run.IORAP_COMMON_BASH_SCRIPT)), - call('adb shell "date -u +\'%Y-%m-%d %H:%M:%S.%N\'"'), - call( - 'timeout {timeout} "{DIR}/launch_application" ' - '"{package}" "{activity}"' - .format(timeout=10, - DIR=run.DIR, - package='music', - activity='MainActivity', - timestamp='2019-07-02 23:20:06.972674825')), - call( - 'bash -c "source {script_path}; ' + with tempfile.NamedTemporaryFile() as input: + prefetch_app_runner = runner.PrefetchAppRunner(package='music', + activity='MainActivity', + readahead='fadvise', + compiler_filter=None, + timeout=10, + simulate=False, + debug=False, + input=input.name) + + prefetch_app_runner.postprocess('2019-07-02 23:20:06.972674825') + + calls = [ + call('bash -c "source {script_path}; ' 'iorapd_readahead_wait_until_finished ' '\'{package}\' \'{activity}\' \'{timestamp}\' \'{timeout}\'"'. - format(timeout=10, - package='music', - activity='MainActivity', - timestamp='2019-07-02 23:20:06.972674825', - script_path=run.IORAP_COMMON_BASH_SCRIPT)), - call('bash -c "source {}; iorapd_readahead_disable"'. - format(run.IORAP_COMMON_BASH_SCRIPT)), - call('adb shell ps | grep "music" | awk \'{print $2;}\''), - call('adb shell "kill 9999"')] - mock_run_shell_command.assert_has_calls(calls) + format(timeout=10, + package='music', + activity='MainActivity', + timestamp='2019-07-02 23:20:06.972674825', + script_path=AppRunner.IORAP_COMMON_BASH_SCRIPT)), + call('bash -c "source {}; iorapd_readahead_disable"'. + format(AppRunner.IORAP_COMMON_BASH_SCRIPT)), + call('adb shell ps | grep "music" | awk \'{print $2;}\''), + call('adb shell "kill 9999"')] + mock_run_shell_command.assert_has_calls(calls) if __name__ == '__main__': pytest.main() |