• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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