blob: 1a148edc49c40c67127030e581e435cfa2db85de [file] [log] [blame]
koder aka kdanilov108ac362017-01-19 20:17:16 +02001import abc
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +02002import array
koder aka kdanilov108ac362017-01-19 20:17:16 +02003from typing import Dict, List, Any, Optional, Tuple, cast, Type, Iterator
4from collections import OrderedDict
koder aka kdanilovf2865172016-12-30 03:35:11 +02005
koder aka kdanilovffaf48d2016-12-27 02:25:29 +02006
7import numpy
koder aka kdanilovf2865172016-12-30 03:35:11 +02008from scipy.stats.mstats_basic import NormaltestResult
koder aka kdanilovffaf48d2016-12-27 02:25:29 +02009
10
koder aka kdanilovf2865172016-12-30 03:35:11 +020011from .node_interfaces import IRPCNode
koder aka kdanilov108ac362017-01-19 20:17:16 +020012from .common_types import IStorable, Storable
koder aka kdanilovf2865172016-12-30 03:35:11 +020013from .utils import round_digits, Number
koder aka kdanilov70227062016-11-26 23:23:21 +020014
15
koder aka kdanilov108ac362017-01-19 20:17:16 +020016class 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 kdanilov70227062016-11-26 23:23:21 +020037
koder aka kdanilovf2865172016-12-30 03:35:11 +020038
39class 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 kdanilov108ac362017-01-19 20:17:16 +020054 remote_dir: str,
55 idx: int) -> None:
koder aka kdanilovf2865172016-12-30 03:35:11 +020056 self.test_type = test_type
57 self.params = params
58 self.run_uuid = run_uuid
59 self.nodes = nodes
koder aka kdanilov108ac362017-01-19 20:17:16 +020060 self.nodes_ids = [node.node_id for node in nodes]
koder aka kdanilovf2865172016-12-30 03:35:11 +020061 self.remote_dir = remote_dir
koder aka kdanilov108ac362017-01-19 20:17:16 +020062 self.storage_id = "{}_{}".format(self.test_type, idx)
koder aka kdanilovf2865172016-12-30 03:35:11 +020063
koder aka kdanilov108ac362017-01-19 20:17:16 +020064 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 kdanilovf2865172016-12-30 03:35:11 +020070 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 kdanilov108ac362017-01-19 20:17:16 +020092class 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 kdanilovf2865172016-12-30 03:35:11 +0200119class 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 kdanilov108ac362017-01-19 20:17:16 +0200125 data: numpy.array,
126 times: numpy.array,
koder aka kdanilovf2865172016-12-30 03:35:11 +0200127 second_axis_size: int = 1,
koder aka kdanilov108ac362017-01-19 20:17:16 +0200128 source: DataSource = None) -> None:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200129
130 # Sensor name. Typically DEV_NAME.METRIC
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200131 self.name = name
koder aka kdanilovf2865172016-12-30 03:35:11 +0200132
133 # Time series times and values. Time in ms from Unix epoch.
koder aka kdanilov108ac362017-01-19 20:17:16 +0200134 self.times = times
135 self.data = data
koder aka kdanilovf2865172016-12-30 03:35:11 +0200136
137 # Not equal to 1 in case of 2d sensors, like latency, when each measurement is a histogram.
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200138 self.second_axis_size = second_axis_size
koder aka kdanilovf2865172016-12-30 03:35:11 +0200139
140 # Raw sensor data (is provided). Like log file for fio iops/bw/lat.
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200141 self.raw = raw
koder aka kdanilov70227062016-11-26 23:23:21 +0200142
koder aka kdanilov108ac362017-01-19 20:17:16 +0200143 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 kdanilov70227062016-11-26 23:23:21 +0200155
156
koder aka kdanilovf2865172016-12-30 03:35:11 +0200157# (node_name, source_dev, metric_name) => metric_results
158JobMetrics = Dict[Tuple[str, str, str], TimeSeries]
koder aka kdanilov70227062016-11-26 23:23:21 +0200159
160
koder aka kdanilovf2865172016-12-30 03:35:11 +0200161class StatProps(IStorable):
162 "Statistic properties for timeseries with unknown data distribution"
koder aka kdanilov108ac362017-01-19 20:17:16 +0200163 def __init__(self, data: numpy.array) -> None:
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200164 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 kdanilov70227062016-11-26 23:23:21 +0200168
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200169 self.min = None # type: Number
170 self.max = None # type: Number
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200171
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200172 # bin_center: bin_count
koder aka kdanilov108ac362017-01-19 20:17:16 +0200173 self.bins_populations = None # type: numpy.array
174 self.bins_mids = None # type: numpy.array
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200175 self.data = data
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200176
koder aka kdanilovf2865172016-12-30 03:35:11 +0200177 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 kdanilov108ac362017-01-19 20:17:16 +0200189 del data['data']
190 data['bins_mids'] = list(self.bins_mids)
koder aka kdanilovf2865172016-12-30 03:35:11 +0200191 data['bins_populations'] = list(self.bins_populations)
192 return data
193
194 @classmethod
195 def fromraw(cls, data: Dict[str, Any]) -> 'StatProps':
koder aka kdanilov108ac362017-01-19 20:17:16 +0200196 data['bins_mids'] = numpy.array(data['bins_mids'])
koder aka kdanilovf2865172016-12-30 03:35:11 +0200197 data['bins_populations'] = numpy.array(data['bins_populations'])
koder aka kdanilov108ac362017-01-19 20:17:16 +0200198 data['data'] = None
koder aka kdanilovf2865172016-12-30 03:35:11 +0200199 res = cls.__new__(cls)
200 res.__dict__.update(data)
201 return res
202
203
204class HistoStatProps(StatProps):
205 """Statistic properties for 2D timeseries with unknown data distribution and histogram as input value.
206 Used for latency"""
koder aka kdanilov108ac362017-01-19 20:17:16 +0200207 def __init__(self, data: numpy.array, second_axis_size: int) -> None:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200208 self.second_axis_size = second_axis_size
209 StatProps.__init__(self, data)
210
211
212class NormStatProps(StatProps):
213 "Statistic properties for timeseries with normal data distribution. Used for iops/bw"
koder aka kdanilov108ac362017-01-19 20:17:16 +0200214 def __init__(self, data: numpy.array) -> None:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200215 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 kdanilov108ac362017-01-19 20:17:16 +0200222 self.skew = None # type: float
223 self.kurt = None # type: float
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200224
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200225 def __str__(self) -> str:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200226 res = ["NormStatProps(size = {}):".format(len(self.data)),
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200227 " 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 kdanilov108ac362017-01-19 20:17:16 +0200234 " normtest = {0.normtest}".format(self),
235 " skew ~ kurt = {0.skew} ~ {0.kurt}".format(self)]
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200236 return "\n".join(res)
237
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200238 def raw(self) -> Dict[str, Any]:
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200239 data = self.__dict__.copy()
koder aka kdanilovf2865172016-12-30 03:35:11 +0200240 data['normtest'] = (data['nortest'].statistic, data['nortest'].pvalue)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200241 del data['data']
242 data['bins_mids'] = list(self.bins_mids)
243 data['bins_populations'] = list(self.bins_populations)
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200244 return data
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200245
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200246 @classmethod
247 def fromraw(cls, data: Dict[str, Any]) -> 'NormStatProps':
koder aka kdanilovf2865172016-12-30 03:35:11 +0200248 data['normtest'] = NormaltestResult(*data['normtest'])
249 obj = StatProps.fromraw(data)
250 obj.__class__ = cls
251 return cast('NormStatProps', obj)
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200252
253
koder aka kdanilovf2865172016-12-30 03:35:11 +0200254JobStatMetrics = Dict[Tuple[str, str, str], StatProps]
255
256
257class 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 kdanilovffaf48d2016-12-27 02:25:29 +0200265 self.info = info
koder aka kdanilovf2865172016-12-30 03:35:11 +0200266 self.run_interval = (begin_time, end_time)
267 self.raw = raw # type: JobMetrics
268 self.processed = None # type: JobStatMetrics
koder aka kdanilov108ac362017-01-19 20:17:16 +0200269
270
271class 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