| # Copyright 2024 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from __future__ import annotations |
| |
| import atexit |
| import io |
| import logging |
| import subprocess |
| import time |
| from typing import TYPE_CHECKING, Iterable, Optional, cast |
| |
| from typing_extensions import override |
| |
| from crossbench.browsers.chromium_based.chromium_based import ChromiumBased |
| from crossbench.probes.profiling.context.base import PosixProfilingContext |
| from crossbench.probes.profiling.enum import CallGraphMode, TargetMode |
| |
| if TYPE_CHECKING: |
| import crossbench.path as pth |
| from crossbench.plt.types import ListCmdArgs |
| from crossbench.probes.results import ProbeResult |
| |
| |
| class AndroidProfilingContext(PosixProfilingContext): |
| |
| def _generate_command_line(self) -> ListCmdArgs: |
| renderer_pid: int | None = None |
| renderer_main_tid: int | None = None |
| if self.probe.target in (TargetMode.RENDERER_MAIN_ONLY, |
| TargetMode.RENDERER_PROCESS_ONLY): |
| renderer_pid, renderer_main_tid = self.renderer_pid_tid |
| return generate_simpleperf_command_line( |
| self.probe.target, |
| str(self.run.browser.path), |
| renderer_pid, |
| renderer_main_tid, |
| self.probe.call_graph_mode, |
| self.probe.frequency, |
| self.probe.count, |
| self.probe.cpu, |
| self.probe.events, |
| self.probe.grouped_events, |
| self.probe.add_counters, |
| self.result_path, |
| ) |
| |
| def _start_simpleperf(self) -> None: |
| command_line = self._generate_command_line() |
| logging.info("Starting simpleperf with command line: %s.", command_line) |
| self._profiling_process = self.browser_platform.popen( |
| *command_line, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| # Wait a bit for simpleperf to start and (potentially) terminate on error. |
| time.sleep(1) |
| if self._profiling_process.poll(): |
| error_msg: str = "" |
| if stdout := self._profiling_process.stdout: |
| if isinstance(stdout, io.BufferedReader): |
| error_msg = stdout.read().decode("utf-8") |
| logging.error(error_msg) |
| raise ValueError(f"Unable to start simpleperf. {error_msg}") |
| atexit.register(self.stop_process) |
| self.browser.performance_mark("probe-profiling-start") |
| |
| def _get_simpleperf_pids(self) -> list[int]: |
| simpleperf_pids = [] |
| for process in self.browser_platform.processes(): |
| if process["name"] == "simpleperf": |
| simpleperf_pids.append(process["pid"]) |
| return simpleperf_pids |
| |
| def _stop_existing_simpleperf(self) -> None: |
| for simpleperf_pid in self._get_simpleperf_pids(): |
| logging.warning("Terminating existing simpleperf process: %d.", |
| simpleperf_pid) |
| self.browser_platform.terminate(simpleperf_pid) |
| |
| def _cpu_mask(self, cpus: Iterable) -> str: |
| assert max(cpus) < 32, "Cpu index too high" |
| mask = 0 |
| for cpu in cpus: |
| mask |= (1 << cpu) |
| return f"{mask:x}" |
| |
| def _pin_renderer_main_core(self, cpu: int) -> None: |
| _, renderer_main_tid = self.renderer_pid_tid |
| self.browser_platform.sh("taskset", "-p", self._cpu_mask([cpu]), |
| str(renderer_main_tid)) |
| |
| def get_default_result_path(self) -> pth.AnyPath: |
| return super().get_default_result_path().parent / "simpleperf.perf.data" |
| |
| def setup(self) -> None: |
| assert self.browser.platform.is_android, ( |
| f"Expected Android platform, found {type(self.browser.platform)}.") |
| assert self.browser.attributes().is_chromium_based, ( |
| f"Expected Chromium-based browser, found {type(self.browser)}.") |
| if (self.browser.platform.is_android and |
| self.browser.attributes().is_chromium_based): |
| chromium = cast(ChromiumBased, self.browser) |
| # Set `--enable-benchmarking-extension` explicitly for |
| # retrieving Renderer PID, if needed. |
| chromium.flags.enable_benchmarking_api() |
| self._stop_existing_simpleperf() |
| |
| def start(self) -> None: |
| if not self.probe.start_profiling_after_setup: |
| self._start_simpleperf() |
| |
| @override |
| def start_story_run(self) -> None: |
| super().start_story_run() |
| if self.probe.pin_renderer_main_core is not None: |
| self._pin_renderer_main_core(self.probe.pin_renderer_main_core) |
| |
| if self.probe.start_profiling_after_setup: |
| self._start_simpleperf() |
| |
| def stop(self) -> None: |
| self.stop_process() |
| |
| def stop_process(self) -> None: |
| if self._profiling_process: |
| self.browser_platform.terminate_gracefully( |
| self._profiling_process, |
| timeout=30, |
| signal=self.browser_platform.signals.SIGINT) |
| self._profiling_process = None |
| self.browser.performance_mark("probe-profiling-stop") |
| |
| def teardown(self) -> ProbeResult: |
| return self.browser_result(perfetto=[self.result_path]) |
| |
| |
| def generate_simpleperf_command_line( |
| target: TargetMode, |
| app_name: str, |
| renderer_pid: Optional[int], |
| renderer_main_tid: Optional[int], |
| call_graph_mode: CallGraphMode, |
| frequency: Optional[int | str], |
| count: Optional[int], |
| cpus: tuple[int, ...], |
| events: tuple[str, ...], |
| grouped_events: tuple[str, ...], |
| add_counters: tuple[str, ...], |
| output_path: pth.AnyPath, |
| ) -> ListCmdArgs: |
| command_line: ListCmdArgs = ["simpleperf", "record"] |
| if target == TargetMode.RENDERER_MAIN_ONLY: |
| assert renderer_main_tid is not None |
| command_line.extend(["-t", str(renderer_main_tid)]) |
| elif target == TargetMode.RENDERER_PROCESS_ONLY: |
| assert renderer_pid is not None |
| command_line.extend(["-p", str(renderer_pid)]) |
| elif target == TargetMode.BROWSER_APP_ONLY: |
| command_line.extend(["--app", app_name]) |
| else: # TargetMode.SYSTEM_WIDE |
| command_line.append("-a") |
| if call_graph_mode == CallGraphMode.FRAME_POINTER: |
| command_line.extend(["--call-graph", "fp"]) |
| elif call_graph_mode == CallGraphMode.DWARF: |
| # Use "--post-unwind=yes" while unwinding with DWARF, to reduce |
| # unwinding overhead during profiling. |
| command_line.extend(["--call-graph", "dwarf", "--post-unwind=yes"]) |
| else: |
| assert call_graph_mode == CallGraphMode.NO_CALL_GRAPH, ( |
| f"Invalid call_graph_mode: {call_graph_mode}") |
| if frequency is not None: |
| command_line.extend(["-f", str(frequency)]) |
| if count is not None: |
| command_line.extend(["-c", str(count)]) |
| if cpus: |
| command_line.extend(["--cpu", ",".join(map(str, cpus))]) |
| # Events and counters need to be provided after `-f` and `-c`. |
| if events: |
| command_line.extend(["-e", ",".join(events)]) |
| if grouped_events: |
| command_line.extend(["--group", ",".join(grouped_events)]) |
| if add_counters: |
| command_line.extend(["--add-counter", ",".join(add_counters)]) |
| # `--no-inherit` is required by simpleperf when `--add-counter` is used. |
| command_line.append("--no-inherit") |
| command_line.extend(["-o", output_path]) |
| return command_line |