# 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 "=\n=\n..." for further parsing. For example "TotalTime=123\nDisplayedTime=121". Return an empty string if no metrics need to be parsed further. """ 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 # seconds 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