• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2023 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5from __future__ import annotations
6
7import atexit
8import datetime as dt
9import enum
10import subprocess
11from typing import TYPE_CHECKING, Optional, Sequence, Tuple
12
13from crossbench import compat, helper
14from crossbench.parse import DurationParser
15from crossbench.probes.probe import (Probe, ProbeConfigParser, ProbeContext,
16                                     ProbeKeyT)
17from crossbench.probes.result_location import ResultLocation
18
19if TYPE_CHECKING:
20  from crossbench.browsers.browser import Browser
21  from crossbench.env import HostEnvironment
22  from crossbench.path import AnyPath
23  from crossbench.probes.results import ProbeResult
24  from crossbench.runner.run import Run
25
26
27@enum.unique
28class SamplerType(compat.StrEnumWithHelp):
29  BATTERY = ("battery", "Battery level")
30  CPU_POWER = ("cpu_power",
31               "CPU power and per-core frequency and idle residency")
32  DISK = ("disk", "Number of read/write ops/bytes")
33  GPU_POWER = ("gpu_power",
34               "GPU power consumption, frequency and active residency")
35  INTERRUPTS = ("interrupts", "Per-core interrupt count")
36  NETWORK = ("network", "Number of in/out packets/bytes")
37  TASKS = ("tasks", "Per-task stats including CPU usage and wakeups")
38  THERMAL = ("thermal", "Thermal pressure state")
39
40
41class PowerMetricsProbe(Probe):
42  """
43  Probe to collect data using macOS's powermetrics command-line tool.
44  """
45
46  NAME = "powermetrics"
47  RESULT_LOCATION = ResultLocation.BROWSER
48  SAMPLERS: Tuple[SamplerType,
49                  ...] = (SamplerType.BATTERY, SamplerType.CPU_POWER,
50                          SamplerType.DISK, SamplerType.GPU_POWER,
51                          SamplerType.INTERRUPTS, SamplerType.NETWORK,
52                          SamplerType.TASKS, SamplerType.THERMAL)
53
54  @classmethod
55  def config_parser(cls) -> ProbeConfigParser:
56    parser = super().config_parser()
57    parser.add_argument(
58        "sampling_interval",
59        type=DurationParser.positive_duration,
60        default=1000)
61    parser.add_argument(
62        "samplers", type=SamplerType, default=cls.SAMPLERS, is_list=True)
63    return parser
64
65  def __init__(self,
66               sampling_interval: dt.timedelta = dt.timedelta(),
67               samplers: Sequence[SamplerType] = SAMPLERS):
68    super().__init__()
69    self._sampling_interval = sampling_interval
70    if sampling_interval.total_seconds() < 0:
71      raise ValueError(f"Invalid sampling_interval={sampling_interval}")
72    self._samplers = tuple(samplers)
73
74  @property
75  def key(self) -> ProbeKeyT:
76    return super().key + (
77        ("sampling_interval", self.sampling_interval.total_seconds()),
78        ("samplers", tuple(map(str, self.samplers))),
79    )
80
81  @property
82  def sampling_interval(self) -> dt.timedelta:
83    return self._sampling_interval
84
85  @property
86  def samplers(self) -> Tuple[SamplerType, ...]:
87    return self._samplers
88
89  def validate_browser(self, env: HostEnvironment, browser: Browser) -> None:
90    super().validate_browser(env, browser)
91    self.expect_macos(browser)
92
93  def get_context(self, run: Run) -> PowerMetricsProbeContext:
94    return PowerMetricsProbeContext(self, run)
95
96
97class PowerMetricsProbeContext(ProbeContext[PowerMetricsProbe]):
98
99  def __init__(self, probe: PowerMetricsProbe, run: Run) -> None:
100    super().__init__(probe, run)
101    self._power_metrics_process: Optional[subprocess.Popen] = None
102    self._output_plist_file: AnyPath = self.result_path.with_suffix(".plist")
103
104  def start(self) -> None:
105    self._power_metrics_process = self.browser_platform.popen(
106        "sudo",
107        "powermetrics",
108        "-f",
109        "plist",
110        f"--samplers={','.join(map(str, self.probe.samplers))}",
111        "-i",
112        f"{int(self.probe.sampling_interval.total_seconds())}",
113        "--output-file",
114        self._output_plist_file,
115        stdout=subprocess.DEVNULL)
116    if self._power_metrics_process.poll():
117      raise ValueError("Could not start powermetrics")
118    atexit.register(self.stop_process)
119
120  def stop(self) -> None:
121    if self._power_metrics_process:
122      self._power_metrics_process.terminate()
123
124  def teardown(self) -> ProbeResult:
125    self.stop_process()
126    return self.browser_result(file=(self._output_plist_file,))
127
128  def stop_process(self) -> None:
129    if self._power_metrics_process:
130      helper.wait_and_kill(self._power_metrics_process)
131      self._power_metrics_process = None
132