1# Copyright 2019, The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Class to run an app.""" 16import os 17import sys 18from typing import Optional, List, Tuple 19 20# local import 21sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname( 22 os.path.abspath(__file__))))) 23 24import app_startup.lib.adb_utils as adb_utils 25import lib.cmd_utils as cmd_utils 26import lib.print_utils as print_utils 27 28class AppRunnerListener(object): 29 """Interface for lisenter of AppRunner. """ 30 31 def preprocess(self) -> None: 32 """Preprocess callback to initialized before the app is running. """ 33 pass 34 35 def postprocess(self, pre_launch_timestamp: str) -> None: 36 """Postprocess callback to cleanup after the app is running. 37 38 param: 39 'pre_launch_timestamp': indicates the timestamp when the app is 40 launching.. """ 41 pass 42 43 def metrics_selector(self, am_start_output: str, 44 pre_launch_timestamp: str) -> None: 45 """A metrics selection callback that waits for the desired metrics to 46 show up in logcat. 47 params: 48 'am_start_output': indicates the output of app startup. 49 'pre_launch_timestamp': indicates the timestamp when the app is 50 launching. 51 returns: 52 a string in the format of "<metric>=<value>\n<metric>=<value>\n..." 53 for further parsing. For example "TotalTime=123\nDisplayedTime=121". 54 Return an empty string if no metrics need to be parsed further. 55 """ 56 pass 57 58class AppRunner(object): 59 """ Class to run an app. """ 60 # static variables 61 DIR = os.path.abspath(os.path.dirname(__file__)) 62 APP_STARTUP_DIR = os.path.dirname(DIR) 63 IORAP_COMMON_BASH_SCRIPT = os.path.realpath(os.path.join(DIR, 64 '../../iorap/common')) 65 DEFAULT_TIMEOUT = 30 # seconds 66 67 def __init__(self, 68 package: str, 69 activity: Optional[str], 70 compiler_filter: Optional[str], 71 timeout: Optional[int], 72 simulate: bool): 73 self.package = package 74 self.simulate = simulate 75 76 # If the argument activity is None, try to set it. 77 self.activity = activity 78 if self.simulate: 79 self.activity = 'act' 80 if self.activity is None: 81 self.activity = AppRunner.get_activity(self.package) 82 83 self.compiler_filter = compiler_filter 84 self.timeout = timeout if timeout else AppRunner.DEFAULT_TIMEOUT 85 86 self.listeners = [] 87 88 def add_callbacks(self, listener: AppRunnerListener): 89 self.listeners.append(listener) 90 91 def remove_callbacks(self, listener: AppRunnerListener): 92 self.listeners.remove(listener) 93 94 @staticmethod 95 def get_activity(package: str) -> str: 96 """ Tries to set the activity based on the package. """ 97 passed, activity = cmd_utils.run_shell_func( 98 AppRunner.IORAP_COMMON_BASH_SCRIPT, 99 'get_activity_name', 100 [package]) 101 102 if not passed or not activity: 103 raise ValueError( 104 'Activity name could not be found, invalid package name?!') 105 106 return activity 107 108 def configure_compiler_filter(self) -> bool: 109 """Configures compiler filter (e.g. speed). 110 111 Returns: 112 A bool indicates whether configure of compiler filer succeeds or not. 113 """ 114 if not self.compiler_filter: 115 print_utils.debug_print('No --compiler-filter specified, don\'t' 116 ' need to force it.') 117 return True 118 119 passed, current_compiler_filter_info = \ 120 cmd_utils.run_shell_command( 121 '{} --package {}'.format(os.path.join(AppRunner.APP_STARTUP_DIR, 122 'query_compiler_filter.py'), 123 self.package)) 124 125 if passed != 0: 126 return passed 127 128 # TODO: call query_compiler_filter directly as a python function instead of 129 # these shell calls. 130 current_compiler_filter, current_reason, current_isa = \ 131 current_compiler_filter_info.split(' ') 132 print_utils.debug_print('Compiler Filter={} Reason={} Isa={}'.format( 133 current_compiler_filter, current_reason, current_isa)) 134 135 # Don't trust reasons that aren't 'unknown' because that means 136 # we didn't manually force the compilation filter. 137 # (e.g. if any automatic system-triggered compilations are not unknown). 138 if current_reason != 'unknown' or \ 139 current_compiler_filter != self.compiler_filter: 140 passed, _ = adb_utils.run_shell_command('{}/force_compiler_filter ' 141 '--compiler-filter "{}" ' 142 '--package "{}"' 143 ' --activity "{}'. 144 format(AppRunner.APP_STARTUP_DIR, 145 self.compiler_filter, 146 self.package, 147 self.activity)) 148 else: 149 adb_utils.debug_print('Queried compiler-filter matched requested ' 150 'compiler-filter, skip forcing.') 151 passed = False 152 return passed 153 154 def run(self) -> Optional[List[Tuple[str]]]: 155 """Runs an app. 156 157 Returns: 158 A list of (metric, value) tuples. 159 """ 160 print_utils.debug_print('==========================================') 161 print_utils.debug_print('===== START =====') 162 print_utils.debug_print('==========================================') 163 # Run the preprocess. 164 for listener in self.listeners: 165 listener.preprocess() 166 167 # Ensure the APK is currently compiled with whatever we passed in 168 # via --compiler-filter. 169 # No-op if this option was not passed in. 170 if not self.configure_compiler_filter(): 171 print_utils.error_print('Compiler filter configuration failed!') 172 return None 173 174 pre_launch_timestamp = adb_utils.logcat_save_timestamp() 175 # Launch the app. 176 results = self.launch_app(pre_launch_timestamp) 177 178 # Run the postprocess. 179 for listener in self.listeners: 180 listener.postprocess(pre_launch_timestamp) 181 182 return results 183 184 def launch_app(self, pre_launch_timestamp: str) -> Optional[List[Tuple[str]]]: 185 """ Launches the app. 186 187 Returns: 188 A list of (metric, value) tuples. 189 """ 190 print_utils.debug_print('Running with timeout {}'.format(self.timeout)) 191 192 passed, am_start_output = cmd_utils.run_shell_command('timeout {timeout} ' 193 '"{DIR}/launch_application" ' 194 '"{package}" ' 195 '"{activity}"'. 196 format(timeout=self.timeout, 197 DIR=AppRunner.APP_STARTUP_DIR, 198 package=self.package, 199 activity=self.activity)) 200 if not passed and not self.simulate: 201 return None 202 203 return self.wait_for_app_finish(pre_launch_timestamp, am_start_output) 204 205 def wait_for_app_finish(self, 206 pre_launch_timestamp: str, 207 am_start_output: str) -> Optional[List[Tuple[str]]]: 208 """ Wait for app finish and all metrics are shown in logcat. 209 210 Returns: 211 A list of (metric, value) tuples. 212 """ 213 if self.simulate: 214 return [('TotalTime', '123')] 215 216 ret = [] 217 for listener in self.listeners: 218 output = listener.metrics_selector(am_start_output, 219 pre_launch_timestamp) 220 ret = ret + AppRunner.parse_metrics_output(output) 221 222 return ret 223 224 @staticmethod 225 def parse_metrics_output(input: str) -> List[ 226 Tuple[str, str, str]]: 227 """Parses output of app startup to metrics and corresponding values. 228 229 It converts 'a=b\nc=d\ne=f\n...' into '[(a,b,''),(c,d,''),(e,f,'')]' 230 231 Returns: 232 A list of tuples that including metric name, metric value and rest info. 233 """ 234 all_metrics = [] 235 for line in input.split('\n'): 236 if not line: 237 continue 238 splits = line.split('=') 239 if len(splits) < 2: 240 print_utils.error_print('Bad line "{}"'.format(line)) 241 continue 242 metric_name = splits[0] 243 metric_value = splits[1] 244 rest = splits[2] if len(splits) > 2 else '' 245 if rest: 246 print_utils.error_print('Corrupt line "{}"'.format(line)) 247 print_utils.debug_print('metric: "{metric_name}", ' 248 'value: "{metric_value}" '. 249 format(metric_name=metric_name, 250 metric_value=metric_value)) 251 252 all_metrics.append((metric_name, metric_value)) 253 return all_metrics 254 255 @staticmethod 256 def parse_total_time( am_start_output: str) -> Optional[str]: 257 """Parses the total time from 'adb shell am start pkg' output. 258 259 Returns: 260 the total time of app startup. 261 """ 262 for line in am_start_output.split('\n'): 263 if 'TotalTime:' in line: 264 return line[len('TotalTime:'):].strip() 265 return None 266 267