summaryrefslogtreecommitdiff
path: root/startop/scripts/app_startup
diff options
context:
space:
mode:
authorYan Wang <yawanng@google.com>2019-07-23 18:09:41 -0700
committerYan Wang <yawanng@google.com>2019-07-30 15:55:18 -0700
commit06f54882c06da4b3090f2edfe321c07b1a7ec2a6 (patch)
tree60cbc203e4061779ae53c5709c03f1d7a9ed0a65 /startop/scripts/app_startup
parent89d954c3e43be1a46ad45e15a41188adfedc453c (diff)
startop: Refactor app running.
Collecting and app running share lots of common code. Create a new class to run app and allow callbacks to specify preprocess and postprocess and metrics selection. Test: pytest run_app_with_prefetch_test.py Test: pytest app_runner_test.py Bug: 138233615 Change-Id: I972c82fb9ff3a0f6cc7661bc3dc47b342716c26c
Diffstat (limited to 'startop/scripts/app_startup')
-rwxr-xr-xstartop/scripts/app_startup/app_startup_runner.py21
-rw-r--r--startop/scripts/app_startup/lib/app_runner.py265
-rw-r--r--startop/scripts/app_startup/lib/app_runner_test.py104
-rw-r--r--startop/scripts/app_startup/run_app_with_prefetch.py407
-rw-r--r--startop/scripts/app_startup/run_app_with_prefetch_test.py201
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()