xref: /aosp_15_r20/system/extras/torq/command_executor.py (revision 288bf5226967eb3dac5cce6c939ccc2a7f2b4fe5)
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