benchmarking/profilers/perfetto/perfetto.py (326 lines of code) (raw):

#!/usr/bin/env python ############################################################################## # Copyright 2021-present, Facebook, Inc. # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. ############################################################################## import logging import os import shutil import tempfile import time from pathlib import Path from tempfile import NamedTemporaryFile from typing import Optional # from platforms.android.android_platform import AndroidPlatform from profilers.perfetto.perfetto_config import ( CONFIG_TEMPLATE, POWER_CONFIG, HEAPPROFD_CONFIG, ANDROID_LOG_CONFIG, LINUX_FTRACE_CONFIG, ) from profilers.profiler_base import ProfilerBase from profilers.utilities import generate_perf_filename, upload_profiling_reports from utils.custom_logger import getLogger PROCESS_KEY = "perfetto" """ Perfetto is a native memory and battery profiling tool for Android OS 10 or better. It can be used to profile both Android applications and native processes running on Android. It can profile both Java and C++ code on Android. Perfetto can be used to profile Android benchmarks as both applications and binaries. The resulting perf data is used to generate an html report including a flamegraph (TODO). Both perf data and the report are uploaded to manifold and the urls are returned as a meta dict which can be updated in the benchmark's meta data. """ logger = logging.getLogger(__name__) class Perfetto(ProfilerBase): CONFIG_FILE = "perfetto.conf" DEVICE_DIRECTORY = "/data/local/tmp/perf" TRACING_PROPERTY = "persist.traced.enable" DEFAULT_TIMEOUT = 5 BUFFER_SIZE_KB_DEFAULT = 256 * 1024 # 256 megabytes BUFFER_SIZE2_KB_DEFAULT = 2 * 1024 # 2 megabytes SHMEM_SIZE_BYTES_DEFAULT = 8388608 SAMPLING_INTERVAL_BYTES_DEFAULT = 4096 BATTERY_POLL_MS_DEFAULT = 1000 MAX_FILE_SIZE_BYTES_DEFAULT = 100000000 def __init__( self, platform, *, types=None, options=None, model_name="benchmark", ): self.platform = platform self.types = types or ["memory"] self.options = options or {} self.android_version: int = int(platform.rel_version.split(".")[0]) self.adb = platform.util self.valid = True self.perfetto_pid = None self.all_heaps = ( f"all_heaps: {self.options.get('all_heaps', 'false')}" if self.android_version >= 12 else "" ) self.basename = generate_perf_filename(model_name, self.adb.device) self.trace_file_name = f"{self.basename}.perfetto-trace" self.trace_file_device = f"{self.DEVICE_DIRECTORY}/{self.trace_file_name}" self.config_file = f"{self.basename}.{self.CONFIG_FILE}" self.config_file_device = f"{self.DEVICE_DIRECTORY}/{self.config_file}" self.data_file = f"{self.basename}.data.json" self.report_file = f"{self.basename}.txt" # f"{self.basename}.html" self.user_home = str(Path.home()) self.host_binary_location = f"{self.user_home}/android" self.host_output_dir = "" self.meta = {} self.is_rooted_device = self.adb.isRootedDevice() self.user_was_root = self.adb.user_is_root() if self.is_rooted_device else False self.original_SELinux_policy = ( self.adb.shell( ["getenforce"], default=[""], timeout=self.DEFAULT_TIMEOUT, )[0] .strip() .lower() ) self.perfetto_cmd = [ "perfetto", "-d", "--txt", "-c", self.config_file_device, "-o", self.trace_file_device, ] super(Perfetto, self).__init__(None) def __enter__(self): self._start() return self def __exit__(self, type, value, traceback): if self.meta == {}: self.meta = self._finish() def _start(self): """Begin Perfetto profiling on platform.""" try: if self.android_version < 10: getLogger().error( f"Attempt to run Perfetto on {self.platform.type} {self.platform.rel_version} device {self.adb.device} ignored." ) self.valid = False return None if not self.is_rooted_device: getLogger().error( f"Attempt to run Perfetto on unrooted device {self.adb.device} ignored." ) self.valid = False return None getLogger().info(f"Collect Perfetto data on device {self.adb.device}") self._enablePerfetto() # Generate and upload custom config file getLogger().info(f"Perfetto profile type(s) = {','.join(self.types)}.") self._setup_perfetto_config() """ # Ensure no old instances of perfetto are running on the device self.adb.shell( ["killall", "perfetto"], timeout=DEFAULT_TIMEOUT, ) """ # call Perfetto output = self._perfetto() if output != 1 and output[0] != "1": self.perfetto_pid = output[0] return output except Exception: self.valid = False getLogger().exception("Perfetto profiling could not be started.") return None def getResults(self): if self.valid: self.meta = self._finish() return self.meta def _finish(self): no_report_str = "Perfetto profiling reporting could not be completed." if not self.valid: self._restoreState() return {} meta = {} self.host_output_dir = tempfile.mkdtemp() try: # if we ran perfetto, signal it to stop profiling if self._signalPerfetto(): getLogger().info( f"Looking for Perfetto data on device {self.adb.device}" ) self._copyPerfDataToHost() self._generateReport() meta = self._uploadResults() else: getLogger().error( no_report_str, ) except Exception as e: getLogger().exception( no_report_str + f" {e}", exc_info=True, ) # TODO: remove reboot + sleep once this is done in device manager self.adb.reboot() time.sleep(10) meta = {} finally: self._restoreState() shutil.rmtree(self.host_output_dir) self.valid = False # prevent additional calls return meta def _uploadResults(self): meta = upload_profiling_reports( { "perfetto_config": os.path.join(self.host_output_dir, self.config_file), "perfetto_data": os.path.join( self.host_output_dir, self.trace_file_name ), # TODO: generate flamegraph here "perfetto_report": os.path.join(self.host_output_dir, self.config_file), } ) getLogger().info( f"Perfetto profiling data uploaded.\nPerfetto Config:\t{meta['perfetto_config']}\nPerfetto Data: \t{meta['perfetto_data']}\nPerfetto Report:\t{meta['perfetto_report']}" ) return meta def _restoreState(self): if self.original_SELinux_policy == "enforcing": self.adb.shell( ["setenforce", "1"], timeout=self.DEFAULT_TIMEOUT, retry=1, ) if (not self.user_was_root) and self.adb.user_is_root(): self.adb.unroot() # unroot only if it was not rooted to start def _signalPerfetto(self) -> bool: # signal perfetto to stop profiling and await results getLogger().info("Stopping Perfetto profiling.") result = None if self.perfetto_pid is not None: sigint_cmd = [ "kill", "-SIGINT", self.perfetto_pid, "&&", "wait", self.perfetto_pid, ] sigterm_cmd = ["kill", "-SIGTERM", self.perfetto_pid] else: sigint_cmd = ["pkill", "-SIGINT", "perfetto"] sigterm_cmd = ["pkill", "-SIGTERM", "perfetto"] cmd = sigint_cmd try: # Wait for Perfetto to finish gracefully getLogger().info("Running '" + " ".join(cmd) + "'.") result = self.adb.shell( sigint_cmd, timeout=30, retry=1, silent=True, ) if self.perfetto_pid is None: time.sleep(6.0) return True except Exception as e: getLogger().exception( f"Perfetto did not respond to SIGINT. Terminating. {e}." ) cmd = sigterm_cmd result = self.adb.shell( cmd, timeout=10, ) return False finally: getLogger().info(f"Running '{' '.join(cmd)}' returned {result}.") def _enablePerfetto(self): if not self.user_was_root: self.adb.root() # Set SELinux to permissive mode if not already if self.original_SELinux_policy == "enforcing": self.adb.shell( ["setenforce", "0"], timeout=self.DEFAULT_TIMEOUT, retry=1, ) # Enable Perfetto if not enabled yet. getprop_tracing_enabled = self.adb.getprop( self.TRACING_PROPERTY, default=["0"], timeout=self.DEFAULT_TIMEOUT, ) perfetto_enabled: str = ( getprop_tracing_enabled if getprop_tracing_enabled else "0" ) if not perfetto_enabled.startswith("1"): self.adb.setprop( self.TRACING_PROPERTY, "1", timeout=self.DEFAULT_TIMEOUT, ) def _setup_perfetto_config( self, *, app_name: str = "program", config_file_host: Optional[str] = None, android_logcat: bool = False, ): with NamedTemporaryFile() as f: if config_file_host is None: # Write custom perfetto config config_file_host = f.name heapprofd_config = "" power_config = "" linux_process_stats_config = "" linux_ftrace_config = "" android_log_config = "" track_event_config = "" buffer_size_kb = self.options.get( "buffer_size_kb", self.BUFFER_SIZE_KB_DEFAULT ) buffer_size2_kb = self.options.get( "buffer_size2_kb", self.BUFFER_SIZE2_KB_DEFAULT ) max_file_size_bytes = self.options.get( "max_file_size_bytes", self.MAX_FILE_SIZE_BYTES_DEFAULT ) if "memory" in self.types: shmem_size_bytes = self.options.get( "shmem_size_bytes", self.SHMEM_SIZE_BYTES_DEFAULT ) sampling_interval_bytes = self.options.get( "sampling_interval_bytes", self.SAMPLING_INTERVAL_BYTES_DEFAULT ) heapprofd_config = HEAPPROFD_CONFIG.format( all_heaps=self.all_heaps, shmem_size_bytes=shmem_size_bytes, sampling_interval_bytes=sampling_interval_bytes, app_name=app_name, ) if "battery" in self.types: battery_poll_ms = self.options.get( "battery_poll_ms", self.BATTERY_POLL_MS_DEFAULT ) power_config = POWER_CONFIG.format( battery_poll_ms=battery_poll_ms, ) linux_ftrace_config = LINUX_FTRACE_CONFIG.format( app_name=app_name, ) if "cpu" in self.types: getLogger().error( "Error: CPU profiling with perfetto is Not Yet Implemented.", ) if android_logcat: android_log_config = ANDROID_LOG_CONFIG # Generate config file config_str = CONFIG_TEMPLATE.format( max_file_size_bytes=max_file_size_bytes, buffer_size_kb=buffer_size_kb, buffer_size2_kb=buffer_size2_kb, android_log_config=android_log_config, power_config=power_config, heapprofd_config=heapprofd_config, linux_process_stats_config=linux_process_stats_config, linux_ftrace_config=linux_ftrace_config, track_event_config=track_event_config, ) f.write(config_str.encode("utf-8")) f.flush() # Push perfetto config to device getLogger().info( f"Host config file = {config_file_host},\nDevice config file = {self.config_file_device}." ) self.adb.push(config_file_host, self.config_file_device) # Setup permissions for it, to avoid perfetto call failure self.adb.shell(["chmod", "777", self.config_file_device]) def _perfetto(self): """Run perfetto on platform with benchmark process id.""" getLogger().info(f"Calling Perfetto: {self.perfetto_cmd}") output = self.platform.util.shell(self.perfetto_cmd) getLogger().info(f"Perfetto returned: {output}.") startup_time: float = 2.0 if self.all_heaps != "false" else 0.2 time.sleep(startup_time) # give it time to spin up return output def _copyPerfDataToHost(self): self.platform.moveFilesFromPlatform( os.path.join(self.trace_file_device), os.path.join(self.host_output_dir), ) self.platform.moveFilesFromPlatform( os.path.join(self.config_file_device), os.path.join(self.host_output_dir), ) def _generateReport(self): """Generate an html report from perfetto data.""" # TODO: implement