Path: blob/master/AndroidRunner/Plugins/perfetto/Perfetto.py
630 views
from AndroidRunner import Tests1from AndroidRunner.Plugins.Profiler import Profiler2from AndroidRunner.Plugins.Profiler import ProfilerException3import subprocess4import os5from AndroidRunner import util6import os.path as op7import datetime8from AndroidRunner import Adb9from AndroidRunner import util1011class Perfetto(Profiler):12"""13Directory used for storing perfetto config files and perfetto traces on the device.14Other (including nested) directories will not work due to strict SELinux rules.1516All paths that are on the device are suffixed with device_path17"""18PERFETTO_CONFIG_DEVICE_PATH = "/sdcard/perfetto/"19PERFETTO_TRACES_DEVICE_PATH = "/data/misc/perfetto-traces/"2021def __init__(self, config, paths):22""" Inits the Perfetto class with config and paths params.2324Parameters25----------26config : collections.OrderedDict27OrderedDictionary that contains the Perfetto plugin settings as provided by the given config.json file.28- paths : dict29Dictionary that contains the ROOT_DIR path, CONFIG_DIR path, OUTPUT_DIR path, ORIGINAL_CONFIG_DIR path and BASE_OUTPUT_DIR path.30"""31super(Perfetto, self).__init__(config, paths)3233self.paths = paths34self.perfetto_trace_file_device_path = ""3536self.perfetto_config_file_local_path = config["config_file"]37self.perfetto_config_file_format = config.get("config_file_format", "text")38self.perfetto_config_file_device_path = ""3940self.adb_path = util.load_json(op.join(self.paths["CONFIG_DIR"], self.paths['ORIGINAL_CONFIG_DIR'])).get("adb_path", "adb")4142def dependencies(self):43return []4445def load(self, device):46""" Prepares the device for running the profiler.4748Parameters49----------50- device : AndroidRunner.Device.Device51device on which the profiler is ran.52"""5354if not os.path.exists(self.perfetto_config_file_local_path):55raise util.ConfigError(f"Config file not found on host. Is {self.perfetto_config_file_local_path} the correct path?")56# Construct path for perfetto config file on device.57perfetto_config_filename = self.perfetto_config_file_local_path.split("/")[-1]58self.perfetto_config_file_device_path = os.path.join(Perfetto.PERFETTO_CONFIG_DEVICE_PATH, perfetto_config_filename)5960# Copy perfetto config file to device at constructed path.61device.push(self.perfetto_config_file_local_path, self.perfetto_config_file_device_path)6263def set_output(self, output_dir):64self.output_dir = output_dir6566def start_profiling(self, device, **kwargs):67""" Start profiling6869Parameters70----------71- device : AndroidRunner.Device.Device72device on which the profiler is ran.73"""74# Construct perfetto trace file path on device.75filename = self._datetime_now().strftime("%Y_%m_%dT%H_%M_%S_%f")76self.perfetto_trace_file_device_path = os.path.join(Perfetto.PERFETTO_TRACES_DEVICE_PATH, f"{filename}.perfetto_trace")7778# Start perfetto in background (-d) so it immediately exits and continues recording trace in background.79# It returns the PID of the perfetto process on the device.80# Before Android 12 we cannot directly pass the trace config file to perfetto due to over-restrictive SELinux rules.81# Instead, we have to pipe it to perfetto's stdin using cat.82perfetto_config_file_format_flag = "--txt" if self.perfetto_config_file_format == "text" else ""83proc = subprocess.Popen([self.adb_path, "-s", device.id, "shell",84f"cat {self.perfetto_config_file_device_path} | perfetto --background {perfetto_config_file_format_flag} -c - -o {self.perfetto_trace_file_device_path}"],85stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)8687out, err = proc.communicate()88if err:89err = err.decode("ascii")90raise ProfilerException(f"There was an error running Perfetto: {err}")9192# On Android 9 the PID is not returned so we "grep" it ourselves ;).93if not out:94pid = device.shell("ps -A | grep perfetto | awk '{print $2}'")9596# We cannot convert it to int if there are more Perfetto processes.. So we throw an error.97try:98pid = int(pid)99except ValueError:100msg = "Found more than one Perfetto process. If you are running Perfetto"\101"on an Android 9 device please set the duration_ms"\102"in the Perfetto trace configuration file."103raise ProfilerException(msg)104self.perfetto_device_pid = str(pid)105else:106self.perfetto_device_pid = out.decode("ascii")107108def stop_profiling(self, device, **kwargs):109""" Stop profiling110111Parameters112----------113- device : AndroidRunner.Device.Device114device on which the profiler is ran.115"""116# Stop the perfetto tracing session by killing the perfetto process on the device using the received pid.117out = device.shell(f"kill {self.perfetto_device_pid}")118119def collect_results(self, device):120""" Copy the profiling data from the device to the host.121122Parameters123----------124- device : AndroidRunner.Device.Device125device on which the profiler is ran.126"""127# Copy resulting trace files from device to host.128# Before Android 9 we cannot directly pull the trace files from the device due to over-restrictive SELinux rules.129# We therefore use cat and redirect its output to a file on the host machine.130filename = self.perfetto_trace_file_device_path.split("/")[-1]131perfetto_trace_file_host_path = os.path.join(self.paths["OUTPUT_DIR"], filename)132133with open(perfetto_trace_file_host_path, "w") as f:134proc = subprocess.Popen([self.adb_path, "-s", device.id, "shell", f"cat {self.perfetto_trace_file_device_path}"], stdin=subprocess.PIPE, stdout=f)135(_, _) = proc.communicate()136137# Remove trace file from device since we already have it locally.138device.shell(f"rm -f {self.perfetto_trace_file_device_path}")139140def unload(self, device):141""" Remove files from device that were used for profiling.142143Parameters144----------145- device : AndroidRunner.Device.Device146device on which the profiler is ran.147"""148# Delete perfetto config file from device.149device.shell(f"rm -Rf {self.perfetto_config_file_device_path}")150151def aggregate_subject(self): # pragma: no cover152# Since we require users to extract the data from the trace files themselves...153pass154155def aggregate_end(self, data_dir, output_file): # pragma: no cover156# Since we require users to extract the data from the trace files themselves...157pass158159def _datetime_now(self):160""" Returns the datetime.now() value: the current local date and time161Used since correctly patching datetime.datetime.now() can be cumbersome when unit testing.162163Returns164-------165datetime.datetime166The current local date and time.167"""168return datetime.datetime.now()169170