koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 1 | import array |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame^] | 2 | from typing import Dict, List, Any, Optional, Tuple, cast |
| 3 | |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 4 | |
| 5 | import numpy |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame^] | 6 | from scipy.stats.mstats_basic import NormaltestResult |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 7 | |
| 8 | |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame^] | 9 | from .node_interfaces import IRPCNode |
| 10 | from .istorable import IStorable, Storable |
| 11 | from .utils import round_digits, Number |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 12 | |
| 13 | |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame^] | 14 | class TestJobConfig(Storable): |
| 15 | def __init__(self) -> None: |
| 16 | self.summary = None # type: str |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 17 | |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame^] | 18 | |
| 19 | class TestSuiteConfig(IStorable): |
| 20 | """ |
| 21 | Test suite input configuration. |
| 22 | |
| 23 | test_type - test type name |
| 24 | params - parameters from yaml file for this test |
| 25 | run_uuid - UUID to be used to create file names & Co |
| 26 | nodes - nodes to run tests on |
| 27 | remote_dir - directory on nodes to be used for local files |
| 28 | """ |
| 29 | def __init__(self, |
| 30 | test_type: str, |
| 31 | params: Dict[str, Any], |
| 32 | run_uuid: str, |
| 33 | nodes: List[IRPCNode], |
| 34 | remote_dir: str) -> None: |
| 35 | self.test_type = test_type |
| 36 | self.params = params |
| 37 | self.run_uuid = run_uuid |
| 38 | self.nodes = nodes |
| 39 | self.nodes_ids = [node.info.node_id() for node in nodes] |
| 40 | self.remote_dir = remote_dir |
| 41 | |
| 42 | def __eq__(self, other: 'TestSuiteConfig') -> bool: |
| 43 | return (self.test_type == other.test_type and |
| 44 | self.params == other.params and |
| 45 | set(self.nodes_ids) == set(other.nodes_ids)) |
| 46 | |
| 47 | def raw(self) -> Dict[str, Any]: |
| 48 | res = self.__dict__.copy() |
| 49 | del res['nodes'] |
| 50 | del res['run_uuid'] |
| 51 | del res['remote_dir'] |
| 52 | return res |
| 53 | |
| 54 | @classmethod |
| 55 | def fromraw(cls, data: Dict[str, Any]) -> 'IStorable': |
| 56 | obj = cls.__new__(cls) |
| 57 | data = data.copy() |
| 58 | data['nodes'] = None |
| 59 | data['run_uuid'] = None |
| 60 | data['remote_dir'] = None |
| 61 | obj.__dict__.update(data) |
| 62 | return obj |
| 63 | |
| 64 | |
| 65 | class TimeSeries: |
| 66 | """Data series from sensor - either system sensor or from load generator tool (e.g. fio)""" |
| 67 | |
| 68 | def __init__(self, |
| 69 | name: str, |
| 70 | raw: Optional[bytes], |
| 71 | data: array.array, |
| 72 | times: array.array, |
| 73 | second_axis_size: int = 1, |
| 74 | bins_edges: List[float] = None) -> None: |
| 75 | |
| 76 | # Sensor name. Typically DEV_NAME.METRIC |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 77 | self.name = name |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame^] | 78 | |
| 79 | # Time series times and values. Time in ms from Unix epoch. |
| 80 | self.times = times # type: List[int] |
| 81 | self.data = data # type: List[int] |
| 82 | |
| 83 | # 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] | 84 | self.second_axis_size = second_axis_size |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame^] | 85 | |
| 86 | # 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] | 87 | self.raw = raw |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 88 | |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame^] | 89 | # bin edges for historgam timeseries |
| 90 | self.bins_edges = bins_edges |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 91 | |
| 92 | |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame^] | 93 | # (node_name, source_dev, metric_name) => metric_results |
| 94 | JobMetrics = Dict[Tuple[str, str, str], TimeSeries] |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 95 | |
| 96 | |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame^] | 97 | class StatProps(IStorable): |
| 98 | "Statistic properties for timeseries with unknown data distribution" |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 99 | def __init__(self, data: List[Number]) -> None: |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 100 | self.perc_99 = None # type: float |
| 101 | self.perc_95 = None # type: float |
| 102 | self.perc_90 = None # type: float |
| 103 | self.perc_50 = None # type: float |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 104 | |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 105 | self.min = None # type: Number |
| 106 | self.max = None # type: Number |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 107 | |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 108 | # bin_center: bin_count |
| 109 | self.bins_populations = None # type: List[int] |
| 110 | self.bins_edges = None # type: List[float] |
| 111 | self.data = data |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 112 | |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame^] | 113 | def __str__(self) -> str: |
| 114 | res = ["{}(size = {}):".format(self.__class__.__name__, len(self.data))] |
| 115 | for name in ["perc_50", "perc_90", "perc_95", "perc_99"]: |
| 116 | res.append(" {} = {}".format(name, round_digits(getattr(self, name)))) |
| 117 | res.append(" range {} {}".format(round_digits(self.min), round_digits(self.max))) |
| 118 | return "\n".join(res) |
| 119 | |
| 120 | def __repr__(self) -> str: |
| 121 | return str(self) |
| 122 | |
| 123 | def raw(self) -> Dict[str, Any]: |
| 124 | data = self.__dict__.copy() |
| 125 | data['bins_edges'] = list(self.bins_edges) |
| 126 | data['bins_populations'] = list(self.bins_populations) |
| 127 | return data |
| 128 | |
| 129 | @classmethod |
| 130 | def fromraw(cls, data: Dict[str, Any]) -> 'StatProps': |
| 131 | data['bins_edges'] = numpy.array(data['bins_edges']) |
| 132 | data['bins_populations'] = numpy.array(data['bins_populations']) |
| 133 | res = cls.__new__(cls) |
| 134 | res.__dict__.update(data) |
| 135 | return res |
| 136 | |
| 137 | |
| 138 | class HistoStatProps(StatProps): |
| 139 | """Statistic properties for 2D timeseries with unknown data distribution and histogram as input value. |
| 140 | Used for latency""" |
| 141 | def __init__(self, data: List[Number], second_axis_size: int) -> None: |
| 142 | self.second_axis_size = second_axis_size |
| 143 | StatProps.__init__(self, data) |
| 144 | |
| 145 | |
| 146 | class NormStatProps(StatProps): |
| 147 | "Statistic properties for timeseries with normal data distribution. Used for iops/bw" |
| 148 | def __init__(self, data: List[Number]) -> None: |
| 149 | StatProps.__init__(self, data) |
| 150 | |
| 151 | self.average = None # type: float |
| 152 | self.deviation = None # type: float |
| 153 | self.confidence = None # type: float |
| 154 | self.confidence_level = None # type: float |
| 155 | self.normtest = None # type: NormaltestResult |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 156 | |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 157 | def __str__(self) -> str: |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame^] | 158 | res = ["NormStatProps(size = {}):".format(len(self.data)), |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 159 | " distr = {} ~ {}".format(round_digits(self.average), round_digits(self.deviation)), |
| 160 | " confidence({0.confidence_level}) = {1}".format(self, round_digits(self.confidence)), |
| 161 | " perc_50 = {}".format(round_digits(self.perc_50)), |
| 162 | " perc_90 = {}".format(round_digits(self.perc_90)), |
| 163 | " perc_95 = {}".format(round_digits(self.perc_95)), |
| 164 | " perc_99 = {}".format(round_digits(self.perc_99)), |
| 165 | " range {} {}".format(round_digits(self.min), round_digits(self.max)), |
| 166 | " normtest = {0.normtest}".format(self)] |
| 167 | return "\n".join(res) |
| 168 | |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 169 | def raw(self) -> Dict[str, Any]: |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 170 | data = self.__dict__.copy() |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame^] | 171 | data['normtest'] = (data['nortest'].statistic, data['nortest'].pvalue) |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 172 | data['bins_edges'] = list(self.bins_edges) |
| 173 | return data |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 174 | |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 175 | @classmethod |
| 176 | def fromraw(cls, data: Dict[str, Any]) -> 'NormStatProps': |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame^] | 177 | data['normtest'] = NormaltestResult(*data['normtest']) |
| 178 | obj = StatProps.fromraw(data) |
| 179 | obj.__class__ = cls |
| 180 | return cast('NormStatProps', obj) |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 181 | |
| 182 | |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame^] | 183 | JobStatMetrics = Dict[Tuple[str, str, str], StatProps] |
| 184 | |
| 185 | |
| 186 | class TestJobResult: |
| 187 | """Contains done test job information""" |
| 188 | |
| 189 | def __init__(self, |
| 190 | info: TestJobConfig, |
| 191 | begin_time: int, |
| 192 | end_time: int, |
| 193 | raw: JobMetrics) -> None: |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 194 | self.info = info |
koder aka kdanilov | f286517 | 2016-12-30 03:35:11 +0200 | [diff] [blame^] | 195 | self.run_interval = (begin_time, end_time) |
| 196 | self.raw = raw # type: JobMetrics |
| 197 | self.processed = None # type: JobStatMetrics |