blob: 43ae72185b588c63e3aff5d067005b6074f51fbb [file] [log] [blame]
koder aka kdanilov108ac362017-01-19 20:17:16 +02001import abc
koder aka kdanilov108ac362017-01-19 20:17:16 +02002from typing import Dict, List, Any, Optional, Tuple, cast, Type, Iterator
koder aka kdanilovf2865172016-12-30 03:35:11 +02003
koder aka kdanilovffaf48d2016-12-27 02:25:29 +02004
5import numpy
koder aka kdanilovf2865172016-12-30 03:35:11 +02006from scipy.stats.mstats_basic import NormaltestResult
koder aka kdanilovffaf48d2016-12-27 02:25:29 +02007
koder aka kdanilovf90de852017-01-20 18:12:27 +02008from .suits.job import JobConfig
koder aka kdanilovf2865172016-12-30 03:35:11 +02009from .node_interfaces import IRPCNode
koder aka kdanilova732a602017-02-01 20:29:56 +020010from .common_types import Storable
koder aka kdanilovf2865172016-12-30 03:35:11 +020011from .utils import round_digits, Number
koder aka kdanilov70227062016-11-26 23:23:21 +020012
13
koder aka kdanilovf90de852017-01-20 18:12:27 +020014class SuiteConfig(Storable):
koder aka kdanilovf2865172016-12-30 03:35:11 +020015 """
16 Test suite input configuration.
17
18 test_type - test type name
19 params - parameters from yaml file for this test
20 run_uuid - UUID to be used to create file names & Co
21 nodes - nodes to run tests on
22 remote_dir - directory on nodes to be used for local files
23 """
koder aka kdanilovf90de852017-01-20 18:12:27 +020024 __ignore_fields__ = ['nodes', 'run_uuid', 'remote_dir']
25
koder aka kdanilovf2865172016-12-30 03:35:11 +020026 def __init__(self,
27 test_type: str,
28 params: Dict[str, Any],
29 run_uuid: str,
30 nodes: List[IRPCNode],
koder aka kdanilov108ac362017-01-19 20:17:16 +020031 remote_dir: str,
koder aka kdanilova732a602017-02-01 20:29:56 +020032 idx: int,
33 keep_raw_files: bool) -> None:
koder aka kdanilovf2865172016-12-30 03:35:11 +020034 self.test_type = test_type
35 self.params = params
36 self.run_uuid = run_uuid
37 self.nodes = nodes
koder aka kdanilov108ac362017-01-19 20:17:16 +020038 self.nodes_ids = [node.node_id for node in nodes]
koder aka kdanilovf2865172016-12-30 03:35:11 +020039 self.remote_dir = remote_dir
koder aka kdanilova732a602017-02-01 20:29:56 +020040 self.keep_raw_files = keep_raw_files
41
42 if 'load' in self.params:
43 self.storage_id = "{}_{}_{}".format(self.test_type, self.params['load'], idx)
44 else:
45 self.storage_id = "{}_{}".format(self.test_type, idx)
koder aka kdanilovf2865172016-12-30 03:35:11 +020046
koder aka kdanilov108ac362017-01-19 20:17:16 +020047 def __eq__(self, o: object) -> bool:
48 if type(o) is not self.__class__:
49 return False
50
koder aka kdanilovf90de852017-01-20 18:12:27 +020051 other = cast(SuiteConfig, o)
koder aka kdanilov108ac362017-01-19 20:17:16 +020052
koder aka kdanilovf2865172016-12-30 03:35:11 +020053 return (self.test_type == other.test_type and
54 self.params == other.params and
55 set(self.nodes_ids) == set(other.nodes_ids))
56
koder aka kdanilovf2865172016-12-30 03:35:11 +020057
koder aka kdanilov108ac362017-01-19 20:17:16 +020058class DataSource:
59 def __init__(self,
60 suite_id: str = None,
61 job_id: str = None,
62 node_id: str = None,
koder aka kdanilov108ac362017-01-19 20:17:16 +020063 sensor: str = None,
koder aka kdanilova732a602017-02-01 20:29:56 +020064 dev: str = None,
65 metric: str = None,
koder aka kdanilov108ac362017-01-19 20:17:16 +020066 tag: str = None) -> None:
67 self.suite_id = suite_id
68 self.job_id = job_id
69 self.node_id = node_id
koder aka kdanilov108ac362017-01-19 20:17:16 +020070 self.sensor = sensor
koder aka kdanilova732a602017-02-01 20:29:56 +020071 self.dev = dev
72 self.metric = metric
koder aka kdanilov108ac362017-01-19 20:17:16 +020073 self.tag = tag
74
koder aka kdanilova732a602017-02-01 20:29:56 +020075 @property
76 def metric_fqdn(self) -> str:
77 return "{0.sensor}.{0.dev}.{0.metric}".format(self)
78
koder aka kdanilov108ac362017-01-19 20:17:16 +020079 def __call__(self, **kwargs) -> 'DataSource':
80 dct = self.__dict__.copy()
81 dct.update(kwargs)
82 return self.__class__(**dct)
83
84 def __str__(self) -> str:
koder aka kdanilova732a602017-02-01 20:29:56 +020085 return ("suite={0.suite_id},job={0.job_id},node={0.node_id}," +
86 "path={0.sensor}.{0.dev}.{0.metric},tag={0.tag}").format(self)
koder aka kdanilov108ac362017-01-19 20:17:16 +020087
88 def __repr__(self) -> str:
89 return str(self)
90
koder aka kdanilova732a602017-02-01 20:29:56 +020091 @property
92 def tpl(self) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str],
93 Optional[str], Optional[str], Optional[str]]:
94 return self.suite_id, self.job_id, self.node_id, self.sensor, self.dev, self.metric, self.tag
95
96 def __eq__(self, o: object) -> bool:
97 return self.tpl == cast(DataSource, o).tpl
98
99 def __hash__(self) -> int:
100 return hash(self.tpl)
101
koder aka kdanilov108ac362017-01-19 20:17:16 +0200102
koder aka kdanilovf2865172016-12-30 03:35:11 +0200103class TimeSeries:
104 """Data series from sensor - either system sensor or from load generator tool (e.g. fio)"""
105
106 def __init__(self,
107 name: str,
108 raw: Optional[bytes],
koder aka kdanilov108ac362017-01-19 20:17:16 +0200109 data: numpy.array,
110 times: numpy.array,
koder aka kdanilovf90de852017-01-20 18:12:27 +0200111 units: str,
koder aka kdanilova732a602017-02-01 20:29:56 +0200112 source: DataSource,
koder aka kdanilovf90de852017-01-20 18:12:27 +0200113 time_units: str = 'us',
koder aka kdanilova732a602017-02-01 20:29:56 +0200114 raw_tag: str = 'txt') -> None:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200115
116 # Sensor name. Typically DEV_NAME.METRIC
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200117 self.name = name
koder aka kdanilovf2865172016-12-30 03:35:11 +0200118
koder aka kdanilovf90de852017-01-20 18:12:27 +0200119 # units for data
120 self.units = units
121
122 # units for time
123 self.time_units = time_units
124
koder aka kdanilovf2865172016-12-30 03:35:11 +0200125 # Time series times and values. Time in ms from Unix epoch.
koder aka kdanilov108ac362017-01-19 20:17:16 +0200126 self.times = times
127 self.data = data
koder aka kdanilovf2865172016-12-30 03:35:11 +0200128
koder aka kdanilovf2865172016-12-30 03:35:11 +0200129 # Raw sensor data (is provided). Like log file for fio iops/bw/lat.
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200130 self.raw = raw
koder aka kdanilova732a602017-02-01 20:29:56 +0200131 self.raw_tag = raw_tag
koder aka kdanilov108ac362017-01-19 20:17:16 +0200132 self.source = source
133
134 def __str__(self) -> str:
135 res = "TS({}):\n".format(self.name)
136 res += " source={}\n".format(self.source)
137 res += " times_size={}\n".format(len(self.times))
koder aka kdanilova732a602017-02-01 20:29:56 +0200138 res += " data_shape={}\n".format(*self.data.shape)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200139 return res
140
141 def __repr__(self) -> str:
142 return str(self)
koder aka kdanilov70227062016-11-26 23:23:21 +0200143
144
koder aka kdanilovf2865172016-12-30 03:35:11 +0200145# (node_name, source_dev, metric_name) => metric_results
146JobMetrics = Dict[Tuple[str, str, str], TimeSeries]
koder aka kdanilov70227062016-11-26 23:23:21 +0200147
148
koder aka kdanilovf90de852017-01-20 18:12:27 +0200149class StatProps(Storable):
koder aka kdanilovf2865172016-12-30 03:35:11 +0200150 "Statistic properties for timeseries with unknown data distribution"
koder aka kdanilovf90de852017-01-20 18:12:27 +0200151
152 __ignore_fields__ = ['data']
153
koder aka kdanilov108ac362017-01-19 20:17:16 +0200154 def __init__(self, data: numpy.array) -> None:
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200155 self.perc_99 = None # type: float
156 self.perc_95 = None # type: float
157 self.perc_90 = None # type: float
158 self.perc_50 = None # type: float
koder aka kdanilova732a602017-02-01 20:29:56 +0200159 self.perc_10 = None # type: float
160 self.perc_5 = None # type: float
161 self.perc_1 = None # type: float
koder aka kdanilov70227062016-11-26 23:23:21 +0200162
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200163 self.min = None # type: Number
164 self.max = None # type: Number
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200165
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200166 # bin_center: bin_count
koder aka kdanilova732a602017-02-01 20:29:56 +0200167 self.log_bins = False
koder aka kdanilov108ac362017-01-19 20:17:16 +0200168 self.bins_populations = None # type: numpy.array
koder aka kdanilova732a602017-02-01 20:29:56 +0200169
170 # bin edges, one more element that in bins_populations
171 self.bins_edges = None # type: numpy.array
172
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200173 self.data = data
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200174
koder aka kdanilovf2865172016-12-30 03:35:11 +0200175 def __str__(self) -> str:
176 res = ["{}(size = {}):".format(self.__class__.__name__, len(self.data))]
koder aka kdanilova732a602017-02-01 20:29:56 +0200177 for name in ["perc_1", "perc_5", "perc_10", "perc_50", "perc_90", "perc_95", "perc_99"]:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200178 res.append(" {} = {}".format(name, round_digits(getattr(self, name))))
179 res.append(" range {} {}".format(round_digits(self.min), round_digits(self.max)))
180 return "\n".join(res)
181
182 def __repr__(self) -> str:
183 return str(self)
184
185 def raw(self) -> Dict[str, Any]:
koder aka kdanilovf90de852017-01-20 18:12:27 +0200186 data = super().raw()
187 data['bins_mids'] = list(data['bins_mids'])
188 data['bins_populations'] = list(data['bins_populations'])
koder aka kdanilovf2865172016-12-30 03:35:11 +0200189 return data
190
191 @classmethod
192 def fromraw(cls, data: Dict[str, Any]) -> 'StatProps':
koder aka kdanilov108ac362017-01-19 20:17:16 +0200193 data['bins_mids'] = numpy.array(data['bins_mids'])
koder aka kdanilovf2865172016-12-30 03:35:11 +0200194 data['bins_populations'] = numpy.array(data['bins_populations'])
koder aka kdanilovf90de852017-01-20 18:12:27 +0200195 return cast(StatProps, super().fromraw(data))
koder aka kdanilovf2865172016-12-30 03:35:11 +0200196
197
198class HistoStatProps(StatProps):
199 """Statistic properties for 2D timeseries with unknown data distribution and histogram as input value.
200 Used for latency"""
koder aka kdanilova732a602017-02-01 20:29:56 +0200201 def __init__(self, data: numpy.array) -> None:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200202 StatProps.__init__(self, data)
203
204
205class NormStatProps(StatProps):
206 "Statistic properties for timeseries with normal data distribution. Used for iops/bw"
koder aka kdanilov108ac362017-01-19 20:17:16 +0200207 def __init__(self, data: numpy.array) -> None:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200208 StatProps.__init__(self, data)
209
210 self.average = None # type: float
211 self.deviation = None # type: float
212 self.confidence = None # type: float
213 self.confidence_level = None # type: float
214 self.normtest = None # type: NormaltestResult
koder aka kdanilov108ac362017-01-19 20:17:16 +0200215 self.skew = None # type: float
216 self.kurt = None # type: float
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200217
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200218 def __str__(self) -> str:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200219 res = ["NormStatProps(size = {}):".format(len(self.data)),
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200220 " distr = {} ~ {}".format(round_digits(self.average), round_digits(self.deviation)),
221 " confidence({0.confidence_level}) = {1}".format(self, round_digits(self.confidence)),
koder aka kdanilova732a602017-02-01 20:29:56 +0200222 " perc_1 = {}".format(round_digits(self.perc_1)),
223 " perc_5 = {}".format(round_digits(self.perc_5)),
224 " perc_10 = {}".format(round_digits(self.perc_10)),
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200225 " perc_50 = {}".format(round_digits(self.perc_50)),
226 " perc_90 = {}".format(round_digits(self.perc_90)),
227 " perc_95 = {}".format(round_digits(self.perc_95)),
228 " perc_99 = {}".format(round_digits(self.perc_99)),
229 " range {} {}".format(round_digits(self.min), round_digits(self.max)),
koder aka kdanilov108ac362017-01-19 20:17:16 +0200230 " normtest = {0.normtest}".format(self),
231 " skew ~ kurt = {0.skew} ~ {0.kurt}".format(self)]
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200232 return "\n".join(res)
233
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200234 def raw(self) -> Dict[str, Any]:
koder aka kdanilovf90de852017-01-20 18:12:27 +0200235 data = super().raw()
koder aka kdanilovf2865172016-12-30 03:35:11 +0200236 data['normtest'] = (data['nortest'].statistic, data['nortest'].pvalue)
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200237 return data
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200238
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200239 @classmethod
240 def fromraw(cls, data: Dict[str, Any]) -> 'NormStatProps':
koder aka kdanilovf2865172016-12-30 03:35:11 +0200241 data['normtest'] = NormaltestResult(*data['normtest'])
koder aka kdanilovf90de852017-01-20 18:12:27 +0200242 return cast(NormStatProps, super().fromraw(data))
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200243
244
koder aka kdanilovf2865172016-12-30 03:35:11 +0200245JobStatMetrics = Dict[Tuple[str, str, str], StatProps]
246
247
koder aka kdanilovf90de852017-01-20 18:12:27 +0200248class JobResult:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200249 """Contains done test job information"""
250
251 def __init__(self,
koder aka kdanilovf90de852017-01-20 18:12:27 +0200252 info: JobConfig,
koder aka kdanilovf2865172016-12-30 03:35:11 +0200253 begin_time: int,
254 end_time: int,
255 raw: JobMetrics) -> None:
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200256 self.info = info
koder aka kdanilovf2865172016-12-30 03:35:11 +0200257 self.run_interval = (begin_time, end_time)
258 self.raw = raw # type: JobMetrics
259 self.processed = None # type: JobStatMetrics
koder aka kdanilov108ac362017-01-19 20:17:16 +0200260
261
262class IResultStorage(metaclass=abc.ABCMeta):
263
264 @abc.abstractmethod
265 def sync(self) -> None:
266 pass
267
268 @abc.abstractmethod
koder aka kdanilova732a602017-02-01 20:29:56 +0200269 def load_sensor(self, ds: DataSource) -> TimeSeries:
270 pass
271
272 @abc.abstractmethod
koder aka kdanilovf90de852017-01-20 18:12:27 +0200273 def put_or_check_suite(self, suite: SuiteConfig) -> None:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200274 pass
275
276 @abc.abstractmethod
koder aka kdanilovf90de852017-01-20 18:12:27 +0200277 def put_job(self, suite: SuiteConfig, job: JobConfig) -> None:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200278 pass
279
280 @abc.abstractmethod
281 def put_ts(self, ts: TimeSeries) -> None:
282 pass
283
284 @abc.abstractmethod
285 def put_extra(self, data: bytes, source: DataSource) -> None:
286 pass
287
288 @abc.abstractmethod
289 def put_stat(self, data: StatProps, source: DataSource) -> None:
290 pass
291
292 @abc.abstractmethod
293 def get_stat(self, stat_cls: Type[StatProps], source: DataSource) -> StatProps:
294 pass
295
296 @abc.abstractmethod
koder aka kdanilovf90de852017-01-20 18:12:27 +0200297 def iter_suite(self, suite_type: str = None) -> Iterator[SuiteConfig]:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200298 pass
299
300 @abc.abstractmethod
koder aka kdanilovf90de852017-01-20 18:12:27 +0200301 def iter_job(self, suite: SuiteConfig) -> Iterator[JobConfig]:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200302 pass
303
304 @abc.abstractmethod
koder aka kdanilovf90de852017-01-20 18:12:27 +0200305 def iter_ts(self, suite: SuiteConfig, job: JobConfig) -> Iterator[TimeSeries]:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200306 pass
307
308 # return path to file to be inserted into report
309 @abc.abstractmethod
310 def put_plot_file(self, data: bytes, source: DataSource) -> str:
311 pass