blob: 488bc75874fd9975e9c0d8b6161178c8fe3600b2 [file] [log] [blame]
# Copyright 2023 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 abc
import logging
import subprocess
from typing import TYPE_CHECKING, Iterable, Optional, cast
from crossbench import helper
from crossbench import path as pth
from crossbench.cli_helper import parse_remote_path
from crossbench.plt.android_adb import AndroidAdbPlatform
from crossbench.probes.probe import (Probe, ProbeConfigParser, ProbeContext,
ProbeIncompatibleBrowser, ProbeKeyT,
ResultLocation)
from crossbench.probes.results import LocalProbeResult, ProbeResult
if TYPE_CHECKING:
from crossbench.browsers.browser import Browser
from crossbench.env import HostEnvironment
from crossbench.runner.groups.browsers import BrowsersRunGroup
from crossbench.runner.run import Run
_PERFETTO_CONFIG_REMOTE_DIR = pth.RemotePath("/data/misc/perfetto-configs/")
_PERFETTO_TRACE_REMOTE_DIR = pth.RemotePath("/data/misc/perfetto-traces/")
class PerfettoProbe(Probe):
"""
Android-only probe to collect Perfetto system traces that can be viewed on
https://ui.perfetto.dev/.
Recommended way to use:
1. Go to https://ui.perfetto.dev/, click "Record new trace" and set up your
preferred tracing options.
2. Click "Recording command" and copy the textproto config part of the
command.
3. Paste it into the textproto field of the probe config. An example probe
config can be found at config/doc/probe/perfetto.config.hjson.
4. Specify the config via the --probe-config command-line flag.
After the run, the trace will be found among the results as
"perfetto.trace.pb.gz".
"""
NAME = "perfetto"
RESULT_LOCATION = ResultLocation.BROWSER
IS_GENERAL_PURPOSE = True
@classmethod
def config_parser(cls) -> ProbeConfigParser:
parser = super().config_parser()
parser.add_argument(
"textproto",
type=str,
help=("Serialized perfetto configuration. "
"See probe instructions for more details"))
parser.add_argument(
"perfetto_bin",
type=parse_remote_path,
default="perfetto",
help="Perfetto binary on the browser device")
return parser
def __init__(self, textproto: str, perfetto_bin: pth.RemotePath):
super().__init__()
if not textproto:
raise ValueError("Please specify a tracing config")
self._textproto = textproto
if not perfetto_bin:
raise ValueError("Please specify a perfetto binary.")
self._perfetto_bin = perfetto_bin
@property
def key(self) -> ProbeKeyT:
return super().key + (
("textproto", self.textproto),
("perfetto_bin", str(self.perfetto_bin)),
)
@property
def textproto(self) -> str:
return self._textproto
@property
def perfetto_bin(self) -> pth.RemotePath:
return self._perfetto_bin
@property
def result_path_name(self) -> str:
return "perfetto.trace.pb"
def validate_browser(self, env: HostEnvironment, browser: Browser) -> None:
super().validate_browser(env, browser)
if not browser.platform.is_android:
raise ProbeIncompatibleBrowser(self, browser, "Only supported on android")
def attach(self, browser: Browser) -> None:
assert browser.attributes.is_chromium_based
browser.features.enable("EnablePerfettoSystemTracing")
super().attach(browser)
def log_run_result(self, run: Run) -> None:
self._log_results([run])
def log_browsers_result(self, group: BrowsersRunGroup) -> None:
self._log_results(group.runs)
def _log_results(self, runs: Iterable[Run]) -> None:
logging.info("-" * 80)
logging.critical("Perfetto trace results:")
for run in runs:
result_file = run.results[self].file
logging.critical(" - %s : %s", result_file,
helper.get_file_size(result_file))
def get_context(self, run: Run) -> PerfettoProbeContext:
# TODO: support more platforms
return AndroidPerfettoProbeContext(self, run)
class PerfettoProbeContext(ProbeContext[PerfettoProbe], metaclass=abc.ABCMeta):
pass
class AndroidPerfettoProbeContext(PerfettoProbeContext):
def __init__(self, probe: PerfettoProbe, run: Run) -> None:
super().__init__(probe, run)
self._host_config_file: pth.LocalPath = (
run.out_dir / "perfetto_config.textproto")
self._browser_config_file: pth.RemotePath = (
_PERFETTO_CONFIG_REMOTE_DIR / "perfetto_config.textproto")
self._pid: Optional[int] = None
def get_default_result_path(self) -> pth.RemotePath:
return _PERFETTO_TRACE_REMOTE_DIR / "perfetto.trace.pb"
@property
def browser_platform(self) -> AndroidAdbPlatform:
browser_platform = super().browser_platform
assert isinstance(browser_platform, AndroidAdbPlatform)
return cast(AndroidAdbPlatform, browser_platform)
def setup(self) -> None:
assert self._pid is None
for p in self.browser_platform.processes():
if p["name"] == "perfetto":
logging.warning("PERFETTO: killing existing session pid: %s", p["pid"])
self.browser_platform.terminate(p["pid"])
if not self.browser_platform.which(self.probe.perfetto_bin):
raise ValueError(
f"perfetto bin '{self.probe.perfetto_bin}' cannot be found "
f"on {self.browser_platform}")
self.runner_platform.set_file_contents(self._host_config_file,
self.probe.textproto)
self.browser_platform.push(self._host_config_file,
self._browser_config_file)
def start(self) -> None:
logging.info("PERFETTO: starting")
proc = self.browser_platform.sh(
self.probe.perfetto_bin,
"--background",
"--config",
self._browser_config_file,
"--txt",
"--out",
self.result_path,
capture_output=True)
if proc.returncode > 0:
logging.error("perfetto command failed with stderr: %s", proc.stderr)
raise subprocess.CalledProcessError(proc.returncode, proc.args,
proc.stdout, proc.stderr)
self._pid = int(proc.stdout.decode("utf-8").rstrip())
self.browser.performance_mark(self.runner,
"crossbench-probe-perfetto-start")
def stop(self) -> None:
self.browser.performance_mark(self.runner, "crossbench-probe-perfetto-stop")
logging.info("PERFETTO: stopping")
if not self._pid:
raise RuntimeError("Perfetto was not started")
# TODO(cbruni): replace with wait_and_terminate
self.browser_platform.terminate(self._pid)
try:
for _ in helper.WaitRange(1, 30).wait_with_backoff():
if not self.browser_platform.process_info(self._pid):
break
except TimeoutError:
logging.error("perfetto process did not stop after 30s. "
"The trace might be incomplete.")
self._pid = None
def teardown(self) -> ProbeResult:
# Copy files:
browser_result = self.browser_result(file=[self.result_path])
local_result_file = browser_result.file
assert local_result_file.is_file(), (
f"Could not copy perfetto results: {local_result_file}")
self.runner_platform.sh("gzip", local_result_file)
local_result_file = local_result_file.with_suffix(
f"{local_result_file.suffix}.gz")
return LocalProbeResult(trace=(local_result_file,))