koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 1 | import abc |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 2 | import array |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 3 | from typing import Dict, List, Any, Optional, Tuple, cast, Type, Iterator |
| 4 | from collections import OrderedDict |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 5 | |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 6 | |
| 7 | import numpy |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 8 | from scipy.stats.mstats_basic import NormaltestResult |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 9 | |
| 10 | |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 11 | from .node_interfaces import IRPCNode |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 12 | from .common_types import IStorable, Storable |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 13 | from .utils import round_digits, Number |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 14 | |
| 15 | |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 16 | class TestJobConfig(Storable, metaclass=abc.ABCMeta): |
| 17 | def __init__(self, idx: int) -> None: |
| 18 | self.idx = idx |
| 19 | self.reliable_info_time_range = None # type: Tuple[int, int] |
| 20 | self.vals = OrderedDict() # type: Dict[str, Any] |
| 21 | |
| 22 | @property |
| 23 | def storage_id(self) -> str: |
| 24 | return "{}_{}".format(self.summary, self.idx) |
| 25 | |
| 26 | @abc.abstractproperty |
| 27 | def characterized_tuple(self) -> Tuple: |
| 28 | pass |
| 29 | |
| 30 | @abc.abstractproperty |
| 31 | def summary(self, *excluded_fields) -> str: |
| 32 | pass |
| 33 | |
| 34 | @abc.abstractproperty |
| 35 | def long_summary(self, *excluded_fields) -> str: |
| 36 | pass |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 37 | |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 38 | |
| 39 | class TestSuiteConfig(IStorable): |
| 40 | """ |
| 41 | Test suite input configuration. |
| 42 | |
| 43 | test_type - test type name |
| 44 | params - parameters from yaml file for this test |
| 45 | run_uuid - UUID to be used to create file names & Co |
| 46 | nodes - nodes to run tests on |
| 47 | remote_dir - directory on nodes to be used for local files |
| 48 | """ |
| 49 | def __init__(self, |
| 50 | test_type: str, |
| 51 | params: Dict[str, Any], |
| 52 | run_uuid: str, |
| 53 | nodes: List[IRPCNode], |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 54 | remote_dir: str, |
| 55 | idx: int) -> None: |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 56 | self.test_type = test_type |
| 57 | self.params = params |
| 58 | self.run_uuid = run_uuid |
| 59 | self.nodes = nodes |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 60 | self.nodes_ids = [node.node_id for node in nodes] |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 61 | self.remote_dir = remote_dir |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 62 | self.storage_id = "{}_{}".format(self.test_type, idx) |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 63 | |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 64 | def __eq__(self, o: object) -> bool: |
| 65 | if type(o) is not self.__class__: |
| 66 | return False |
| 67 | |
| 68 | other = cast(TestSuiteConfig, o) |
| 69 | |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 70 | return (self.test_type == other.test_type and |
| 71 | self.params == other.params and |
| 72 | set(self.nodes_ids) == set(other.nodes_ids)) |
| 73 | |
| 74 | def raw(self) -> Dict[str, Any]: |
| 75 | res = self.__dict__.copy() |
| 76 | del res['nodes'] |
| 77 | del res['run_uuid'] |
| 78 | del res['remote_dir'] |
| 79 | return res |
| 80 | |
| 81 | @classmethod |
| 82 | def fromraw(cls, data: Dict[str, Any]) -> 'IStorable': |
| 83 | obj = cls.__new__(cls) |
| 84 | data = data.copy() |
| 85 | data['nodes'] = None |
| 86 | data['run_uuid'] = None |
| 87 | data['remote_dir'] = None |
| 88 | obj.__dict__.update(data) |
| 89 | return obj |
| 90 | |
| 91 | |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 92 | class DataSource: |
| 93 | def __init__(self, |
| 94 | suite_id: str = None, |
| 95 | job_id: str = None, |
| 96 | node_id: str = None, |
| 97 | dev: str = None, |
| 98 | sensor: str = None, |
| 99 | tag: str = None) -> None: |
| 100 | self.suite_id = suite_id |
| 101 | self.job_id = job_id |
| 102 | self.node_id = node_id |
| 103 | self.dev = dev |
| 104 | self.sensor = sensor |
| 105 | self.tag = tag |
| 106 | |
| 107 | def __call__(self, **kwargs) -> 'DataSource': |
| 108 | dct = self.__dict__.copy() |
| 109 | dct.update(kwargs) |
| 110 | return self.__class__(**dct) |
| 111 | |
| 112 | def __str__(self) -> str: |
| 113 | return "{0.suite_id}.{0.job_id}/{0.node_id}/{0.dev}.{0.sensor}.{0.tag}".format(self) |
| 114 | |
| 115 | def __repr__(self) -> str: |
| 116 | return str(self) |
| 117 | |
| 118 | |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 119 | class TimeSeries: |
| 120 | """Data series from sensor - either system sensor or from load generator tool (e.g. fio)""" |
| 121 | |
| 122 | def __init__(self, |
| 123 | name: str, |
| 124 | raw: Optional[bytes], |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 125 | data: numpy.array, |
| 126 | times: numpy.array, |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 127 | second_axis_size: int = 1, |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 128 | source: DataSource = None) -> None: |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 129 | |
| 130 | # Sensor name. Typically DEV_NAME.METRIC |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 131 | self.name = name |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 132 | |
| 133 | # Time series times and values. Time in ms from Unix epoch. |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 134 | self.times = times |
| 135 | self.data = data |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 136 | |
| 137 | # Not equal to 1 in case of 2d sensors, like latency, when each measurement is a histogram. |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 138 | self.second_axis_size = second_axis_size |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 139 | |
| 140 | # Raw sensor data (is provided). Like log file for fio iops/bw/lat. |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 141 | self.raw = raw |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 142 | |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 143 | self.source = source |
| 144 | |
| 145 | def __str__(self) -> str: |
| 146 | res = "TS({}):\n".format(self.name) |
| 147 | res += " source={}\n".format(self.source) |
| 148 | res += " times_size={}\n".format(len(self.times)) |
| 149 | res += " data_size={}\n".format(len(self.data)) |
| 150 | res += " data_shape={}x{}\n".format(len(self.data) // self.second_axis_size, self.second_axis_size) |
| 151 | return res |
| 152 | |
| 153 | def __repr__(self) -> str: |
| 154 | return str(self) |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 155 | |
| 156 | |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 157 | # (node_name, source_dev, metric_name) => metric_results |
| 158 | JobMetrics = Dict[Tuple[str, str, str], TimeSeries] |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 159 | |
| 160 | |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 161 | class StatProps(IStorable): |
| 162 | "Statistic properties for timeseries with unknown data distribution" |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 163 | def __init__(self, data: numpy.array) -> None: |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 164 | self.perc_99 = None # type: float |
| 165 | self.perc_95 = None # type: float |
| 166 | self.perc_90 = None # type: float |
| 167 | self.perc_50 = None # type: float |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 168 | |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 169 | self.min = None # type: Number |
| 170 | self.max = None # type: Number |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 171 | |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 172 | # bin_center: bin_count |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 173 | self.bins_populations = None # type: numpy.array |
| 174 | self.bins_mids = None # type: numpy.array |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 175 | self.data = data |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 176 | |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 177 | def __str__(self) -> str: |
| 178 | res = ["{}(size = {}):".format(self.__class__.__name__, len(self.data))] |
| 179 | for name in ["perc_50", "perc_90", "perc_95", "perc_99"]: |
| 180 | res.append(" {} = {}".format(name, round_digits(getattr(self, name)))) |
| 181 | res.append(" range {} {}".format(round_digits(self.min), round_digits(self.max))) |
| 182 | return "\n".join(res) |
| 183 | |
| 184 | def __repr__(self) -> str: |
| 185 | return str(self) |
| 186 | |
| 187 | def raw(self) -> Dict[str, Any]: |
| 188 | data = self.__dict__.copy() |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 189 | del data['data'] |
| 190 | data['bins_mids'] = list(self.bins_mids) |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 191 | data['bins_populations'] = list(self.bins_populations) |
| 192 | return data |
| 193 | |
| 194 | @classmethod |
| 195 | def fromraw(cls, data: Dict[str, Any]) -> 'StatProps': |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 196 | data['bins_mids'] = numpy.array(data['bins_mids']) |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 197 | data['bins_populations'] = numpy.array(data['bins_populations']) |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 198 | data['data'] = None |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 199 | res = cls.__new__(cls) |
| 200 | res.__dict__.update(data) |
| 201 | return res |
| 202 | |
| 203 | |
| 204 | class HistoStatProps(StatProps): |
| 205 | """Statistic properties for 2D timeseries with unknown data distribution and histogram as input value. |
| 206 | Used for latency""" |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 207 | def __init__(self, data: numpy.array, second_axis_size: int) -> None: |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 208 | self.second_axis_size = second_axis_size |
| 209 | StatProps.__init__(self, data) |
| 210 | |
| 211 | |
| 212 | class NormStatProps(StatProps): |
| 213 | "Statistic properties for timeseries with normal data distribution. Used for iops/bw" |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 214 | def __init__(self, data: numpy.array) -> None: |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 215 | StatProps.__init__(self, data) |
| 216 | |
| 217 | self.average = None # type: float |
| 218 | self.deviation = None # type: float |
| 219 | self.confidence = None # type: float |
| 220 | self.confidence_level = None # type: float |
| 221 | self.normtest = None # type: NormaltestResult |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 222 | self.skew = None # type: float |
| 223 | self.kurt = None # type: float |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 224 | |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 225 | def __str__(self) -> str: |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 226 | res = ["NormStatProps(size = {}):".format(len(self.data)), |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 227 | " distr = {} ~ {}".format(round_digits(self.average), round_digits(self.deviation)), |
| 228 | " confidence({0.confidence_level}) = {1}".format(self, round_digits(self.confidence)), |
| 229 | " perc_50 = {}".format(round_digits(self.perc_50)), |
| 230 | " perc_90 = {}".format(round_digits(self.perc_90)), |
| 231 | " perc_95 = {}".format(round_digits(self.perc_95)), |
| 232 | " perc_99 = {}".format(round_digits(self.perc_99)), |
| 233 | " range {} {}".format(round_digits(self.min), round_digits(self.max)), |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 234 | " normtest = {0.normtest}".format(self), |
| 235 | " skew ~ kurt = {0.skew} ~ {0.kurt}".format(self)] |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 236 | return "\n".join(res) |
| 237 | |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 238 | def raw(self) -> Dict[str, Any]: |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 239 | data = self.__dict__.copy() |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 240 | data['normtest'] = (data['nortest'].statistic, data['nortest'].pvalue) |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 241 | del data['data'] |
| 242 | data['bins_mids'] = list(self.bins_mids) |
| 243 | data['bins_populations'] = list(self.bins_populations) |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 244 | return data |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 245 | |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 246 | @classmethod |
| 247 | def fromraw(cls, data: Dict[str, Any]) -> 'NormStatProps': |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 248 | data['normtest'] = NormaltestResult(*data['normtest']) |
| 249 | obj = StatProps.fromraw(data) |
| 250 | obj.__class__ = cls |
| 251 | return cast('NormStatProps', obj) |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 252 | |
| 253 | |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 254 | JobStatMetrics = Dict[Tuple[str, str, str], StatProps] |
| 255 | |
| 256 | |
| 257 | class TestJobResult: |
| 258 | """Contains done test job information""" |
| 259 | |
| 260 | def __init__(self, |
| 261 | info: TestJobConfig, |
| 262 | begin_time: int, |
| 263 | end_time: int, |
| 264 | raw: JobMetrics) -> None: |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 265 | self.info = info |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame] | 266 | self.run_interval = (begin_time, end_time) |
| 267 | self.raw = raw # type: JobMetrics |
| 268 | self.processed = None # type: JobStatMetrics |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame^] | 269 | |
| 270 | |
| 271 | class IResultStorage(metaclass=abc.ABCMeta): |
| 272 | |
| 273 | @abc.abstractmethod |
| 274 | def sync(self) -> None: |
| 275 | pass |
| 276 | |
| 277 | @abc.abstractmethod |
| 278 | def put_or_check_suite(self, suite: TestSuiteConfig) -> None: |
| 279 | pass |
| 280 | |
| 281 | @abc.abstractmethod |
| 282 | def put_job(self, suite: TestSuiteConfig, job: TestJobConfig) -> None: |
| 283 | pass |
| 284 | |
| 285 | @abc.abstractmethod |
| 286 | def put_ts(self, ts: TimeSeries) -> None: |
| 287 | pass |
| 288 | |
| 289 | @abc.abstractmethod |
| 290 | def put_extra(self, data: bytes, source: DataSource) -> None: |
| 291 | pass |
| 292 | |
| 293 | @abc.abstractmethod |
| 294 | def put_stat(self, data: StatProps, source: DataSource) -> None: |
| 295 | pass |
| 296 | |
| 297 | @abc.abstractmethod |
| 298 | def get_stat(self, stat_cls: Type[StatProps], source: DataSource) -> StatProps: |
| 299 | pass |
| 300 | |
| 301 | @abc.abstractmethod |
| 302 | def iter_suite(self, suite_type: str = None) -> Iterator[TestSuiteConfig]: |
| 303 | pass |
| 304 | |
| 305 | @abc.abstractmethod |
| 306 | def iter_job(self, suite: TestSuiteConfig) -> Iterator[TestJobConfig]: |
| 307 | pass |
| 308 | |
| 309 | @abc.abstractmethod |
| 310 | def iter_ts(self, suite: TestSuiteConfig, job: TestJobConfig) -> Iterator[TimeSeries]: |
| 311 | pass |
| 312 | |
| 313 | # return path to file to be inserted into report |
| 314 | @abc.abstractmethod |
| 315 | def put_plot_file(self, data: bytes, source: DataSource) -> str: |
| 316 | pass |