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