• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2024 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
5import dataclasses
6from typing import List
7
8from immutabledict import immutabledict
9
10from crossbench import path as pth
11from crossbench.browsers.browser import Browser
12from crossbench.probes.cpu_frequency_map import CPUFrequencyMap
13from crossbench.env import HostEnvironment
14from crossbench.probes.env_modifier import EnvModifier
15from crossbench.probes.probe import (ProbeConfigParser, ProbeContext, ProbeKeyT)
16from crossbench.probes.results import EmptyProbeResult, ProbeResult
17from crossbench.runner.run import Run
18
19
20class FrequencyProbe(EnvModifier):
21  """
22  Probe to pin a frequency for certain parts of the system, e.g. CPUs and
23  memory on platforms with SysFS (Linux and Android). As of 10/2024, only CPUs
24  are supported. The probe can be configured as follows:
25
26  // Probe config HJSON.
27  frequency: {
28    cpus: {
29      cpu0: 1111,
30      cpu1: "min", // Will use the minimum allowed frequency.
31      cpu2: "max"  // Will use the maximum allowed frequency.
32    }
33  }
34
35  Generally, the system only allows a certain set of frequency values (for CPUs
36  the values can be found in [1]). Using an invalid value in the probe config
37  will cause a runtime error, but also print the list of valid values. Numerical
38  values can be specified as both integers (1111) and strings ("1111").
39
40  Wildcards are supported in 2 ways:
41
42  frequency: {
43    cpus: "max"
44  }
45
46
47  frequency: {
48    cpus: {
49      // When * is used, there should be no other keys in the map.
50      *: "max"
51    }
52  }
53
54  Note that when running with different platforms (e.g.
55  --browser=android:chrome-stable --browser=linux:chrome-stable), "*", "min"
56  and "max" might mean different things for each platform.
57
58  [1] https://docs.kernel.org/admin-guide/pm/cpufreq.html#:~:text=scaling_available_frequencies
59  """
60
61  NAME = "frequency"
62
63  IS_GENERAL_PURPOSE = True
64  PRODUCES_DATA = False
65
66  def __init__(self, cpus: CPUFrequencyMap):
67    super().__init__()
68    self._cpu_frequency_map: CPUFrequencyMap = cpus
69
70  @classmethod
71  def config_parser(cls) -> ProbeConfigParser:
72    parser = super().config_parser()
73    parser.add_argument(
74        "cpus",
75        type=CPUFrequencyMap,
76        default=CPUFrequencyMap.parse({}),
77        help="CPU frequency map, see FrequencyProbe docs")
78    return parser
79
80  @property
81  def key(self) -> ProbeKeyT:
82    return super().key + (("cpus", self._cpu_frequency_map.key),)
83
84  def validate_browser(self, env: HostEnvironment, browser: Browser) -> None:
85    super().validate_browser(env, browser)
86    # As long as a valid platform map can be derived, all is good.
87    self._cpu_frequency_map.get_target_frequencies(browser.platform)
88
89  @property
90  def cpu_frequency_map(self) -> CPUFrequencyMap:
91    return self._cpu_frequency_map
92
93  def get_context(self, run: Run):
94    return FrequencyProbeContext(self, run)
95
96
97@dataclasses.dataclass(frozen=True)
98class _FrequencyState:
99  dir: pth.AnyPosixPath
100  min: str
101  max: str
102
103
104class FrequencyProbeContext(ProbeContext[FrequencyProbe]):
105
106  _MIN_FREQUENCY_FILE: str = "scaling_min_freq"
107  _MAX_FREQUENCY_FILE: str = "scaling_max_freq"
108
109
110  def __init__(self, probe: FrequencyProbe, run: Run) -> None:
111    super().__init__(probe, run)
112    self._previous_frequencies: List[_FrequencyState] = []
113
114  def start(self) -> None:
115    target_cpu_frequencies: immutabledict[pth.AnyPosixPath, int] = (
116        self.probe.cpu_frequency_map.get_target_frequencies(
117            self.browser_platform))
118    for cpu_dir in target_cpu_frequencies.keys():
119      self._previous_frequencies.append(
120          _FrequencyState(
121              dir=cpu_dir,
122              min=self.browser_platform.cat(cpu_dir / self._MIN_FREQUENCY_FILE),
123              max=self.browser_platform.cat(cpu_dir /
124                                            self._MAX_FREQUENCY_FILE)))
125
126    try:
127      for cpu_dir, frequency in target_cpu_frequencies.items():
128        self.browser_platform.set_file_contents(
129            cpu_dir / self._MIN_FREQUENCY_FILE, f"{frequency}\n")
130        self.browser_platform.set_file_contents(
131            cpu_dir / self._MAX_FREQUENCY_FILE, f"{frequency}\n")
132    except Exception:
133      self._restore_frequencies()
134      raise
135
136  def stop(self) -> None:
137    self._restore_frequencies()
138
139  def _restore_frequencies(self) -> None:
140    for state in self._previous_frequencies:
141      self.browser_platform.set_file_contents(
142          state.dir / self._MIN_FREQUENCY_FILE, state.min)
143      self.browser_platform.set_file_contents(
144          state.dir / self._MAX_FREQUENCY_FILE, state.max)
145
146  def teardown(self) -> ProbeResult:
147    return EmptyProbeResult()
148