blob: e0b9e625cb635c23e22d5f8232a4b7915683ee7c [file] [log] [blame]
# 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 logging
import time
from typing import TYPE_CHECKING, Final, Optional
from typing_extensions import override
import crossbench.path as pth
from crossbench.cli import ui
from crossbench.probes.profiling.context.base import PosixProfilingContext
from crossbench.probes.profiling.enum import TargetMode
if TYPE_CHECKING:
from crossbench.probes.profiling.system_profiling import ProfilingProbe
from crossbench.probes.results import ProbeResult
from crossbench.runner.run import Run
_MAC_TRACE_TEMPLATE_PATH: Final[pth.LocalPath] = pth.LocalPath(
__file__).parents[1] / "time-profile.tracetemplate"
_XPATH_EXPRESSION: Final[str] = (
"//trace-toc/run/data/table["
'@category="PointsOfInterest" and @schema="os-signpost"]|'
'//trace-toc/run/data/table[@schema="cpu-profile"]')
class MacOSProfilingContext(PosixProfilingContext):
def __init__(self, probe: ProfilingProbe, run: Run) -> None:
super().__init__(probe, run)
assert self.probe.target in (
TargetMode.SYSTEM_WIDE, TargetMode.RENDERER_PROCESS_ONLY), (
f"Unsupported profiling mode for Mac: {str(self.probe.target)}")
@override
def get_default_result_path(self) -> pth.AnyPath:
return super().get_default_result_path().parent / "profile.trace"
def _start_xctrace(self, pid: Optional[int] = None) -> None:
assert self.browser_platform.is_file(_MAC_TRACE_TEMPLATE_PATH), (
f"Didn't find {_MAC_TRACE_TEMPLATE_PATH}")
atexit.register(self.stop_process)
process_filter = ["--all-processes"
] if pid is None else ["--attach", str(pid)]
self._profiling_process = self.browser_platform.popen(
"xctrace", "record", "--template", _MAC_TRACE_TEMPLATE_PATH,
*process_filter, "--output", self.result_path)
# xctrace takes some time to start up
time.sleep(3)
if self._profiling_process.poll():
raise ValueError("Could not start xctrace")
def start(self) -> None:
pass
@override
def start_story_run(self) -> None:
super().start_story_run()
# In theory this could start earlier but we leave it here as the
# renderer-process mode requires us to run when we are guaranteed
# to have a renderer available.
if self.probe.target == TargetMode.SYSTEM_WIDE:
self._start_xctrace()
elif self.probe.target == TargetMode.RENDERER_PROCESS_ONLY:
self._start_xctrace(self.renderer_pid_tid[0])
def stop(self) -> None:
# Needs to be SIGINT for xctrace, terminate won't work.
assert self._profiling_process
self.browser_platform.send_signal(self._profiling_process,
self.browser_platform.signals.SIGINT)
def teardown(self) -> ProbeResult:
self.stop_process()
trace_xml_path = self._export_trace_xml()
return self.browser_result(
trace=(self.result_path,), perfetto=(trace_xml_path,))
def _export_trace_xml(self) -> pth.AnyPath:
trace_xml_path = self.result_path.with_name("profile.trace.xml")
with self.run.actions(
f"Probe {self.probe.name}: Exporting {trace_xml_path.name}",
verbose=True), ui.spinner():
self.browser_platform.sh("xctrace", "export", "--input", self.result_path,
"--output", trace_xml_path, "--xpath",
_XPATH_EXPRESSION)
return trace_xml_path
def stop_process(self) -> None:
if not self._profiling_process:
return
logging.info(" Waiting for xctrace profiles (slow)...")
with ui.spinner():
self.browser_platform.terminate_gracefully(
self._profiling_process,
signal=self.browser_platform.signals.SIGINT,
timeout=60)
self._profiling_process = None
atexit.unregister(self.stop_process)