1# Copyright 2022 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 abc 8from typing import (TYPE_CHECKING, Dict, Hashable, Optional, Set, Tuple, Type, 9 TypeVar) 10 11from crossbench import plt 12from crossbench.config import ConfigParser 13from crossbench.probes.probe_context import ProbeContext, ProbeSessionContext 14from crossbench.probes.result_location import ResultLocation 15from crossbench.probes.results import EmptyProbeResult, ProbeResult 16 17if TYPE_CHECKING: 18 from crossbench.browsers.attributes import BrowserAttributes 19 from crossbench.browsers.browser import Browser 20 from crossbench.env import HostEnvironment 21 from crossbench.runner.groups.browsers import BrowsersRunGroup 22 from crossbench.runner.groups.cache_temperatures import \ 23 CacheTemperaturesRunGroup 24 from crossbench.runner.groups.repetitions import RepetitionsRunGroup 25 from crossbench.runner.groups.session import BrowserSessionRunGroup 26 from crossbench.runner.groups.stories import StoriesRunGroup 27 from crossbench.runner.run import Run 28 29 30ProbeT = TypeVar("ProbeT", bound="Probe") 31 32 33class ProbeConfigParser(ConfigParser[ProbeT]): 34 35 def __init__(self, probe_cls: Type[ProbeT]) -> None: 36 super().__init__("Probe", probe_cls, allow_unused_config_data=False) 37 self._probe_cls: Type[ProbeT] = probe_cls 38 39 @property 40 def probe_cls(self) -> Type[ProbeT]: 41 return self._probe_cls 42 43 44class ProbeMissingDataError(ValueError): 45 pass 46 47 48class ProbeValidationError(ValueError): 49 50 def __init__(self, probe: Probe, message: str) -> None: 51 self.probe = probe 52 super().__init__(f"Probe({probe.NAME}): {message}") 53 54 55class ProbeIncompatibleBrowser(ProbeValidationError): 56 57 def __init__(self, 58 probe: Probe, 59 browser: Browser, 60 message: str = "Incompatible browser") -> None: 61 super().__init__(probe, f"{message}, got {browser.attributes}") 62 63 64ProbeKeyT = Tuple[Tuple[str, Hashable], ...] 65 66 67class Probe(abc.ABC): 68 """ 69 Abstract Probe class. 70 71 Probes are responsible for extracting performance numbers from websites 72 / stories. 73 74 Probe interface: 75 - scope(): Return a custom ProbeContext (see below) 76 - validate_browser(): Customize to display warnings before using Probes with 77 incompatible settings / browsers. 78 The Probe object can the customize how to merge probe (performance) date at 79 multiple levels: 80 - multiple repetitions of the same story 81 - merged repetitions from multiple stories (same browser) 82 - Probe data from all Runs 83 84 Probes use a ProbeContext that is active during a story-Run. 85 The ProbeContext class defines a customizable interface 86 - setup(): Used for high-overhead Probe initialization 87 - start(): Low-overhead start-to-measure signal 88 - stop(): Low-overhead stop-to-measure signal 89 - teardown(): Used for high-overhead Probe cleanup 90 91 """ 92 NAME: str = "" 93 94 @classmethod 95 def config_parser(cls) -> ProbeConfigParser: 96 return ProbeConfigParser(cls) 97 98 @classmethod 99 def from_config(cls: Type[ProbeT], config_data: Dict) -> ProbeT: 100 return cls.config_parser().parse(config_data) 101 102 @classmethod 103 def help_text(cls) -> str: 104 return cls.config_parser().help 105 106 @classmethod 107 def summary_text(cls) -> str: 108 return cls.config_parser().summary 109 110 # Set to False if the Probe cannot be used with arbitrary Stories or Pages 111 IS_GENERAL_PURPOSE: bool = True 112 PRODUCES_DATA: bool = True 113 # Set the default probe result location, used to figure out whether result 114 # files need to be transferred from a remote machine. 115 RESULT_LOCATION = ResultLocation.LOCAL 116 # Set to True if the probe only works on battery power with single runs 117 BATTERY_ONLY: bool = False 118 119 def __init__(self) -> None: 120 assert self.name is not None, "A Probe must define a name" 121 self._browsers: Set[Browser] = set() 122 123 def __str__(self) -> str: 124 return type(self).__name__ 125 126 def __eq__(self, other) -> bool: 127 if self is other: 128 return True 129 if type(self) is not type(other): 130 return False 131 return self.key == other.key 132 133 @property 134 def is_internal(self) -> bool: 135 """Returns True for subclasses of InternalProbe that are not 136 directly user-accessible.""" 137 return False 138 139 @property 140 def key(self) -> ProbeKeyT: 141 """Return a sort key.""" 142 return (("name", self.name),) 143 144 def __hash__(self) -> int: 145 return hash(self.key) 146 147 @property 148 def host_platform(self) -> plt.Platform: 149 return plt.PLATFORM 150 151 @property 152 def name(self) -> str: 153 return self.NAME 154 155 @property 156 def result_path_name(self) -> str: 157 return self.name 158 159 @property 160 def is_attached(self) -> bool: 161 return len(self._browsers) > 0 162 163 def attach(self, browser: Browser) -> None: 164 assert browser not in self._browsers, ( 165 f"Probe={self.name} is attached multiple times to the same browser") 166 self._browsers.add(browser) 167 168 def validate_env(self, env: HostEnvironment) -> None: 169 """ 170 Part of the Checklist, make sure everything is set up correctly for a probe 171 to run. 172 Browser-only validation is handled in validate_browser(...). 173 """ 174 # Ensure that the proper super methods for setting up a probe were 175 # called. 176 assert self.is_attached, ( 177 f"Probe {self.name} is not properly attached to a browser") 178 for browser in self._browsers: 179 self.validate_browser(env, browser) 180 181 def validate_browser(self, env: HostEnvironment, browser: Browser) -> None: 182 """ 183 Validate that browser is compatible with this Probe. 184 - Raise ProbeValidationError for hard-errors, 185 - Use env.handle_warning for soft errors where we expect recoverable errors 186 or only partially broken results. 187 """ 188 del env, browser 189 190 def expect_browser(self, 191 browser: Browser, 192 attributes: BrowserAttributes, 193 message: Optional[str] = None) -> None: 194 if attributes in browser.attributes: 195 return 196 if not message: 197 message = f"Incompatible browser, expected {attributes}" 198 raise ProbeIncompatibleBrowser(self, browser, message) 199 200 def expect_macos(self, browser: Browser) -> None: 201 if not browser.platform.is_macos: 202 raise ProbeIncompatibleBrowser(self, browser, "Only supported on macOS") 203 204 def merge_cache_temperatures(self, 205 group: CacheTemperaturesRunGroup) -> ProbeResult: 206 """ 207 For merging probe data from multiple browser cache temperatures with the 208 same repetition, story and browser. 209 """ 210 # Return the first result by default. 211 return tuple(group.runs)[0].results[self] 212 213 def merge_repetitions(self, group: RepetitionsRunGroup) -> ProbeResult: 214 """ 215 For merging probe data from multiple repetitions of the same story. 216 """ 217 del group 218 return EmptyProbeResult() 219 220 def merge_stories(self, group: StoriesRunGroup) -> ProbeResult: 221 """ 222 For merging multiple stories for the same browser. 223 """ 224 del group 225 return EmptyProbeResult() 226 227 def merge_browsers(self, group: BrowsersRunGroup) -> ProbeResult: 228 """ 229 For merging all probe data (from multiple stories and browsers.) 230 """ 231 del group 232 return EmptyProbeResult() 233 234 @abc.abstractmethod 235 def get_context(self: ProbeT, run: Run) -> Optional[ProbeContext[ProbeT]]: 236 pass 237 238 def get_session_context( 239 self: ProbeT, 240 session: BrowserSessionRunGroup) -> Optional[ProbeSessionContext[ProbeT]]: 241 del session 242 243 def log_run_result(self, run: Run) -> None: 244 """ 245 Override to print a short summary of the collected results after a run 246 completes. 247 """ 248 del run 249 250 def log_browsers_result(self, group: BrowsersRunGroup) -> None: 251 """ 252 Override to print a short summary of all the collected results. 253 """ 254 del group 255