blob: fe172d5993849d1fae50f000784eb2876cd9115f [file] [log] [blame]
# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from __future__ import annotations
import abc
import argparse
from typing import (TYPE_CHECKING, Any, Dict, Hashable, Optional, Set, Tuple,
Type, TypeVar)
from crossbench import plt
from crossbench.browsers.attributes import BrowserAttributes
from crossbench.config import ConfigParser
from crossbench.probes.probe_context import ProbeContext, ProbeSessionContext
from crossbench.probes.result_location import ResultLocation
from crossbench.probes.results import EmptyProbeResult, ProbeResult
if TYPE_CHECKING:
from crossbench.browsers.browser import Browser
from crossbench.env import HostEnvironment
from crossbench.runner.groups import (BrowserSessionRunGroup,
BrowsersRunGroup,
CacheTemperatureRunGroup,
RepetitionsRunGroup, StoriesRunGroup)
from crossbench.runner.run import Run
ProbeT = TypeVar("ProbeT", bound="Probe")
class ProbeConfigParser(ConfigParser[ProbeT]):
def __init__(self, probe_cls: Type[ProbeT]) -> None:
super().__init__("Probe", probe_cls, allow_unused_config_data=False)
self._probe_cls: Type[ProbeT] = probe_cls
@property
def probe_cls(self) -> Type[ProbeT]:
return self._probe_cls
class ProbeMissingDataError(ValueError):
pass
class ProbeValidationError(ValueError):
def __init__(self, probe: Probe, message: str) -> None:
self.probe = probe
super().__init__(f"Probe({probe.NAME}): {message}")
class ProbeIncompatibleBrowser(ProbeValidationError):
def __init__(self,
probe: Probe,
browser: Browser,
message: str = "Incompatible browser") -> None:
super().__init__(probe, f"{message}, got {browser.attributes}")
ProbeKeyT = Tuple[Tuple[str, Hashable], ...]
class Probe(abc.ABC):
"""
Abstract Probe class.
Probes are responsible for extracting performance numbers from websites
/ stories.
Probe interface:
- scope(): Return a custom ProbeContext (see below)
- validate_browser(): Customize to display warnings before using Probes with
incompatible settings / browsers.
The Probe object can the customize how to merge probe (performance) date at
multiple levels:
- multiple repetitions of the same story
- merged repetitions from multiple stories (same browser)
- Probe data from all Runs
Probes use a ProbeContext that is active during a story-Run.
The ProbeContext class defines a customizable interface
- setup(): Used for high-overhead Probe initialization
- start(): Low-overhead start-to-measure signal
- stop(): Low-overhead stop-to-measure signal
- teardown(): Used for high-overhead Probe cleanup
"""
NAME: str = ""
@classmethod
def config_parser(cls) -> ProbeConfigParser:
return ProbeConfigParser(cls)
@classmethod
def from_config(cls: Type[ProbeT], config_data: Dict) -> ProbeT:
return cls.config_parser().parse(config_data)
@classmethod
def help_text(cls) -> str:
return cls.config_parser().help
@classmethod
def summary_text(cls) -> str:
return cls.config_parser().summary
# Set to False if the Probe cannot be used with arbitrary Stories or Pages
IS_GENERAL_PURPOSE: bool = True
PRODUCES_DATA: bool = True
# Set the default probe result location, used to figure out whether result
# files need to be transferred from a remote machine.
RESULT_LOCATION = ResultLocation.LOCAL
# Set to True if the probe only works on battery power with single runs
BATTERY_ONLY: bool = False
def __init__(self) -> None:
assert self.name is not None, "A Probe must define a name"
self._browsers: Set[Browser] = set()
def __str__(self) -> str:
return type(self).__name__
def __eq__(self, other) -> bool:
if self is other:
return True
if type(self) is not type(other):
return False
return self.key == other.key
@property
def is_internal(self) -> bool:
"""Returns True for subclasses of InternalProbe that are not
directly user-accessible."""
return False
@property
def key(self) -> ProbeKeyT:
"""Return a sort key."""
return (("name", self.name),)
def __hash__(self) -> int:
return hash(self.key)
@property
def runner_platform(self) -> plt.Platform:
return plt.PLATFORM
@property
def name(self) -> str:
return self.NAME
@property
def result_path_name(self) -> str:
return self.name
@property
def is_attached(self) -> bool:
return len(self._browsers) > 0
def attach(self, browser: Browser) -> None:
assert browser not in self._browsers, (
f"Probe={self.name} is attached multiple times to the same browser")
self._browsers.add(browser)
def validate_env(self, env: HostEnvironment) -> None:
"""
Part of the Checklist, make sure everything is set up correctly for a probe
to run.
Browser-only validation is handled in validate_browser(...).
"""
# Ensure that the proper super methods for setting up a probe were
# called.
assert self.is_attached, (
f"Probe {self.name} is not properly attached to a browser")
for browser in self._browsers:
self.validate_browser(env, browser)
def validate_browser(self, env: HostEnvironment, browser: Browser) -> None:
"""
Validate that browser is compatible with this Probe.
- Raise ProbeValidationError for hard-errors,
- Use env.handle_warning for soft errors where we expect recoverable errors
or only partially broken results.
"""
del env, browser
def expect_browser(self,
browser: Browser,
attributes: BrowserAttributes,
message: Optional[str] = None) -> None:
if attributes in browser.attributes:
return
if not message:
message = f"Incompatible browser, expected {attributes}"
raise ProbeIncompatibleBrowser(self, browser, message)
def expect_macos(self, browser: Browser) -> None:
if not browser.platform.is_macos:
raise ProbeIncompatibleBrowser(self, browser, "Only supported on macOS")
def merge_cache_temperatures(self,
group: CacheTemperatureRunGroup) -> ProbeResult:
"""
For merging probe data from multiple browser cache temperatures with the
same repetition, story and browser.
"""
# Return the first result by default.
return tuple(group.runs)[0].results[self]
def merge_repetitions(self, group: RepetitionsRunGroup) -> ProbeResult:
"""
For merging probe data from multiple repetitions of the same story.
"""
del group
return EmptyProbeResult()
def merge_stories(self, group: StoriesRunGroup) -> ProbeResult:
"""
For merging multiple stories for the same browser.
"""
del group
return EmptyProbeResult()
def merge_browsers(self, group: BrowsersRunGroup) -> ProbeResult:
"""
For merging all probe data (from multiple stories and browsers.)
"""
del group
return EmptyProbeResult()
@abc.abstractmethod
def get_context(self: ProbeT, run: Run) -> Optional[ProbeContext[ProbeT]]:
pass
def get_session_context(
self: ProbeT,
session: BrowserSessionRunGroup) -> Optional[ProbeSessionContext[ProbeT]]:
del session
def log_run_result(self, run: Run) -> None:
"""
Override to print a short summary of the collected results after a run
completes.
"""
del run
def log_browsers_result(self, group: BrowsersRunGroup) -> None:
"""
Override to print a short summary of all the collected results.
"""
del group