• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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