# # Copyright (C) 2024 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. # import datetime import subprocess import time from abc import ABC, abstractmethod from config_builder import PREDEFINED_PERFETTO_CONFIGS, build_custom_config from open_ui import open_trace from device import SIMPLEPERF_TRACE_FILE PERFETTO_TRACE_FILE = "/data/misc/perfetto-traces/trace.perfetto-trace" PERFETTO_BOOT_TRACE_FILE = "/data/misc/perfetto-traces/boottrace.perfetto-trace" WEB_UI_ADDRESS = "https://ui.perfetto.dev" TRACE_START_DELAY_SECS = 0.5 MAX_WAIT_FOR_INIT_USER_SWITCH_SECS = 180 ANDROID_SDK_VERSION_T = 33 class CommandExecutor(ABC): """ Abstract base class representing a command executor. """ def __init__(self): pass def execute(self, command, device): error = device.check_device_connection() if error is not None: return error device.root_device() error = command.validate(device) if error is not None: return error return self.execute_command(command, device) @abstractmethod def execute_command(self, command, device): raise NotImplementedError class ProfilerCommandExecutor(CommandExecutor): def execute_command(self, command, device): config, error = self.create_config(command, device.get_android_sdk_version()) if error is not None: return error error = self.prepare_device(command, device, config) if error is not None: return error host_file = None for run in range(1, command.runs + 1): timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") if command.profiler == "perfetto": host_file = f"{command.out_dir}/trace-{timestamp}.perfetto-trace" else: host_file = f"{command.out_dir}/perf-{timestamp}.data" error = self.prepare_device_for_run(command, device) if error is not None: return error error = self.execute_run(command, device, config, run) if error is not None: return error error = self.retrieve_perf_data(command, device, host_file) if error is not None: return error if command.runs != run: time.sleep(command.between_dur_ms / 1000) error = self.cleanup(command, device) if error is not None: return error if command.use_ui: open_trace(host_file, WEB_UI_ADDRESS) return None @staticmethod def create_config(command, android_sdk_version): if command.perfetto_config in PREDEFINED_PERFETTO_CONFIGS: return PREDEFINED_PERFETTO_CONFIGS[command.perfetto_config]( command, android_sdk_version) else: return build_custom_config(command) def prepare_device(self, command, device, config): return None def prepare_device_for_run(self, command, device): if command.profiler == "perfetto": device.remove_file(PERFETTO_TRACE_FILE) else: device.remove_file(SIMPLEPERF_TRACE_FILE) def execute_run(self, command, device, config, run): print("Performing run %s" % run) if command.profiler == "perfetto": process = device.start_perfetto_trace(config) else: process = device.start_simpleperf_trace(command) time.sleep(TRACE_START_DELAY_SECS) error = self.trigger_system_event(command, device) if error is not None: device.kill_pid(command.profiler) return error process.wait() def trigger_system_event(self, command, device): return None def retrieve_perf_data(self, command, device, host_file): if command.profiler == "perfetto": device.pull_file(PERFETTO_TRACE_FILE, host_file) else: device.pull_file(SIMPLEPERF_TRACE_FILE, host_file) def cleanup(self, command, device): return None class UserSwitchCommandExecutor(ProfilerCommandExecutor): def prepare_device_for_run(self, command, device): super().prepare_device_for_run(command, device) current_user = device.get_current_user() if command.from_user != current_user: dur_seconds = min(command.dur_ms / 1000, MAX_WAIT_FOR_INIT_USER_SWITCH_SECS) print("Switching from the current user, %s, to the from-user, %s. Waiting" " for %s seconds." % (current_user, command.from_user, dur_seconds)) device.perform_user_switch(command.from_user) time.sleep(dur_seconds) if device.get_current_user() != command.from_user: raise Exception(("Device with serial %s took more than %d secs to " "switch to the initial user." % (device.serial, dur_seconds))) def trigger_system_event(self, command, device): print("Switching from the from-user, %s, to the to-user, %s." % (command.from_user, command.to_user)) device.perform_user_switch(command.to_user) def cleanup(self, command, device): if device.get_current_user() != command.original_user: print("Switching from the to-user, %s, back to the original user, %s." % (command.to_user, command.original_user)) device.perform_user_switch(command.original_user) class BootCommandExecutor(ProfilerCommandExecutor): def prepare_device(self, command, device, config): device.write_to_file("/data/misc/perfetto-configs/boottrace.pbtxt", config) def prepare_device_for_run(self, command, device): device.remove_file(PERFETTO_BOOT_TRACE_FILE) device.set_prop("persist.debug.perfetto.boottrace", "1") def execute_run(self, command, device, config, run): print("Performing run %s" % run) self.trigger_system_event(command, device) device.wait_for_device() device.root_device() dur_seconds = command.dur_ms / 1000 print("Tracing for %s seconds." % dur_seconds) time.sleep(dur_seconds) device.wait_for_boot_to_complete() def trigger_system_event(self, command, device): device.reboot() def retrieve_perf_data(self, command, device, host_file): device.pull_file(PERFETTO_BOOT_TRACE_FILE, host_file) class AppStartupCommandExecutor(ProfilerCommandExecutor): def execute_run(self, command, device, config, run): error = super().execute_run(command, device, config, run) if error is not None: return error device.force_stop_package(command.app) def trigger_system_event(self, command, device): return device.start_package(command.app) class ConfigCommandExecutor(CommandExecutor): def execute(self, command, device): return self.execute_command(command, device) def execute_command(self, command, device): match command.get_type(): case "config list": print("\n".join(list(PREDEFINED_PERFETTO_CONFIGS.keys()))) return None case "config show" | "config pull": return self.execute_config_command(command, device) case _: raise ValueError("Invalid config subcommand was used.") def execute_config_command(self, command, device): android_sdk_version = ANDROID_SDK_VERSION_T error = device.check_device_connection() if error is None: device.root_device() android_sdk_version = device.get_android_sdk_version() config, error = PREDEFINED_PERFETTO_CONFIGS[command.config_name]( command, android_sdk_version) if error is not None: return error if command.get_type() == "config pull": subprocess.run(("cat > %s %s" % (command.file_path, config)), shell=True) else: print("\n".join(config.strip().split("\n")[2:-2])) return None