Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
S2-group
GitHub Repository: S2-group/android-runner
Path: blob/master/AndroidRunner/Plugins/perfetto/Perfetto.py
630 views
1
from AndroidRunner import Tests
2
from AndroidRunner.Plugins.Profiler import Profiler
3
from AndroidRunner.Plugins.Profiler import ProfilerException
4
import subprocess
5
import os
6
from AndroidRunner import util
7
import os.path as op
8
import datetime
9
from AndroidRunner import Adb
10
from AndroidRunner import util
11
12
class Perfetto(Profiler):
13
"""
14
Directory used for storing perfetto config files and perfetto traces on the device.
15
Other (including nested) directories will not work due to strict SELinux rules.
16
17
All paths that are on the device are suffixed with device_path
18
"""
19
PERFETTO_CONFIG_DEVICE_PATH = "/sdcard/perfetto/"
20
PERFETTO_TRACES_DEVICE_PATH = "/data/misc/perfetto-traces/"
21
22
def __init__(self, config, paths):
23
""" Inits the Perfetto class with config and paths params.
24
25
Parameters
26
----------
27
config : collections.OrderedDict
28
OrderedDictionary that contains the Perfetto plugin settings as provided by the given config.json file.
29
- paths : dict
30
Dictionary that contains the ROOT_DIR path, CONFIG_DIR path, OUTPUT_DIR path, ORIGINAL_CONFIG_DIR path and BASE_OUTPUT_DIR path.
31
"""
32
super(Perfetto, self).__init__(config, paths)
33
34
self.paths = paths
35
self.perfetto_trace_file_device_path = ""
36
37
self.perfetto_config_file_local_path = config["config_file"]
38
self.perfetto_config_file_format = config.get("config_file_format", "text")
39
self.perfetto_config_file_device_path = ""
40
41
self.adb_path = util.load_json(op.join(self.paths["CONFIG_DIR"], self.paths['ORIGINAL_CONFIG_DIR'])).get("adb_path", "adb")
42
43
def dependencies(self):
44
return []
45
46
def load(self, device):
47
""" Prepares the device for running the profiler.
48
49
Parameters
50
----------
51
- device : AndroidRunner.Device.Device
52
device on which the profiler is ran.
53
"""
54
55
if not os.path.exists(self.perfetto_config_file_local_path):
56
raise util.ConfigError(f"Config file not found on host. Is {self.perfetto_config_file_local_path} the correct path?")
57
# Construct path for perfetto config file on device.
58
perfetto_config_filename = self.perfetto_config_file_local_path.split("/")[-1]
59
self.perfetto_config_file_device_path = os.path.join(Perfetto.PERFETTO_CONFIG_DEVICE_PATH, perfetto_config_filename)
60
61
# Copy perfetto config file to device at constructed path.
62
device.push(self.perfetto_config_file_local_path, self.perfetto_config_file_device_path)
63
64
def set_output(self, output_dir):
65
self.output_dir = output_dir
66
67
def start_profiling(self, device, **kwargs):
68
""" Start profiling
69
70
Parameters
71
----------
72
- device : AndroidRunner.Device.Device
73
device on which the profiler is ran.
74
"""
75
# Construct perfetto trace file path on device.
76
filename = self._datetime_now().strftime("%Y_%m_%dT%H_%M_%S_%f")
77
self.perfetto_trace_file_device_path = os.path.join(Perfetto.PERFETTO_TRACES_DEVICE_PATH, f"{filename}.perfetto_trace")
78
79
# Start perfetto in background (-d) so it immediately exits and continues recording trace in background.
80
# It returns the PID of the perfetto process on the device.
81
# Before Android 12 we cannot directly pass the trace config file to perfetto due to over-restrictive SELinux rules.
82
# Instead, we have to pipe it to perfetto's stdin using cat.
83
perfetto_config_file_format_flag = "--txt" if self.perfetto_config_file_format == "text" else ""
84
proc = subprocess.Popen([self.adb_path, "-s", device.id, "shell",
85
f"cat {self.perfetto_config_file_device_path} | perfetto --background {perfetto_config_file_format_flag} -c - -o {self.perfetto_trace_file_device_path}"],
86
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
87
88
out, err = proc.communicate()
89
if err:
90
err = err.decode("ascii")
91
raise ProfilerException(f"There was an error running Perfetto: {err}")
92
93
# On Android 9 the PID is not returned so we "grep" it ourselves ;).
94
if not out:
95
pid = device.shell("ps -A | grep perfetto | awk '{print $2}'")
96
97
# We cannot convert it to int if there are more Perfetto processes.. So we throw an error.
98
try:
99
pid = int(pid)
100
except ValueError:
101
msg = "Found more than one Perfetto process. If you are running Perfetto"\
102
"on an Android 9 device please set the duration_ms"\
103
"in the Perfetto trace configuration file."
104
raise ProfilerException(msg)
105
self.perfetto_device_pid = str(pid)
106
else:
107
self.perfetto_device_pid = out.decode("ascii")
108
109
def stop_profiling(self, device, **kwargs):
110
""" Stop profiling
111
112
Parameters
113
----------
114
- device : AndroidRunner.Device.Device
115
device on which the profiler is ran.
116
"""
117
# Stop the perfetto tracing session by killing the perfetto process on the device using the received pid.
118
out = device.shell(f"kill {self.perfetto_device_pid}")
119
120
def collect_results(self, device):
121
""" Copy the profiling data from the device to the host.
122
123
Parameters
124
----------
125
- device : AndroidRunner.Device.Device
126
device on which the profiler is ran.
127
"""
128
# Copy resulting trace files from device to host.
129
# Before Android 9 we cannot directly pull the trace files from the device due to over-restrictive SELinux rules.
130
# We therefore use cat and redirect its output to a file on the host machine.
131
filename = self.perfetto_trace_file_device_path.split("/")[-1]
132
perfetto_trace_file_host_path = os.path.join(self.paths["OUTPUT_DIR"], filename)
133
134
with open(perfetto_trace_file_host_path, "w") as f:
135
proc = subprocess.Popen([self.adb_path, "-s", device.id, "shell", f"cat {self.perfetto_trace_file_device_path}"], stdin=subprocess.PIPE, stdout=f)
136
(_, _) = proc.communicate()
137
138
# Remove trace file from device since we already have it locally.
139
device.shell(f"rm -f {self.perfetto_trace_file_device_path}")
140
141
def unload(self, device):
142
""" Remove files from device that were used for profiling.
143
144
Parameters
145
----------
146
- device : AndroidRunner.Device.Device
147
device on which the profiler is ran.
148
"""
149
# Delete perfetto config file from device.
150
device.shell(f"rm -Rf {self.perfetto_config_file_device_path}")
151
152
def aggregate_subject(self): # pragma: no cover
153
# Since we require users to extract the data from the trace files themselves...
154
pass
155
156
def aggregate_end(self, data_dir, output_file): # pragma: no cover
157
# Since we require users to extract the data from the trace files themselves...
158
pass
159
160
def _datetime_now(self):
161
""" Returns the datetime.now() value: the current local date and time
162
Used since correctly patching datetime.datetime.now() can be cumbersome when unit testing.
163
164
Returns
165
-------
166
datetime.datetime
167
The current local date and time.
168
"""
169
return datetime.datetime.now()
170