blob: 20ce3a44809ee7c18f6eb5719c208f86292b82ae [file] [log] [blame]
#
# 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