• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2023 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
8import datetime as dt
9import enum
10from typing import Dict, Generic, Optional, Set, Type, TypeVar
11
12from crossbench import plt
13from crossbench.config import ConfigParser
14from crossbench.helper.state import BaseState, StateMachine
15from crossbench.probes.results import EmptyProbeResult, ProbeResult
16
17DecoratorT = TypeVar("DecoratorT", bound="Decorator")
18DecoratorTargetT = TypeVar("DecoratorTargetT")
19
20
21class DecoratorConfigParser(ConfigParser[DecoratorT]):
22
23  def __init__(self, probe_cls: Type[DecoratorT]) -> None:
24    super().__init__(
25        probe_cls.__name__, probe_cls, allow_unused_config_data=False)
26    self._probe_cls = probe_cls
27
28
29class Decorator(abc.ABC, Generic[DecoratorTargetT]):
30  """ Abstract base class for RunDecorator and SessionDecorator that can
31  temporarily modify Runs or BrowserSessions.
32  """
33
34  NAME: str = ""
35
36  @classmethod
37  def config_parser(cls) -> DecoratorConfigParser:
38    return DecoratorConfigParser(cls)
39
40  @classmethod
41  def from_config(cls: Type[DecoratorT], config_data: Dict) -> DecoratorT:
42    return cls.config_parser().parse(config_data)
43
44  @classmethod
45  def help_text(cls) -> str:
46    return str(cls.config_parser())
47
48  def __init__(self) -> None:
49    assert self.name is not None, f"{type(self).__name__} must have a name"
50    self._targets: Set[DecoratorTargetT] = set()
51
52  def __str__(self) -> str:
53    return type(self).__name__
54
55  @property
56  def host_platform(self) -> plt.Platform:
57    return plt.PLATFORM
58
59  @property
60  def name(self) -> str:
61    return self.NAME
62
63  @abc.abstractmethod
64  def context(
65      self: DecoratorT,
66      target: DecoratorTargetT,
67  ) -> DecoratorContext[DecoratorT, DecoratorTargetT]:
68    pass
69
70
71class DecoratorContext(abc.ABC, Generic[DecoratorT, DecoratorTargetT]):
72  """
73  The active python context-manager for a Decorator with a life-time interface
74  to manage measurement, services or resources.
75
76  +- setup()
77  |  Unmeasured scope, browser might not be active yet.
78  |
79  | +- start()
80  | |  Browser active / measured section.
81  | +- stop()
82  |
83  *- teardown()
84  """
85
86  @enum.unique
87  class _State(BaseState):
88    READY = enum.auto()
89    STARTING = enum.auto()
90    RUNNING = enum.auto()
91    SUCCESS = enum.auto()
92    FAILURE = enum.auto()
93
94  def __init__(self, decorator: DecoratorT, target: DecoratorTargetT) -> None:
95    self._decorator = decorator
96    self._target = target
97    self._state = StateMachine(self._State.READY)
98    self._is_success: bool = False
99    self._start_time: Optional[dt.datetime] = None
100    self._stop_time: Optional[dt.datetime] = None
101    self._label = f"{type(self).__name__} {self.name}"
102
103  @property
104  def name(self) -> str:
105    return self._decorator.name
106
107  @property
108  def label(self) -> str:
109    return self._label
110
111  @property
112  def start_time(self) -> dt.datetime:
113    """
114    Returns a unified start time that is the same for all active Decorators.
115    This can be used to account for startup delays caused by other Decorators.
116    """
117    assert self._start_time
118    return self._start_time
119
120  @property
121  def duration(self) -> dt.timedelta:
122    assert self._start_time and self._stop_time
123    return self._stop_time - self._start_time
124
125  @property
126  def is_success(self) -> bool:
127    return self._is_success
128
129  def set_start_time(self, start_datetime: dt.datetime) -> None:
130    # Used to set a uniform start time across all active DecoratorContexts.
131    assert self._start_time is None
132    self._start_time = start_datetime
133
134  def __enter__(self) -> None:
135    self._state.transition(self._State.READY, to=self._State.STARTING)
136    with self._target.exception_handler(f"{self._label} start"):
137      try:
138        self.start()
139        self._state.transition(self._State.STARTING, to=self._State.RUNNING)
140      except:
141        self._state.transition(self._State.STARTING, to=self._State.FAILURE)
142        raise
143
144  def __exit__(self, exc_type, exc_value, traceback) -> None:
145    self._state.expect(self._State.RUNNING, self._State.FAILURE)
146    with self._target.exception_handler(f"{self._label} stop"):
147      try:
148        self.stop()
149        if self._state == self._State.RUNNING:
150          self._state.transition(self._State.RUNNING, to=self._State.SUCCESS)
151      except:
152        self._state.transition(to=self._State.FAILURE)
153        raise
154      finally:
155        self._stop_time = dt.datetime.now()
156
157  def setup(self) -> None:
158    """
159    Called before starting the target.
160    Not on the critical path, can be used for heavy computation.
161    """
162
163  def start(self) -> None:
164    """
165    Called immediately before starting the given target, after the browser
166    started.
167    This method should have as little overhead as possible.
168    If possible, delegate heavy computation to the "setup" method.
169    """
170
171  def stop(self) -> None:
172    """
173    Called immediately after finishing the given Target with the browser still
174    running.
175    This method should have as little overhead as possible.
176    If possible, delegate heavy computation to the "teardown" method.
177    """
178
179  def teardown(self) -> ProbeResult:
180    """
181    Non time-critical, called after stopping all Decorators and after stopping
182    the target.
183    Heavy post-processing can be performed here without affect the result of
184    other DecoratorContexts.
185    """
186    return EmptyProbeResult()
187