1# 2# Copyright (C) 2024 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15# 16 17import datetime 18import subprocess 19import time 20from abc import ABC, abstractmethod 21from config_builder import PREDEFINED_PERFETTO_CONFIGS, build_custom_config 22from open_ui import open_trace 23from device import SIMPLEPERF_TRACE_FILE 24 25PERFETTO_TRACE_FILE = "/data/misc/perfetto-traces/trace.perfetto-trace" 26PERFETTO_BOOT_TRACE_FILE = "/data/misc/perfetto-traces/boottrace.perfetto-trace" 27WEB_UI_ADDRESS = "https://ui.perfetto.dev" 28TRACE_START_DELAY_SECS = 0.5 29MAX_WAIT_FOR_INIT_USER_SWITCH_SECS = 180 30ANDROID_SDK_VERSION_T = 33 31 32 33class CommandExecutor(ABC): 34 """ 35 Abstract base class representing a command executor. 36 """ 37 def __init__(self): 38 pass 39 40 def execute(self, command, device): 41 error = device.check_device_connection() 42 if error is not None: 43 return error 44 device.root_device() 45 error = command.validate(device) 46 if error is not None: 47 return error 48 return self.execute_command(command, device) 49 50 @abstractmethod 51 def execute_command(self, command, device): 52 raise NotImplementedError 53 54 55class ProfilerCommandExecutor(CommandExecutor): 56 57 def execute_command(self, command, device): 58 config, error = self.create_config(command, device.get_android_sdk_version()) 59 if error is not None: 60 return error 61 error = self.prepare_device(command, device, config) 62 if error is not None: 63 return error 64 host_file = None 65 for run in range(1, command.runs + 1): 66 timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") 67 if command.profiler == "perfetto": 68 host_file = f"{command.out_dir}/trace-{timestamp}.perfetto-trace" 69 else: 70 host_file = f"{command.out_dir}/perf-{timestamp}.data" 71 error = self.prepare_device_for_run(command, device) 72 if error is not None: 73 return error 74 error = self.execute_run(command, device, config, run) 75 if error is not None: 76 return error 77 error = self.retrieve_perf_data(command, device, host_file) 78 if error is not None: 79 return error 80 if command.runs != run: 81 time.sleep(command.between_dur_ms / 1000) 82 error = self.cleanup(command, device) 83 if error is not None: 84 return error 85 if command.use_ui: 86 open_trace(host_file, WEB_UI_ADDRESS) 87 return None 88 89 @staticmethod 90 def create_config(command, android_sdk_version): 91 if command.perfetto_config in PREDEFINED_PERFETTO_CONFIGS: 92 return PREDEFINED_PERFETTO_CONFIGS[command.perfetto_config]( 93 command, android_sdk_version) 94 else: 95 return build_custom_config(command) 96 97 def prepare_device(self, command, device, config): 98 return None 99 100 def prepare_device_for_run(self, command, device): 101 if command.profiler == "perfetto": 102 device.remove_file(PERFETTO_TRACE_FILE) 103 else: 104 device.remove_file(SIMPLEPERF_TRACE_FILE) 105 106 def execute_run(self, command, device, config, run): 107 print("Performing run %s" % run) 108 if command.profiler == "perfetto": 109 process = device.start_perfetto_trace(config) 110 else: 111 process = device.start_simpleperf_trace(command) 112 time.sleep(TRACE_START_DELAY_SECS) 113 error = self.trigger_system_event(command, device) 114 if error is not None: 115 device.kill_pid(command.profiler) 116 return error 117 process.wait() 118 119 def trigger_system_event(self, command, device): 120 return None 121 122 def retrieve_perf_data(self, command, device, host_file): 123 if command.profiler == "perfetto": 124 device.pull_file(PERFETTO_TRACE_FILE, host_file) 125 else: 126 device.pull_file(SIMPLEPERF_TRACE_FILE, host_file) 127 128 def cleanup(self, command, device): 129 return None 130 131 132class UserSwitchCommandExecutor(ProfilerCommandExecutor): 133 134 def prepare_device_for_run(self, command, device): 135 super().prepare_device_for_run(command, device) 136 current_user = device.get_current_user() 137 if command.from_user != current_user: 138 dur_seconds = min(command.dur_ms / 1000, 139 MAX_WAIT_FOR_INIT_USER_SWITCH_SECS) 140 print("Switching from the current user, %s, to the from-user, %s. Waiting" 141 " for %s seconds." 142 % (current_user, command.from_user, dur_seconds)) 143 device.perform_user_switch(command.from_user) 144 time.sleep(dur_seconds) 145 if device.get_current_user() != command.from_user: 146 raise Exception(("Device with serial %s took more than %d secs to " 147 "switch to the initial user." 148 % (device.serial, dur_seconds))) 149 150 def trigger_system_event(self, command, device): 151 print("Switching from the from-user, %s, to the to-user, %s." 152 % (command.from_user, command.to_user)) 153 device.perform_user_switch(command.to_user) 154 155 def cleanup(self, command, device): 156 if device.get_current_user() != command.original_user: 157 print("Switching from the to-user, %s, back to the original user, %s." 158 % (command.to_user, command.original_user)) 159 device.perform_user_switch(command.original_user) 160 161 162class BootCommandExecutor(ProfilerCommandExecutor): 163 164 def prepare_device(self, command, device, config): 165 device.write_to_file("/data/misc/perfetto-configs/boottrace.pbtxt", config) 166 167 def prepare_device_for_run(self, command, device): 168 device.remove_file(PERFETTO_BOOT_TRACE_FILE) 169 device.set_prop("persist.debug.perfetto.boottrace", "1") 170 171 def execute_run(self, command, device, config, run): 172 print("Performing run %s" % run) 173 self.trigger_system_event(command, device) 174 device.wait_for_device() 175 device.root_device() 176 dur_seconds = command.dur_ms / 1000 177 print("Tracing for %s seconds." % dur_seconds) 178 time.sleep(dur_seconds) 179 device.wait_for_boot_to_complete() 180 181 def trigger_system_event(self, command, device): 182 device.reboot() 183 184 def retrieve_perf_data(self, command, device, host_file): 185 device.pull_file(PERFETTO_BOOT_TRACE_FILE, host_file) 186 187 188class AppStartupCommandExecutor(ProfilerCommandExecutor): 189 190 def execute_run(self, command, device, config, run): 191 error = super().execute_run(command, device, config, run) 192 if error is not None: 193 return error 194 device.force_stop_package(command.app) 195 196 def trigger_system_event(self, command, device): 197 return device.start_package(command.app) 198 199 200class ConfigCommandExecutor(CommandExecutor): 201 202 def execute(self, command, device): 203 return self.execute_command(command, device) 204 205 def execute_command(self, command, device): 206 match command.get_type(): 207 case "config list": 208 print("\n".join(list(PREDEFINED_PERFETTO_CONFIGS.keys()))) 209 return None 210 case "config show" | "config pull": 211 return self.execute_config_command(command, device) 212 case _: 213 raise ValueError("Invalid config subcommand was used.") 214 215 def execute_config_command(self, command, device): 216 android_sdk_version = ANDROID_SDK_VERSION_T 217 error = device.check_device_connection() 218 if error is None: 219 device.root_device() 220 android_sdk_version = device.get_android_sdk_version() 221 222 config, error = PREDEFINED_PERFETTO_CONFIGS[command.config_name]( 223 command, android_sdk_version) 224 225 if error is not None: 226 return error 227 228 if command.get_type() == "config pull": 229 subprocess.run(("cat > %s %s" % (command.file_path, config)), shell=True) 230 else: 231 print("\n".join(config.strip().split("\n")[2:-2])) 232 233 return None 234