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 24from .utils import convert_simpleperf_to_gecko 25 26PERFETTO_TRACE_FILE = "/data/misc/perfetto-traces/trace.perfetto-trace" 27PERFETTO_BOOT_TRACE_FILE = "/data/misc/perfetto-traces/boottrace.perfetto-trace" 28WEB_UI_ADDRESS = "https://ui.perfetto.dev" 29TRACE_START_DELAY_SECS = 0.5 30MAX_WAIT_FOR_INIT_USER_SWITCH_SECS = 180 31ANDROID_SDK_VERSION_T = 33 32 33 34class CommandExecutor(ABC): 35 """ 36 Abstract base class representing a command executor. 37 """ 38 def __init__(self): 39 pass 40 41 def execute(self, command, device): 42 error = device.check_device_connection() 43 if error is not None: 44 return error 45 device.root_device() 46 error = command.validate(device) 47 if error is not None: 48 return error 49 return self.execute_command(command, device) 50 51 @abstractmethod 52 def execute_command(self, command, device): 53 raise NotImplementedError 54 55 56class ProfilerCommandExecutor(CommandExecutor): 57 58 def execute_command(self, command, device): 59 config, error = self.create_config(command, device.get_android_sdk_version()) 60 if error is not None: 61 return error 62 error = self.prepare_device(command, device, config) 63 if error is not None: 64 return error 65 host_raw_trace_filename = None 66 host_gecko_trace_filename = None 67 for run in range(1, command.runs + 1): 68 timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") 69 if command.profiler == "perfetto": 70 host_raw_trace_filename = f"{command.out_dir}/trace-{timestamp}.perfetto-trace" 71 else: 72 host_raw_trace_filename = f"{command.out_dir}/perf-{timestamp}.data" 73 host_gecko_trace_filename = f"{command.out_dir}/perf-{timestamp}.json" 74 error = self.prepare_device_for_run(command, device) 75 if error is not None: 76 return error 77 error = self.execute_run(command, device, config, run) 78 if error is not None: 79 return error 80 error = self.retrieve_perf_data(command, device, host_raw_trace_filename, 81 host_gecko_trace_filename) 82 if error is not None: 83 return error 84 if command.runs != run: 85 time.sleep(command.between_dur_ms / 1000) 86 error = self.cleanup(command, device) 87 if error is not None: 88 return error 89 if command.use_ui: 90 error = open_trace(host_raw_trace_filename 91 if command.profiler == "perfetto" else 92 host_gecko_trace_filename, WEB_UI_ADDRESS, False) 93 if error is not None: 94 return error 95 return None 96 97 @staticmethod 98 def create_config(command, android_sdk_version): 99 if command.perfetto_config in PREDEFINED_PERFETTO_CONFIGS: 100 return PREDEFINED_PERFETTO_CONFIGS[command.perfetto_config]( 101 command, android_sdk_version) 102 else: 103 return build_custom_config(command) 104 105 def prepare_device(self, command, device, config): 106 return None 107 108 def prepare_device_for_run(self, command, device): 109 if command.profiler == "perfetto": 110 device.remove_file(PERFETTO_TRACE_FILE) 111 else: 112 device.remove_file(SIMPLEPERF_TRACE_FILE) 113 114 def execute_run(self, command, device, config, run): 115 print("Performing run %s" % run) 116 if command.profiler == "perfetto": 117 process = device.start_perfetto_trace(config) 118 else: 119 process = device.start_simpleperf_trace(command) 120 time.sleep(TRACE_START_DELAY_SECS) 121 error = self.trigger_system_event(command, device) 122 if error is not None: 123 device.kill_pid(command.profiler) 124 return error 125 process.wait() 126 127 def trigger_system_event(self, command, device): 128 return None 129 130 def retrieve_perf_data(self, command, device, host_raw_trace_filename, 131 host_gecko_trace_filename): 132 if command.profiler == "perfetto": 133 device.pull_file(PERFETTO_TRACE_FILE, host_raw_trace_filename) 134 else: 135 device.pull_file(SIMPLEPERF_TRACE_FILE, host_raw_trace_filename) 136 convert_simpleperf_to_gecko(command.scripts_path, host_raw_trace_filename, 137 host_gecko_trace_filename, command.symbols) 138 139 def cleanup(self, command, device): 140 return None 141 142 143class UserSwitchCommandExecutor(ProfilerCommandExecutor): 144 145 def prepare_device_for_run(self, command, device): 146 super().prepare_device_for_run(command, device) 147 current_user = device.get_current_user() 148 if command.from_user != current_user: 149 dur_seconds = min(command.dur_ms / 1000, 150 MAX_WAIT_FOR_INIT_USER_SWITCH_SECS) 151 print("Switching from the current user, %s, to the from-user, %s. Waiting" 152 " for %s seconds." 153 % (current_user, command.from_user, dur_seconds)) 154 device.perform_user_switch(command.from_user) 155 time.sleep(dur_seconds) 156 if device.get_current_user() != command.from_user: 157 raise Exception(("Device with serial %s took more than %d secs to " 158 "switch to the initial user." 159 % (device.serial, dur_seconds))) 160 161 def trigger_system_event(self, command, device): 162 print("Switching from the from-user, %s, to the to-user, %s." 163 % (command.from_user, command.to_user)) 164 device.perform_user_switch(command.to_user) 165 166 def cleanup(self, command, device): 167 if device.get_current_user() != command.original_user: 168 print("Switching from the to-user, %s, back to the original user, %s." 169 % (command.to_user, command.original_user)) 170 device.perform_user_switch(command.original_user) 171 172 173class BootCommandExecutor(ProfilerCommandExecutor): 174 175 def prepare_device(self, command, device, config): 176 device.write_to_file("/data/misc/perfetto-configs/boottrace.pbtxt", config) 177 178 def prepare_device_for_run(self, command, device): 179 device.remove_file(PERFETTO_BOOT_TRACE_FILE) 180 device.set_prop("persist.debug.perfetto.boottrace", "1") 181 182 def execute_run(self, command, device, config, run): 183 print("Performing run %s" % run) 184 self.trigger_system_event(command, device) 185 device.wait_for_device() 186 device.root_device() 187 dur_seconds = command.dur_ms / 1000 188 print("Tracing for %s seconds." % dur_seconds) 189 time.sleep(dur_seconds) 190 device.wait_for_boot_to_complete() 191 192 def trigger_system_event(self, command, device): 193 device.reboot() 194 195 def retrieve_perf_data(self, command, device, host_raw_trace_filename, 196 host_gecko_trace_filename): 197 device.pull_file(PERFETTO_BOOT_TRACE_FILE, host_raw_trace_filename) 198 199 200class AppStartupCommandExecutor(ProfilerCommandExecutor): 201 202 def execute_run(self, command, device, config, run): 203 error = super().execute_run(command, device, config, run) 204 if error is not None: 205 return error 206 device.force_stop_package(command.app) 207 208 def trigger_system_event(self, command, device): 209 return device.start_package(command.app) 210 211 212class ConfigCommandExecutor(CommandExecutor): 213 214 def execute(self, command, device): 215 return self.execute_command(command, device) 216 217 def execute_command(self, command, device): 218 match command.get_type(): 219 case "config list": 220 print("\n".join(list(PREDEFINED_PERFETTO_CONFIGS.keys()))) 221 return None 222 case "config show" | "config pull": 223 return self.execute_config_command(command, device) 224 case _: 225 raise ValueError("Invalid config subcommand was used.") 226 227 def execute_config_command(self, command, device): 228 android_sdk_version = ANDROID_SDK_VERSION_T 229 error = device.check_device_connection() 230 if error is None: 231 device.root_device() 232 android_sdk_version = device.get_android_sdk_version() 233 234 config, error = PREDEFINED_PERFETTO_CONFIGS[command.config_name]( 235 command, android_sdk_version) 236 237 if error is not None: 238 return error 239 240 if command.get_type() == "config pull": 241 subprocess.run(("cat > %s %s" % (command.file_path, config)), shell=True) 242 else: 243 print("\n".join(config.strip().split("\n")[2:-2])) 244 245 return None 246