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