koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 1 | import os |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 2 | import abc |
koder aka kdanilov | a047e1b | 2015-04-21 23:16:59 +0300 | [diff] [blame] | 3 | import logging |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 4 | import collections |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 5 | from collections import defaultdict |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 6 | from typing import Dict, Any, Iterator, Tuple, cast, List, Set, Optional, Union, Type, Iterable |
koder aka kdanilov | cff7b2e | 2015-04-18 20:48:15 +0300 | [diff] [blame] | 7 | |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 8 | import numpy |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 9 | import scipy.stats |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 10 | from statsmodels.tsa.stattools import adfuller |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 11 | |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 12 | import xmlbuilder3 |
koder aka kdanilov | be8f89f | 2015-04-28 14:51:51 +0300 | [diff] [blame] | 13 | |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 14 | import wally |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 15 | |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 16 | from cephlib import html |
| 17 | from cephlib.units import b2ssize, b2ssize_10, unit_conversion_coef, unit_conversion_coef_f |
| 18 | from cephlib.statistic import calc_norm_stat_props |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 19 | from cephlib.storage_selectors import sum_sensors, find_sensors_to_2d, update_storage_selector, DevRoles |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 20 | from cephlib.wally_storage import find_nodes_by_roles |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 21 | from cephlib.plot import (plot_simple_bars, plot_hmap_from_2d, plot_lat_over_time, plot_simple_over_time, |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 22 | plot_histo_heatmap, plot_v_over_time, plot_hist, plot_dots_with_regression) |
| 23 | from cephlib.numeric_types import ndarray2d |
| 24 | from cephlib.node import NodeRole |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 25 | |
| 26 | from .utils import STORAGE_ROLES |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 27 | from .stage import Stage, StepOrder |
| 28 | from .test_run_class import TestRun |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 29 | from .result_classes import IWallyStorage |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 30 | from .result_classes import DataSource, TimeSeries, SuiteConfig |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 31 | from .suits.io.fio import FioTest, FioJobConfig |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 32 | from .suits.io.fio_job import FioJobParams |
| 33 | from .suits.job import JobConfig |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 34 | from .data_selectors import get_aggregated, AGG_TAG |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 35 | from .report_profiles import (DefStyleProfile, DefColorProfile, StyleProfile, ColorProfile, |
| 36 | default_format, io_chart_format) |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 37 | from .plot import io_chart |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 38 | from .resources import ResourceNames, get_resources_usage, make_iosum, get_cluster_cpu_load |
| 39 | from .console_report import get_console_report_table, console_report_headers, console_report_align, Texttable |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 40 | |
| 41 | |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 42 | logger = logging.getLogger("wally") |
koder aka kdanilov | a047e1b | 2015-04-21 23:16:59 +0300 | [diff] [blame] | 43 | |
| 44 | |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 45 | # ---------------- CONSTS --------------------------------------------------------------------------------------------- |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 46 | |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 47 | |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 48 | DEBUG = False |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 49 | |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 50 | |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 51 | # -------------- AGGREGATION AND STAT FUNCTIONS ---------------------------------------------------------------------- |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 52 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 53 | LEVEL_SENSORS = {("block-io", "io_queue"), ("system-cpu", "procs_blocked"), ("system-cpu", "procs_queue")} |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 54 | |
| 55 | |
| 56 | def is_level_sensor(sensor: str, metric: str) -> bool: |
| 57 | """Returns True if sensor measure level of any kind, E.g. queue depth.""" |
| 58 | return (sensor, metric) in LEVEL_SENSORS |
| 59 | |
| 60 | |
| 61 | def is_delta_sensor(sensor: str, metric: str) -> bool: |
| 62 | """Returns True if sensor provides deltas for cumulative value. E.g. io completed in given period""" |
| 63 | return not is_level_sensor(sensor, metric) |
| 64 | |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 65 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 66 | # def get_idle_load(rstorage: ResultStorage, *args, **kwargs) -> float: |
| 67 | # if 'idle' not in rstorage.storage: |
| 68 | # return 0.0 |
| 69 | # idle_time = rstorage.storage.get('idle') |
| 70 | # ssum = summ_sensors(rstorage, time_range=idle_time, *args, **kwargs) |
| 71 | # return numpy.average(ssum) |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 72 | |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 73 | |
| 74 | # -------------------- REPORT HELPERS -------------------------------------------------------------------------------- |
| 75 | |
| 76 | |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 77 | class HTMLBlock: |
| 78 | data = None # type: str |
| 79 | js_links = [] # type: List[str] |
| 80 | css_links = [] # type: List[str] |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 81 | order_attr = None # type: Any |
| 82 | |
| 83 | def __init__(self, data: str, order_attr: Any = None) -> None: |
| 84 | self.data = data |
| 85 | self.order_attr = order_attr |
| 86 | |
kdanylov aka koder | 4518318 | 2017-04-30 23:55:40 +0300 | [diff] [blame] | 87 | def __eq__(self, o: Any) -> bool: |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 88 | return o.order_attr == self.order_attr # type: ignore |
| 89 | |
kdanylov aka koder | 4518318 | 2017-04-30 23:55:40 +0300 | [diff] [blame] | 90 | def __lt__(self, o: Any) -> bool: |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 91 | return o.order_attr > self.order_attr # type: ignore |
| 92 | |
| 93 | |
| 94 | class Table: |
| 95 | def __init__(self, header: List[str]) -> None: |
| 96 | self.header = header |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 97 | self.data = [] # type: List[List[str]] |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 98 | |
| 99 | def add_line(self, values: List[str]) -> None: |
| 100 | self.data.append(values) |
| 101 | |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 102 | def html(self) -> str: |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 103 | return html.table("", self.header, self.data) |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 104 | |
| 105 | |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 106 | class Menu1st: |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 107 | summary = "Summary" |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 108 | per_job = "Per Job" |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 109 | engineering = "Engineering" |
| 110 | engineering_per_job = "Engineering per job" |
| 111 | order = [summary, per_job, engineering, engineering_per_job] |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 112 | |
| 113 | |
| 114 | class Menu2ndEng: |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 115 | summary = "Summary" |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 116 | iops_time = "IOPS(time)" |
| 117 | hist = "IOPS/lat overall histogram" |
| 118 | lat_time = "Lat(time)" |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 119 | resource_regression = "Resource usage LR" |
| 120 | order = [summary, iops_time, hist, lat_time, resource_regression] |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 121 | |
| 122 | |
| 123 | class Menu2ndSumm: |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 124 | summary = "Summary" |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 125 | io_lat_qd = "IO & Lat vs QD" |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 126 | resources_usage_qd = "Resource usage" |
| 127 | order = [summary, io_lat_qd, resources_usage_qd] |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 128 | |
| 129 | |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 130 | menu_1st_order = [Menu1st.summary, Menu1st.engineering, Menu1st.per_job, Menu1st.engineering_per_job] |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 131 | |
| 132 | |
| 133 | # -------------------- REPORTS -------------------------------------------------------------------------------------- |
| 134 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 135 | class ReporterBase: |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 136 | def __init__(self, rstorage: IWallyStorage, style: StyleProfile, colors: ColorProfile) -> None: |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 137 | self.style = style |
| 138 | self.colors = colors |
| 139 | self.rstorage = rstorage |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 140 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 141 | def plt(self, func, ds: DataSource, *args, **kwargs) -> str: |
| 142 | return func(self.rstorage, self.style, self.colors, ds, *args, **kwargs) |
| 143 | |
| 144 | |
| 145 | class SuiteReporter(ReporterBase, metaclass=abc.ABCMeta): |
| 146 | suite_types = set() # type: Set[str] |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 147 | |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 148 | @abc.abstractmethod |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 149 | def get_divs(self, suite: SuiteConfig) -> Iterator[Tuple[str, str, HTMLBlock]]: |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 150 | pass |
| 151 | |
| 152 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 153 | class JobReporter(ReporterBase, metaclass=abc.ABCMeta): |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 154 | suite_type = set() # type: Set[str] |
| 155 | |
| 156 | @abc.abstractmethod |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 157 | def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]: |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 158 | pass |
| 159 | |
| 160 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 161 | # # Linearization report |
| 162 | # class IOPSBsize(SuiteReporter): |
| 163 | # """Creates graphs, which show how IOPS and Latency depend on block size""" |
| 164 | # |
| 165 | # |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 166 | |
| 167 | |
| 168 | class StoragePerfSummary: |
| 169 | iops_units = "KiBps" |
| 170 | bw_units = "Bps" |
| 171 | NO_VAL = -1 |
| 172 | |
| 173 | def __init__(self) -> None: |
kdanylov aka koder | 13e5845 | 2018-07-15 02:51:51 +0300 | [diff] [blame^] | 174 | self.rw_iops_10ms = self.NO_VAL |
| 175 | self.rw_iops_30ms = self.NO_VAL |
| 176 | self.rw_iops_100ms = self.NO_VAL |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 177 | |
kdanylov aka koder | 13e5845 | 2018-07-15 02:51:51 +0300 | [diff] [blame^] | 178 | self.rr_iops_10ms = self.NO_VAL |
| 179 | self.rr_iops_30ms = self.NO_VAL |
| 180 | self.rr_iops_100ms = self.NO_VAL |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 181 | |
kdanylov aka koder | 13e5845 | 2018-07-15 02:51:51 +0300 | [diff] [blame^] | 182 | self.bw_write_max = self.NO_VAL |
| 183 | self.bw_read_max = self.NO_VAL |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 184 | |
kdanylov aka koder | 13e5845 | 2018-07-15 02:51:51 +0300 | [diff] [blame^] | 185 | self.bw: Optional[float] = None |
| 186 | self.read_iops: Optional[float] = None |
| 187 | self.write_iops: Optional[float] = None |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 188 | |
| 189 | |
| 190 | def get_performance_summary(storage: IWallyStorage, suite: SuiteConfig, |
| 191 | hboxes: int, large_blocks: int) -> Tuple[StoragePerfSummary, StoragePerfSummary]: |
| 192 | |
| 193 | psum95 = StoragePerfSummary() |
| 194 | psum50 = StoragePerfSummary() |
| 195 | |
| 196 | for job in storage.iter_job(suite): |
| 197 | if isinstance(job, FioJobConfig): |
| 198 | fjob = cast(FioJobConfig, job) |
| 199 | io_sum = make_iosum(storage, suite, job, hboxes) |
| 200 | |
| 201 | bw_avg = io_sum.bw.average * unit_conversion_coef(io_sum.bw.units, StoragePerfSummary.bw_units) |
| 202 | |
| 203 | if fjob.bsize < large_blocks: |
| 204 | lat_95_ms = io_sum.lat.perc_95 * unit_conversion_coef(io_sum.lat.units, 'ms') |
| 205 | lat_50_ms = io_sum.lat.perc_50 * unit_conversion_coef(io_sum.lat.units, 'ms') |
| 206 | |
| 207 | iops_avg = io_sum.bw.average * unit_conversion_coef(io_sum.bw.units, StoragePerfSummary.iops_units) |
| 208 | iops_avg /= fjob.bsize |
| 209 | |
| 210 | if fjob.oper == 'randwrite' and fjob.sync_mode == 'd': |
| 211 | for lat, field in [(10, 'rw_iops_10ms'), (30, 'rw_iops_30ms'), (100, 'rw_iops_100ms')]: |
| 212 | if lat_95_ms <= lat: |
| 213 | setattr(psum95, field, max(getattr(psum95, field), iops_avg)) |
| 214 | if lat_50_ms <= lat: |
| 215 | setattr(psum50, field, max(getattr(psum50, field), iops_avg)) |
| 216 | |
| 217 | if fjob.oper == 'randread' and fjob.sync_mode == 'd': |
| 218 | for lat, field in [(10, 'rr_iops_10ms'), (30, 'rr_iops_30ms'), (100, 'rr_iops_100ms')]: |
| 219 | if lat_95_ms <= lat: |
| 220 | setattr(psum95, field, max(getattr(psum95, field), iops_avg)) |
| 221 | if lat_50_ms <= lat: |
| 222 | setattr(psum50, field, max(getattr(psum50, field), iops_avg)) |
| 223 | elif fjob.sync_mode == 'd': |
| 224 | if fjob.oper in ('randwrite', 'write'): |
| 225 | psum50.bw_write_max = max(psum50.bw_write_max, bw_avg) |
| 226 | elif fjob.oper in ('randread', 'read'): |
| 227 | psum50.bw_read_max = max(psum50.bw_read_max, bw_avg) |
| 228 | |
| 229 | return psum50, psum95 |
| 230 | |
| 231 | |
| 232 | # Main performance report |
| 233 | class PerformanceSummary(SuiteReporter): |
| 234 | """Aggregated summary for storage""" |
| 235 | def get_divs(self, suite: SuiteConfig) -> Iterator[Tuple[str, str, HTMLBlock]]: |
| 236 | psum50, psum95 = get_performance_summary(self.rstorage, suite, self.style.hist_boxes, self.style.large_blocks) |
| 237 | |
| 238 | caption = "Storage summary report" |
| 239 | res = html.H3(html.center(caption)) |
| 240 | |
| 241 | headers = ["Mode", "Stats", "Explanation"] |
| 242 | align = ['left', 'right', "left"] |
kdanylov aka koder | 13e5845 | 2018-07-15 02:51:51 +0300 | [diff] [blame^] | 243 | data: List[Union[str, Tuple[str, str, str]]] = [] |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 244 | |
| 245 | if psum95.rr_iops_10ms != psum95.NO_VAL or psum95.rr_iops_30ms != psum95.NO_VAL or \ |
| 246 | psum95.rr_iops_100ms != psum95.NO_VAL: |
| 247 | data.append("Average random read IOPS for small blocks") |
| 248 | |
| 249 | if psum95.rr_iops_10ms != psum95.NO_VAL: |
| 250 | data.append(("Database", b2ssize_10(psum95.rr_iops_10ms), "Latency 95th percentile < 10ms")) |
| 251 | if psum95.rr_iops_30ms != psum95.NO_VAL: |
| 252 | data.append(("File system", b2ssize_10(psum95.rr_iops_30ms), "Latency 95th percentile < 30ms")) |
| 253 | if psum95.rr_iops_100ms != psum95.NO_VAL: |
| 254 | data.append(("File server", b2ssize_10(psum95.rr_iops_100ms), "Latency 95th percentile < 100ms")) |
| 255 | |
| 256 | if psum95.rw_iops_10ms != psum95.NO_VAL or psum95.rw_iops_30ms != psum95.NO_VAL or \ |
| 257 | psum95.rw_iops_100ms != psum95.NO_VAL: |
| 258 | data.append("Average random write IOPS for small blocks") |
| 259 | |
| 260 | if psum95.rw_iops_10ms != psum95.NO_VAL: |
| 261 | data.append(("Database", b2ssize_10(psum95.rw_iops_10ms), "Latency 95th percentile < 10ms")) |
| 262 | if psum95.rw_iops_30ms != psum95.NO_VAL: |
| 263 | data.append(("File system", b2ssize_10(psum95.rw_iops_30ms), "Latency 95th percentile < 30ms")) |
| 264 | if psum95.rw_iops_100ms != psum95.NO_VAL: |
| 265 | data.append(("File server", b2ssize_10(psum95.rw_iops_100ms), "Latency 95th percentile < 100ms")) |
| 266 | |
| 267 | if psum50.bw_write_max != psum50.NO_VAL or psum50.bw_read_max != psum50.NO_VAL: |
| 268 | data.append("Average sequention IO") |
| 269 | |
| 270 | if psum50.bw_write_max != psum95.NO_VAL: |
| 271 | data.append(("Write", b2ssize(psum50.bw_write_max) + psum50.bw_units, |
| 272 | "Large blocks (>={}KiB)".format(self.style.large_blocks))) |
| 273 | if psum50.bw_read_max != psum95.NO_VAL: |
| 274 | data.append(("Read", b2ssize(psum50.bw_read_max) + psum50.bw_units, |
| 275 | "Large blocks (>={}KiB)".format(self.style.large_blocks))) |
| 276 | |
kdanylov aka koder | 13e5845 | 2018-07-15 02:51:51 +0300 | [diff] [blame^] | 277 | if data: |
| 278 | res += html.center(html.table("Performance", headers, data, align=align)) |
| 279 | yield Menu1st.summary, Menu2ndSumm.summary, HTMLBlock(res) |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 280 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 281 | |
| 282 | # # Node load over test time |
| 283 | # class NodeLoad(SuiteReporter): |
| 284 | # """IOPS/latency during test""" |
| 285 | |
| 286 | # # Ceph operation breakout report |
| 287 | # class CephClusterSummary(SuiteReporter): |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 288 | |
| 289 | |
| 290 | # Main performance report |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 291 | class IOQD(SuiteReporter): |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 292 | """Creates graph, which show how IOPS and Latency depend on QD""" |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 293 | suite_types = {'fio'} |
| 294 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 295 | def get_divs(self, suite: SuiteConfig) -> Iterator[Tuple[str, str, HTMLBlock]]: |
kdanylov aka koder | 13e5845 | 2018-07-15 02:51:51 +0300 | [diff] [blame^] | 296 | ts_map: Dict[FioJobParams, List[Tuple[SuiteConfig, FioJobConfig]]] = defaultdict(list) |
| 297 | str_summary: Dict[FioJobParams, Tuple[str, str]] = {} |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 298 | |
| 299 | for job in self.rstorage.iter_job(suite): |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 300 | fjob = cast(FioJobConfig, job) |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 301 | fjob_no_qd = cast(FioJobParams, fjob.params.copy(qd=None)) |
| 302 | str_summary[fjob_no_qd] = (fjob_no_qd.summary, fjob_no_qd.long_summary) |
| 303 | ts_map[fjob_no_qd].append((suite, fjob)) |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 304 | |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 305 | caption = "IOPS, bandwith, and latency as function of parallel IO request count (QD)" |
| 306 | yield Menu1st.summary, Menu2ndSumm.io_lat_qd, HTMLBlock(html.H3(html.center(caption))) |
| 307 | |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 308 | for tpl, suites_jobs in ts_map.items(): |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 309 | if len(suites_jobs) >= self.style.min_iops_vs_qd_jobs: |
| 310 | iosums = [make_iosum(self.rstorage, suite, job, self.style.hist_boxes) for suite, job in suites_jobs] |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 311 | iosums.sort(key=lambda x: x.qd) |
| 312 | summary, summary_long = str_summary[tpl] |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 313 | |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 314 | ds = DataSource(suite_id=suite.storage_id, |
| 315 | job_id=summary, |
| 316 | node_id=AGG_TAG, |
| 317 | sensor="fio", |
| 318 | dev=AGG_TAG, |
| 319 | metric="io_over_qd", |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 320 | tag=io_chart_format) |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 321 | |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 322 | fpath = self.plt(io_chart, ds, title=summary_long, legend="IOPS/BW", iosums=iosums) |
| 323 | yield Menu1st.summary, Menu2ndSumm.io_lat_qd, HTMLBlock(html.img(fpath)) |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 324 | |
| 325 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 326 | class ResourceQD(SuiteReporter): |
| 327 | suite_types = {'fio'} |
| 328 | |
| 329 | def get_divs(self, suite: SuiteConfig) -> Iterator[Tuple[str, str, HTMLBlock]]: |
kdanylov aka koder | 13e5845 | 2018-07-15 02:51:51 +0300 | [diff] [blame^] | 330 | qd_grouped_jobs: Dict[FioJobParams, List[FioJobConfig]] = {} |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 331 | test_nc = len(list(find_nodes_by_roles(self.rstorage.storage, ['testnode']))) |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 332 | for job in self.rstorage.iter_job(suite): |
| 333 | fjob = cast(FioJobConfig, job) |
| 334 | if fjob.bsize != 4: |
| 335 | continue |
| 336 | |
| 337 | fjob_no_qd = cast(FioJobParams, fjob.params.copy(qd=None)) |
| 338 | qd_grouped_jobs.setdefault(fjob_no_qd, []).append(fjob) |
| 339 | |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 340 | yield Menu1st.summary, Menu2ndSumm.resources_usage_qd, HTMLBlock(html.center(html.H3("Resource usage summary"))) |
| 341 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 342 | for jc_no_qd, jobs in sorted(qd_grouped_jobs.items()): |
| 343 | cpu_usage2qd = {} |
| 344 | for job in jobs: |
| 345 | usage, iops_ok = get_resources_usage(suite, job, self.rstorage, hist_boxes=self.style.hist_boxes, |
| 346 | large_block=self.style.large_blocks) |
| 347 | |
| 348 | if iops_ok: |
| 349 | cpu_usage2qd[job.qd] = usage[ResourceNames.storage_cpu_s] |
| 350 | |
| 351 | if len(cpu_usage2qd) < StyleProfile.min_iops_vs_qd_jobs: |
| 352 | continue |
| 353 | |
kdanylov aka koder | 13e5845 | 2018-07-15 02:51:51 +0300 | [diff] [blame^] | 354 | labels, vals, errs = zip(*((l, avg, dev) |
| 355 | for l, (_, avg, dev) in sorted(cpu_usage2qd.items()))) # type: ignore |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 356 | |
| 357 | if test_nc == 1: |
| 358 | labels = list(map(str, labels)) |
| 359 | else: |
| 360 | labels = ["{} * {}".format(label, test_nc) for label in labels] |
| 361 | |
| 362 | ds = DataSource(suite_id=suite.storage_id, |
| 363 | job_id=jc_no_qd.summary, |
| 364 | node_id="cluster", |
| 365 | sensor=AGG_TAG, |
| 366 | dev='cpu', |
| 367 | metric="cpu_for_iop", |
| 368 | tag=io_chart_format) |
| 369 | |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 370 | title = "CPU time per IOP, " + jc_no_qd.long_summary |
| 371 | fpath = self.plt(plot_simple_bars, ds, title, labels, vals, errs, |
| 372 | xlabel="CPU core time per IOP", |
| 373 | ylabel="QD * Test nodes" if test_nc != 1 else "QD", |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 374 | x_formatter=(lambda x, pos: b2ssize_10(x) + 's'), |
| 375 | one_point_zero_line=False) |
| 376 | |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 377 | yield Menu1st.summary, Menu2ndSumm.resources_usage_qd, HTMLBlock(html.img(fpath)) |
| 378 | |
| 379 | |
| 380 | def get_resources_usage2(suite: SuiteConfig, job: JobConfig, rstorage: IWallyStorage, |
| 381 | roles, sensor, metric, test_metric, agg_window: int = 5) -> ndarray2d: |
| 382 | assert test_metric == 'iops' |
| 383 | fjob = cast(FioJobConfig, job) |
| 384 | bw = get_aggregated(rstorage, suite.storage_id, job.storage_id, "bw", job.reliable_info_range_s) |
| 385 | io_transfered = bw.data * unit_conversion_coef_f(bw.units, "Bps") |
| 386 | ops_done = io_transfered / (fjob.bsize * unit_conversion_coef_f("KiBps", "Bps")) |
| 387 | nodes = [node for node in rstorage.load_nodes() if node.roles.intersection(STORAGE_ROLES)] |
| 388 | |
| 389 | if sensor == 'system-cpu': |
| 390 | assert metric == 'used' |
| 391 | core_count = None |
| 392 | for node in nodes: |
| 393 | if core_count is None: |
| 394 | core_count = sum(cores for _, cores in node.hw_info.cpus) |
| 395 | else: |
| 396 | assert core_count == sum(cores for _, cores in node.hw_info.cpus) |
| 397 | cpu_ts = get_cluster_cpu_load(rstorage, roles, job.reliable_info_range_s) |
| 398 | metric_data = (1.0 - (cpu_ts['idle'].data + cpu_ts['iowait'].data) / cpu_ts['total'].data) * core_count |
| 399 | else: |
| 400 | metric_data = sum_sensors(rstorage, job.reliable_info_range_s, |
| 401 | node_id=[node.node_id for node in nodes], sensor=sensor, metric=metric) |
| 402 | |
| 403 | res = [] |
| 404 | for pos in range(0, len(ops_done) - agg_window, agg_window): |
| 405 | pe = pos + agg_window |
| 406 | res.append((numpy.average(ops_done[pos: pe]), numpy.average(metric_data.data[pos: pe]))) |
| 407 | |
| 408 | return res |
| 409 | |
| 410 | |
| 411 | class ResourceConsumptionSummary(SuiteReporter): |
| 412 | suite_types = {'fio'} |
| 413 | |
| 414 | def get_divs(self, suite: SuiteConfig) -> Iterator[Tuple[str, str, HTMLBlock]]: |
| 415 | vs = 'iops' |
| 416 | for job_tp in ('rwd4', 'rrd4'): |
| 417 | for sensor_metric in ('net-io.send_packets', 'system-cpu.used'): |
| 418 | sensor, metric = sensor_metric.split(".") |
| 419 | usage = [] |
| 420 | for job in self.rstorage.iter_job(suite): |
| 421 | if job_tp in job.summary: |
| 422 | usage.extend(get_resources_usage2(suite, job, self.rstorage, STORAGE_ROLES, |
| 423 | sensor=sensor, metric=metric, test_metric=vs)) |
| 424 | |
| 425 | if not usage: |
| 426 | continue |
| 427 | |
| 428 | iops, cpu = zip(*usage) |
| 429 | slope, intercept, r_value, p_value, std_err = scipy.stats.linregress(iops, cpu) |
| 430 | x = numpy.array([0.0, max(iops) * 1.1]) |
| 431 | |
| 432 | ds = DataSource(suite_id=suite.storage_id, |
| 433 | job_id=job_tp, |
| 434 | node_id="storage", |
| 435 | sensor='usage-regression', |
| 436 | dev=AGG_TAG, |
| 437 | metric=sensor_metric + '.VS.' + vs, |
| 438 | tag=default_format) |
| 439 | |
| 440 | fname = self.plt(plot_dots_with_regression, ds, |
| 441 | "{}::{}.{}".format(job_tp, sensor_metric, vs), |
| 442 | x=iops, y=cpu, |
| 443 | xlabel=vs, |
| 444 | ylabel=sensor_metric, |
| 445 | x_approx=x, y_approx=intercept + slope * x) |
| 446 | |
| 447 | yield Menu1st.engineering, Menu2ndEng.resource_regression, HTMLBlock(html.img(fname)) |
| 448 | |
| 449 | |
| 450 | class EngineeringSummary(SuiteReporter): |
| 451 | suite_types = {'fio'} |
| 452 | |
| 453 | def get_divs(self, suite: SuiteConfig) -> Iterator[Tuple[str, str, HTMLBlock]]: |
| 454 | tbl = [line for line in get_console_report_table(suite, self.rstorage) if line is not Texttable.HLINE] |
| 455 | align = [{'l': 'left', 'r': 'right'}[al] for al in console_report_align] |
| 456 | res = html.center(html.table("Test results", console_report_headers, tbl, align=align)) |
| 457 | yield Menu1st.engineering, Menu2ndEng.summary, HTMLBlock(res) |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 458 | |
| 459 | |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 460 | class StatInfo(JobReporter): |
| 461 | """Statistic info for job results""" |
| 462 | suite_types = {'fio'} |
| 463 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 464 | def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]: |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 465 | |
| 466 | fjob = cast(FioJobConfig, job) |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 467 | io_sum = make_iosum(self.rstorage, suite, fjob, self.style.hist_boxes) |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 468 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 469 | caption = "Test summary - " + job.params.long_summary |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 470 | test_nc = len(list(find_nodes_by_roles(self.rstorage.storage, ['testnode']))) |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 471 | if test_nc > 1: |
| 472 | caption += " * {} nodes".format(test_nc) |
| 473 | |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 474 | res = html.H3(html.center(caption)) |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 475 | stat_data_headers = ["Name", |
| 476 | "Total done", |
| 477 | "Average ~ Dev", |
| 478 | "Conf interval", |
| 479 | "Mediana", |
| 480 | "Mode", |
| 481 | "Kurt / Skew", |
| 482 | "95%", |
| 483 | "99%", |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 484 | "ADF test"] |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 485 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 486 | align = ['left'] + ['right'] * (len(stat_data_headers) - 1) |
| 487 | |
| 488 | bw_units = "B" |
| 489 | bw_target_units = bw_units + 'ps' |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 490 | bw_coef = unit_conversion_coef_f(io_sum.bw.units, bw_target_units) |
kdanylov aka koder | 4518318 | 2017-04-30 23:55:40 +0300 | [diff] [blame] | 491 | |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 492 | adf_v, *_1, stats, _2 = adfuller(io_sum.bw.data) |
| 493 | |
| 494 | for v in ("1%", "5%", "10%"): |
| 495 | if adf_v <= stats[v]: |
| 496 | ad_test = v |
| 497 | break |
| 498 | else: |
| 499 | ad_test = "Failed" |
| 500 | |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 501 | bw_data = ["Bandwidth", |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 502 | b2ssize(io_sum.bw.data.sum() * bw_coef) + bw_units, |
kdanylov aka koder | 4518318 | 2017-04-30 23:55:40 +0300 | [diff] [blame] | 503 | "{}{} ~ {}{}".format(b2ssize(io_sum.bw.average * bw_coef), bw_target_units, |
| 504 | b2ssize(io_sum.bw.deviation * bw_coef), bw_target_units), |
| 505 | b2ssize(io_sum.bw.confidence * bw_coef) + bw_target_units, |
| 506 | b2ssize(io_sum.bw.perc_50 * bw_coef) + bw_target_units, |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 507 | "-", |
| 508 | "{:.2f} / {:.2f}".format(io_sum.bw.kurt, io_sum.bw.skew), |
kdanylov aka koder | 4518318 | 2017-04-30 23:55:40 +0300 | [diff] [blame] | 509 | b2ssize(io_sum.bw.perc_5 * bw_coef) + bw_target_units, |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 510 | b2ssize(io_sum.bw.perc_1 * bw_coef) + bw_target_units, |
| 511 | ad_test] |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 512 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 513 | stat_data = [bw_data] |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 514 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 515 | if fjob.bsize < StyleProfile.large_blocks: |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 516 | iops_coef = unit_conversion_coef_f(io_sum.bw.units, 'KiBps') / fjob.bsize |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 517 | iops_data = ["IOPS", |
| 518 | b2ssize_10(io_sum.bw.data.sum() * iops_coef), |
| 519 | "{}IOPS ~ {}IOPS".format(b2ssize_10(io_sum.bw.average * iops_coef), |
| 520 | b2ssize_10(io_sum.bw.deviation * iops_coef)), |
| 521 | b2ssize_10(io_sum.bw.confidence * iops_coef) + "IOPS", |
| 522 | b2ssize_10(io_sum.bw.perc_50 * iops_coef) + "IOPS", |
| 523 | "-", |
| 524 | "{:.2f} / {:.2f}".format(io_sum.bw.kurt, io_sum.bw.skew), |
| 525 | b2ssize_10(io_sum.bw.perc_5 * iops_coef) + "IOPS", |
| 526 | b2ssize_10(io_sum.bw.perc_1 * iops_coef) + "IOPS", |
| 527 | ad_test] |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 528 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 529 | lat_target_unit = 's' |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 530 | lat_coef = unit_conversion_coef_f(io_sum.lat.units, lat_target_unit) |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 531 | # latency |
| 532 | lat_data = ["Latency", |
| 533 | "-", |
| 534 | "-", |
| 535 | "-", |
| 536 | b2ssize_10(io_sum.lat.perc_50 * lat_coef) + lat_target_unit, |
| 537 | "-", |
| 538 | "-", |
| 539 | b2ssize_10(io_sum.lat.perc_95 * lat_coef) + lat_target_unit, |
| 540 | b2ssize_10(io_sum.lat.perc_99 * lat_coef) + lat_target_unit, |
| 541 | '-'] |
| 542 | |
| 543 | # sensor usage |
| 544 | stat_data.extend([iops_data, lat_data]) |
| 545 | |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 546 | res += html.center(html.table("Test results", stat_data_headers, stat_data, align=align)) |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 547 | yield Menu1st.per_job, job.summary, HTMLBlock(res) |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 548 | |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 549 | |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 550 | class Resources(JobReporter): |
| 551 | """Statistic info for job results""" |
| 552 | suite_types = {'fio'} |
| 553 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 554 | def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]: |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 555 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 556 | records, iops_ok = get_resources_usage(suite, job, self.rstorage, |
| 557 | large_block=self.style.large_blocks, |
| 558 | hist_boxes=self.style.hist_boxes) |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 559 | |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 560 | table_structure = [ |
| 561 | "Service provided", |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 562 | (ResourceNames.io_made, ResourceNames.data_tr), |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 563 | "Test nodes total load", |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 564 | (ResourceNames.test_send_pkt, ResourceNames.test_send), |
| 565 | (ResourceNames.test_recv_pkt, ResourceNames.test_recv), |
| 566 | (ResourceNames.test_net_pkt, ResourceNames.test_net), |
| 567 | (ResourceNames.test_write_iop, ResourceNames.test_write), |
| 568 | (ResourceNames.test_read_iop, ResourceNames.test_read), |
| 569 | (ResourceNames.test_iop, ResourceNames.test_rw), |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 570 | "Storage nodes resource consumed", |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 571 | (ResourceNames.storage_send_pkt, ResourceNames.storage_send), |
| 572 | (ResourceNames.storage_recv_pkt, ResourceNames.storage_recv), |
| 573 | (ResourceNames.storage_net_pkt, ResourceNames.storage_net), |
| 574 | (ResourceNames.storage_write_iop, ResourceNames.storage_write), |
| 575 | (ResourceNames.storage_read_iop, ResourceNames.storage_read), |
| 576 | (ResourceNames.storage_iop, ResourceNames.storage_rw), |
| 577 | (ResourceNames.storage_cpu_s, ResourceNames.storage_cpu_s_b), |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 578 | ] # type: List[Union[str, Tuple[Optional[str], ...]]] |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 579 | |
| 580 | if not iops_ok: |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 581 | table_structure2 = [] # type: List[Union[Tuple[str, ...], str]] |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 582 | for line in table_structure: |
| 583 | if isinstance(line, str): |
| 584 | table_structure2.append(line) |
| 585 | else: |
| 586 | assert len(line) == 2 |
| 587 | table_structure2.append((line[1],)) |
| 588 | table_structure = table_structure2 |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 589 | |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 590 | yield Menu1st.per_job, job.summary, HTMLBlock(html.H3(html.center("Resources usage"))) |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 591 | |
| 592 | doc = xmlbuilder3.XMLBuilder("table", |
| 593 | **{"class": "table table-bordered table-striped table-condensed table-hover", |
| 594 | "style": "width: auto;"}) |
| 595 | |
| 596 | with doc.thead: |
| 597 | with doc.tr: |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 598 | [doc.th(header) for header in ["Resource", "Usage count", "To service"] * (2 if iops_ok else 1)] |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 599 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 600 | cols = 6 if iops_ok else 3 |
| 601 | col_per_tp = 3 |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 602 | |
| 603 | short_name = { |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 604 | name: (name if name in {ResourceNames.io_made, ResourceNames.data_tr} |
| 605 | else " ".join(name.split()[2:]).capitalize()) |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 606 | for name in records.keys() |
| 607 | } |
| 608 | |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 609 | short_name[ResourceNames.storage_cpu_s] = "CPU core (s/IOP)" |
| 610 | short_name[ResourceNames.storage_cpu_s_b] = "CPU core (s/B)" |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 611 | |
| 612 | with doc.tbody: |
| 613 | with doc.tr: |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 614 | if iops_ok: |
| 615 | doc.td(colspan=str(col_per_tp)).center.b("Operations") |
| 616 | doc.td(colspan=str(col_per_tp)).center.b("Bytes") |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 617 | |
| 618 | for line in table_structure: |
| 619 | with doc.tr: |
| 620 | if isinstance(line, str): |
| 621 | with doc.td(colspan=str(cols)): |
| 622 | doc.center.b(line) |
| 623 | else: |
| 624 | for name in line: |
| 625 | if name is None: |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 626 | doc.td("-", colspan=str(col_per_tp)) |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 627 | continue |
| 628 | |
| 629 | amount_s, avg, dev = records[name] |
| 630 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 631 | if name in (ResourceNames.storage_cpu_s, ResourceNames.storage_cpu_s_b) and avg is not None: |
| 632 | if dev is None: |
| 633 | rel_val_s = b2ssize_10(avg) + 's' |
| 634 | else: |
| 635 | dev_s = str(int(dev * 100 / avg)) + "%" if avg > 1E-9 else b2ssize_10(dev) + 's' |
| 636 | rel_val_s = "{}s ~ {}".format(b2ssize_10(avg), dev_s) |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 637 | else: |
| 638 | if avg is None: |
| 639 | rel_val_s = '-' |
| 640 | else: |
| 641 | avg_s = int(avg) if avg > 10 else '{:.1f}'.format(avg) |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 642 | if dev is None: |
| 643 | rel_val_s = avg_s |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 644 | else: |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 645 | if avg > 1E-5: |
| 646 | dev_s = str(int(dev * 100 / avg)) + "%" |
| 647 | else: |
| 648 | dev_s = int(dev) if dev > 10 else '{:.1f}'.format(dev) |
| 649 | rel_val_s = "{} ~ {}".format(avg_s, dev_s) |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 650 | |
| 651 | doc.td(short_name[name], align="left") |
| 652 | doc.td(amount_s, align="right") |
| 653 | |
| 654 | if avg is None or avg < 0.9: |
| 655 | doc.td(rel_val_s, align="right") |
| 656 | elif avg < 2.0: |
| 657 | doc.td(align="right").font(rel_val_s, color='green') |
| 658 | elif avg < 5.0: |
| 659 | doc.td(align="right").font(rel_val_s, color='orange') |
| 660 | else: |
| 661 | doc.td(align="right").font(rel_val_s, color='red') |
| 662 | |
| 663 | res = xmlbuilder3.tostr(doc).split("\n", 1)[1] |
| 664 | yield Menu1st.per_job, job.summary, HTMLBlock(html.center(res)) |
| 665 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 666 | iop_names = [ResourceNames.test_write_iop, ResourceNames.test_read_iop, ResourceNames.test_iop, |
| 667 | ResourceNames.storage_write_iop, ResourceNames.storage_read_iop, ResourceNames.storage_iop] |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 668 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 669 | bytes_names = [ResourceNames.test_write, ResourceNames.test_read, ResourceNames.test_rw, |
| 670 | ResourceNames.test_send, ResourceNames.test_recv, ResourceNames.test_net, |
| 671 | ResourceNames.storage_write, ResourceNames.storage_read, ResourceNames.storage_rw, |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 672 | ResourceNames.storage_send, ResourceNames.storage_recv, |
| 673 | ResourceNames.storage_net] # type: List[str] |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 674 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 675 | net_pkt_names = [ResourceNames.test_send_pkt, ResourceNames.test_recv_pkt, ResourceNames.test_net_pkt, |
| 676 | ResourceNames.storage_send_pkt, ResourceNames.storage_recv_pkt, ResourceNames.storage_net_pkt] |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 677 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 678 | pairs = [("bytes", bytes_names)] |
| 679 | if iops_ok: |
| 680 | pairs.insert(0, ('iop', iop_names)) |
| 681 | pairs.append(('Net packets per IOP', net_pkt_names)) |
| 682 | |
| 683 | yield Menu1st.per_job, job.summary, \ |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 684 | HTMLBlock(html.H3(html.center("Resource consumption per service provided"))) |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 685 | |
| 686 | for tp, names in pairs: |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 687 | vals = [] # type: List[float] |
| 688 | devs = [] # type: List[float] |
| 689 | avail_names = [] # type: List[str] |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 690 | for name in names: |
| 691 | if name in records: |
| 692 | avail_names.append(name) |
| 693 | _, avg, dev = records[name] |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 694 | |
| 695 | if dev is None: |
| 696 | dev = 0 |
| 697 | |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 698 | vals.append(avg) |
| 699 | devs.append(dev) |
| 700 | |
| 701 | # synchronously sort values and names, values is a key |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 702 | vals, names, devs = map(list, zip(*sorted(zip(vals, names, devs)))) # type: ignore |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 703 | |
| 704 | ds = DataSource(suite_id=suite.storage_id, |
| 705 | job_id=job.storage_id, |
| 706 | node_id=AGG_TAG, |
| 707 | sensor='resources', |
| 708 | dev=AGG_TAG, |
| 709 | metric=tp.replace(' ', "_") + '2service_bar', |
| 710 | tag=default_format) |
| 711 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 712 | fname = self.plt(plot_simple_bars, ds, tp.capitalize(), |
| 713 | [name.replace(" nodes", "") for name in names], |
| 714 | vals, devs) |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 715 | |
| 716 | yield Menu1st.per_job, job.summary, HTMLBlock(html.img(fname)) |
| 717 | |
| 718 | |
| 719 | class BottleNeck(JobReporter): |
| 720 | """Statistic info for job results""" |
| 721 | suite_types = {'fio'} |
| 722 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 723 | def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]: |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 724 | |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 725 | nodes = list(find_nodes_by_roles(self.rstorage.storage, STORAGE_ROLES)) |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 726 | |
| 727 | sensor = 'block-io' |
| 728 | metric = 'io_queue' |
| 729 | bn_val = 16 |
| 730 | |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 731 | for node_id in nodes: |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 732 | bn = 0 |
| 733 | tot = 0 |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 734 | for ds in self.rstorage.iter_sensors(node_id=node_id, sensor=sensor, metric=metric): |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 735 | if ds.dev in ('sdb', 'sdc', 'sdd', 'sde'): |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 736 | ts = self.rstorage.get_sensor(ds, job.reliable_info_range_s) |
| 737 | bn += (ts.data > bn_val).sum() |
| 738 | tot += len(ts.data) |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 739 | |
| 740 | yield Menu1st.per_job, job.summary, HTMLBlock("") |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 741 | |
| 742 | |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 743 | # CPU load |
| 744 | class CPULoadPlot(JobReporter): |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 745 | def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]: |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 746 | |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 747 | # plot CPU time |
| 748 | for rt, roles in [('storage', STORAGE_ROLES), ('test', ['testnode'])]: |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 749 | cpu_ts = get_cluster_cpu_load(self.rstorage, roles, job.reliable_info_range_s) |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 750 | tss = [(name, ts.data * 100 / cpu_ts['total'].data) |
| 751 | for name, ts in cpu_ts.items() |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 752 | if name in {'user', 'sys', 'idle', 'iowait'}] |
| 753 | |
| 754 | |
| 755 | ds = cpu_ts['idle'].source(job_id=job.storage_id, suite_id=suite.storage_id, |
| 756 | node_id=AGG_TAG, metric='allcpu', tag=rt + '.plt.' + default_format) |
| 757 | |
| 758 | fname = self.plt(plot_simple_over_time, ds, tss=tss, average=True, ylabel="CPU time %", |
| 759 | title="{} nodes CPU usage".format(rt.capitalize()), |
| 760 | xlabel="Time from test begin") |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 761 | |
| 762 | yield Menu1st.per_job, job.summary, HTMLBlock(html.img(fname)) |
| 763 | |
| 764 | |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 765 | def roles_for_sensors(storage: IWallyStorage) -> Dict[str, List[DataSource]]: |
| 766 | role2ds = defaultdict(list) |
| 767 | |
| 768 | for node in storage.load_nodes(): |
| 769 | ds = DataSource(node_id=node.node_id) |
| 770 | if 'ceph-osd' in node.roles: |
| 771 | for jdev in node.params.get('ceph_journal_devs', []): |
kdanylov aka koder | 13e5845 | 2018-07-15 02:51:51 +0300 | [diff] [blame^] | 772 | role2ds[DevRoles.osd_journal].append(ds(dev=jdev)) |
| 773 | role2ds[DevRoles.storage_block].append(ds(dev=jdev)) |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 774 | |
| 775 | for sdev in node.params.get('ceph_storage_devs', []): |
kdanylov aka koder | 13e5845 | 2018-07-15 02:51:51 +0300 | [diff] [blame^] | 776 | role2ds[DevRoles.osd_storage].append(ds(dev=sdev)) |
| 777 | role2ds[DevRoles.storage_block].append(ds(dev=sdev)) |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 778 | |
| 779 | if node.hw_info: |
| 780 | for dev in node.hw_info.disks_info: |
kdanylov aka koder | 13e5845 | 2018-07-15 02:51:51 +0300 | [diff] [blame^] | 781 | role2ds[DevRoles.storage_block].append(ds(dev=dev)) |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 782 | |
| 783 | if 'testnode' in node.roles: |
kdanylov aka koder | 13e5845 | 2018-07-15 02:51:51 +0300 | [diff] [blame^] | 784 | role2ds[DevRoles.client_block].append(ds(dev='rbd0')) |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 785 | |
| 786 | return role2ds |
| 787 | |
| 788 | |
| 789 | def get_sources_for_roles(roles: Iterable[str]) -> List[DataSource]: |
| 790 | return [] |
| 791 | |
| 792 | |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 793 | # IO time and QD |
| 794 | class QDIOTimeHeatmap(JobReporter): |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 795 | def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]: |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 796 | trange = (job.reliable_info_range[0] // 1000, job.reliable_info_range[1] // 1000) |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 797 | test_nc = len(list(find_nodes_by_roles(self.rstorage.storage, ['testnode']))) |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 798 | |
kdanylov aka koder | 13e5845 | 2018-07-15 02:51:51 +0300 | [diff] [blame^] | 799 | for dev_role in (DevRoles.osd_storage, DevRoles.osd_journal, DevRoles.client_block): |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 800 | |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 801 | caption = "{} IO heatmaps - {}".format(dev_role.capitalize(), cast(FioJobParams, job).params.long_summary) |
| 802 | if test_nc != 1: |
| 803 | caption += " * {} nodes".format(test_nc) |
| 804 | |
| 805 | yield Menu1st.engineering_per_job, job.summary, HTMLBlock(html.H3(html.center(caption))) |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 806 | |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 807 | # QD heatmap |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 808 | ioq2d = find_sensors_to_2d(self.rstorage, trange, dev_role=dev_role, sensor='block-io', metric='io_queue') |
| 809 | |
| 810 | ds = DataSource(suite.storage_id, job.storage_id, AGG_TAG, 'block-io', dev_role, |
| 811 | tag="hmap." + default_format) |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 812 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 813 | fname = self.plt(plot_hmap_from_2d, ds(metric='io_queue'), data2d=ioq2d, xlabel='Time', ylabel="IO QD", |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 814 | title=dev_role.capitalize() + " devs QD", bins=StyleProfile.qd_bins) |
| 815 | yield Menu1st.engineering_per_job, job.summary, HTMLBlock(html.img(fname)) |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 816 | |
| 817 | # Block size heatmap |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 818 | wc2d = find_sensors_to_2d(self.rstorage, trange, dev_role=dev_role, sensor='block-io', |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 819 | metric='writes_completed') |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 820 | wc2d[wc2d < 1E-3] = 1 |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 821 | sw2d = find_sensors_to_2d(self.rstorage, trange, dev_role=dev_role, sensor='block-io', |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 822 | metric='sectors_written') |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 823 | data2d = sw2d / wc2d / 1024 |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 824 | fname = self.plt(plot_hmap_from_2d, ds(metric='wr_block_size'), |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 825 | data2d=data2d, title=dev_role.capitalize() + " write block size", |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 826 | ylabel="IO bsize, KiB", xlabel='Time', bins=StyleProfile.block_size_bins) |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 827 | yield Menu1st.engineering_per_job, job.summary, HTMLBlock(html.img(fname)) |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 828 | |
| 829 | # iotime heatmap |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 830 | wtime2d = find_sensors_to_2d(self.rstorage, trange, dev_role=dev_role, sensor='block-io', |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 831 | metric='io_time') |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 832 | fname = self.plt(plot_hmap_from_2d, ds(metric='io_time'), data2d=wtime2d, |
| 833 | xlabel='Time', ylabel="IO time (ms) per second", |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 834 | title=dev_role.capitalize() + " iotime", bins=StyleProfile.iotime_bins) |
| 835 | yield Menu1st.engineering_per_job, job.summary, HTMLBlock(html.img(fname)) |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 836 | |
| 837 | |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 838 | # IOPS/latency over test time for each job |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 839 | class LoadToolResults(JobReporter): |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 840 | """IOPS/latency during test""" |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 841 | suite_types = {'fio'} |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 842 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 843 | def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]: |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 844 | fjob = cast(FioJobConfig, job) |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 845 | |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 846 | # caption = "Load tool results, " + job.params.long_summary |
| 847 | caption = "Load tool results" |
| 848 | yield Menu1st.per_job, job.summary, HTMLBlock(html.H3(html.center(caption))) |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 849 | |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 850 | agg_io = get_aggregated(self.rstorage, suite.storage_id, fjob.storage_id, "bw", job.reliable_info_range_s) |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 851 | |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 852 | if fjob.bsize >= DefStyleProfile.large_blocks: |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 853 | title = "Fio measured bandwidth over time" |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 854 | units = "MiBps" |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 855 | agg_io.data //= int(unit_conversion_coef_f(units, agg_io.units)) |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 856 | else: |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 857 | title = "Fio measured IOPS over time" |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 858 | agg_io.data //= (int(unit_conversion_coef_f("KiBps", agg_io.units)) * fjob.bsize) |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 859 | units = "IOPS" |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 860 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 861 | fpath = self.plt(plot_v_over_time, agg_io.source(tag='ts.' + default_format), title, units, agg_io) |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 862 | yield Menu1st.per_job, fjob.summary, HTMLBlock(html.img(fpath)) |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 863 | |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 864 | title = "BW distribution" if fjob.bsize >= DefStyleProfile.large_blocks else "IOPS distribution" |
| 865 | io_stat_prop = calc_norm_stat_props(agg_io, bins_count=StyleProfile.hist_boxes) |
| 866 | fpath = self.plt(plot_hist, agg_io.source(tag='hist.' + default_format), title, units, io_stat_prop) |
| 867 | yield Menu1st.per_job, fjob.summary, HTMLBlock(html.img(fpath)) |
| 868 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 869 | if fjob.bsize < DefStyleProfile.large_blocks: |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 870 | agg_lat = get_aggregated(self.rstorage, suite.storage_id, fjob.storage_id, "lat", |
| 871 | job.reliable_info_range_s) |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 872 | TARGET_UNITS = 'ms' |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 873 | coef = unit_conversion_coef_f(agg_lat.units, TARGET_UNITS) |
| 874 | agg_lat.histo_bins = agg_lat.histo_bins.copy() * coef |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 875 | agg_lat.units = TARGET_UNITS |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 876 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 877 | fpath = self.plt(plot_lat_over_time, agg_lat.source(tag='ts.' + default_format), "Latency", agg_lat, |
| 878 | ylabel="Latency, " + agg_lat.units) |
| 879 | yield Menu1st.per_job, fjob.summary, HTMLBlock(html.img(fpath)) |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 880 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 881 | fpath = self.plt(plot_histo_heatmap, agg_lat.source(tag='hmap.' + default_format), |
| 882 | "Latency heatmap", agg_lat, ylabel="Latency, " + agg_lat.units, xlabel='Test time') |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 883 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 884 | yield Menu1st.per_job, fjob.summary, HTMLBlock(html.img(fpath)) |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 885 | |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 886 | |
| 887 | # Cluster load over test time |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 888 | class ClusterLoad(JobReporter): |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 889 | """IOPS/latency during test""" |
| 890 | |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 891 | # TODO: units should came from sensor |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 892 | storage_sensors = [ |
kdanylov aka koder | 4518318 | 2017-04-30 23:55:40 +0300 | [diff] [blame] | 893 | ('block-io', 'reads_completed', "Read", 'iop'), |
| 894 | ('block-io', 'writes_completed', "Write", 'iop'), |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 895 | ('block-io', 'sectors_read', "Read", 'MiB'), |
| 896 | ('block-io', 'sectors_written', "Write", 'MiB'), |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 897 | ] |
| 898 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 899 | def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]: |
| 900 | |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 901 | yield Menu1st.per_job, job.summary, HTMLBlock(html.H3(html.center("Cluster load"))) |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 902 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 903 | sensors = [] |
| 904 | max_iop = 0 |
| 905 | max_bytes = 0 |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 906 | stor_nodes = find_nodes_by_roles(self.rstorage.storage, STORAGE_ROLES) |
kdanylov aka koder | 4518318 | 2017-04-30 23:55:40 +0300 | [diff] [blame] | 907 | for sensor, metric, op, units in self.storage_sensors: |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 908 | ts = sum_sensors(self.rstorage, job.reliable_info_range_s, node_id=stor_nodes, sensor=sensor, metric=metric) |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 909 | if ts is not None: |
| 910 | ds = DataSource(suite_id=suite.storage_id, |
| 911 | job_id=job.storage_id, |
| 912 | node_id="storage", |
| 913 | sensor=sensor, |
| 914 | dev=AGG_TAG, |
| 915 | metric=metric, |
| 916 | tag="ts." + default_format) |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 917 | |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 918 | data = ts.data if units != 'MiB' else ts.data * unit_conversion_coef_f(ts.units, 'MiB') |
| 919 | ts = TimeSeries(times=numpy.arange(*job.reliable_info_range_s), |
| 920 | data=data, |
| 921 | units=units if ts.units is None else ts.units, |
| 922 | time_units=ts.time_units, |
| 923 | source=ds, |
| 924 | histo_bins=ts.histo_bins) |
kdanylov aka koder | 0e0cfcb | 2017-03-27 22:19:09 +0300 | [diff] [blame] | 925 | |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 926 | sensors.append(("{} {}".format(op, units), ds, ts, units)) |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 927 | |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 928 | if units == 'iop': |
| 929 | max_iop = max(max_iop, data.sum()) |
| 930 | else: |
| 931 | assert units == 'MiB' |
| 932 | max_bytes = max(max_bytes, data.sum()) |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 933 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 934 | for title, ds, ts, units in sensors: |
| 935 | if ts.data.sum() >= (max_iop if units == 'iop' else max_bytes) * DefStyleProfile.min_load_diff: |
| 936 | fpath = self.plt(plot_v_over_time, ds, title, units, ts=ts) |
| 937 | yield Menu1st.per_job, job.summary, HTMLBlock(html.img(fpath)) |
| 938 | else: |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 939 | logger.info("Hide '%s' plot for %s, as it's load is less then %s%% from maximum", |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 940 | title, job.summary, int(DefStyleProfile.min_load_diff * 100)) |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 941 | |
| 942 | |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 943 | # ------------------------------------------ REPORT STAGES ----------------------------------------------------------- |
| 944 | |
| 945 | |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 946 | def add_devroles(ctx: TestRun): |
| 947 | # TODO: need to detect all devices for node on this stage using hw info |
| 948 | detected_selectors = collections.defaultdict( |
| 949 | lambda: collections.defaultdict(list)) # type: Dict[str, Dict[str, List[str]]] |
| 950 | |
| 951 | for node in ctx.nodes: |
| 952 | if NodeRole.osd in node.info.roles: |
| 953 | all_devs = set() |
| 954 | |
| 955 | jdevs = node.info.params.get('ceph_journal_devs') |
| 956 | if jdevs: |
| 957 | all_devs.update(jdevs) |
| 958 | detected_selectors[node.info.hostname]["|".join(jdevs)].append(DevRoles.osd_journal) |
| 959 | |
| 960 | sdevs = node.info.params.get('ceph_storage_devs') |
| 961 | if sdevs: |
| 962 | all_devs.update(sdevs) |
| 963 | detected_selectors[node.info.hostname]["|".join(sdevs)].append(DevRoles.osd_storage) |
| 964 | |
| 965 | if all_devs: |
| 966 | detected_selectors[node.info.hostname]["|".join(all_devs)].append(DevRoles.storage_block) |
| 967 | |
| 968 | for hostname, dev_rules in detected_selectors.items(): |
| 969 | dev_locs = [] # type: List[Dict[str, List[str]]] |
| 970 | ctx.devs_locator.append({hostname: dev_locs}) |
| 971 | for dev_names, roles in dev_rules.items(): |
| 972 | dev_locs.append({dev_names: roles}) |
| 973 | |
| 974 | |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 975 | class HtmlReportStage(Stage): |
| 976 | priority = StepOrder.REPORT |
| 977 | |
| 978 | def run(self, ctx: TestRun) -> None: |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 979 | nodes = ctx.rstorage.load_nodes() |
| 980 | update_storage_selector(ctx.rstorage, ctx.devs_locator, nodes) |
| 981 | |
kdanylov aka koder | 470a8fa | 2017-07-14 21:07:58 +0300 | [diff] [blame] | 982 | # role2ds = roles_for_sensors(ctx.rstorage) |
| 983 | |
| 984 | job_reporters_cls = [StatInfo, LoadToolResults, Resources, ClusterLoad, CPULoadPlot] #, QDIOTimeHeatmap] |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 985 | # job_reporters_cls = [QDIOTimeHeatmap] |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 986 | job_reporters = [rcls(ctx.rstorage, DefStyleProfile, DefColorProfile) |
| 987 | for rcls in job_reporters_cls] # type: ignore |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 988 | |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 989 | suite_reporters_cls = [IOQD, |
| 990 | ResourceQD, |
| 991 | PerformanceSummary, |
| 992 | EngineeringSummary, |
| 993 | ResourceConsumptionSummary] # type: List[Type[SuiteReporter]] |
| 994 | # suite_reporters_cls = [] # type: List[Type[SuiteReporter]] |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 995 | suite_reporters = [rcls(ctx.rstorage, DefStyleProfile, DefColorProfile) |
| 996 | for rcls in suite_reporters_cls] # type: ignore |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 997 | |
| 998 | root_dir = os.path.dirname(os.path.dirname(wally.__file__)) |
| 999 | doc_templ_path = os.path.join(root_dir, "report_templates/index.html") |
| 1000 | report_template = open(doc_templ_path, "rt").read() |
| 1001 | css_file_src = os.path.join(root_dir, "report_templates/main.css") |
| 1002 | css_file = open(css_file_src, "rt").read() |
| 1003 | |
| 1004 | menu_block = [] |
| 1005 | content_block = [] |
| 1006 | link_idx = 0 |
| 1007 | |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 1008 | items = defaultdict(lambda: defaultdict(list)) # type: Dict[str, Dict[str, List[HTMLBlock]]] |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 1009 | DEBUG = False |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 1010 | job_summ_sort_order = [] |
| 1011 | |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 1012 | # TODO: filter reporters |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 1013 | for suite in ctx.rstorage.iter_suite(FioTest.name): |
| 1014 | all_jobs = list(ctx.rstorage.iter_job(suite)) |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 1015 | all_jobs.sort(key=lambda job: job.params) |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 1016 | |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 1017 | new_jobs_in_order = [job.summary for job in all_jobs] |
| 1018 | same = set(new_jobs_in_order).intersection(set(job_summ_sort_order)) |
| 1019 | assert not same, "Job with same names in different suits found: " + ",".join(same) |
| 1020 | job_summ_sort_order.extend(new_jobs_in_order) |
| 1021 | |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 1022 | for job in all_jobs: |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 1023 | try: |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 1024 | for reporter in job_reporters: # type: JobReporter |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 1025 | logger.debug("Start reporter %s on job %s suite %s", |
| 1026 | reporter.__class__.__name__, job.summary, suite.test_type) |
| 1027 | for block, item, html in reporter.get_divs(suite, job): |
| 1028 | items[block][item].append(html) |
| 1029 | if DEBUG: |
| 1030 | break |
| 1031 | except Exception: |
| 1032 | logger.exception("Failed to generate report for %s", job.summary) |
| 1033 | |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 1034 | for sreporter in suite_reporters: # type: SuiteReporter |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 1035 | try: |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 1036 | logger.debug("Start reporter %s on suite %s", sreporter.__class__.__name__, suite.test_type) |
| 1037 | for block, item, html in sreporter.get_divs(suite): |
kdanylov aka koder | 736e5c1 | 2017-05-07 17:27:14 +0300 | [diff] [blame] | 1038 | items[block][item].append(html) |
kdanylov aka koder | 026e5f2 | 2017-05-15 01:04:39 +0300 | [diff] [blame] | 1039 | except Exception: |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 1040 | logger.exception("Failed to generate report for suite %s", suite.storage_id) |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 1041 | |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 1042 | if DEBUG: |
| 1043 | break |
| 1044 | |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 1045 | logger.debug("Generating result html") |
| 1046 | |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 1047 | for idx_1st, menu_1st in enumerate(sorted(items, key=lambda x: menu_1st_order.index(x))): |
| 1048 | menu_block.append( |
| 1049 | '<a href="#item{}" class="nav-group" data-toggle="collapse" data-parent="#MainMenu">{}</a>' |
| 1050 | .format(idx_1st, menu_1st) |
| 1051 | ) |
| 1052 | menu_block.append('<div class="collapse" id="item{}">'.format(idx_1st)) |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 1053 | |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 1054 | if menu_1st in (Menu1st.per_job, Menu1st.engineering_per_job): |
| 1055 | key = job_summ_sort_order.index |
| 1056 | elif menu_1st == Menu1st.engineering: |
| 1057 | key = Menu2ndEng.order.index |
| 1058 | elif menu_1st == Menu1st.summary: |
| 1059 | key = Menu2ndSumm.order.index |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 1060 | else: |
kdanylov aka koder | 84de1e4 | 2017-05-22 14:00:07 +0300 | [diff] [blame] | 1061 | key = lambda x: x |
| 1062 | |
| 1063 | in_order = sorted(items[menu_1st], key=key) |
kdanylov aka koder | 3a9e5db | 2017-05-09 20:00:44 +0300 | [diff] [blame] | 1064 | |
| 1065 | for menu_2nd in in_order: |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 1066 | menu_block.append(' <a href="#content{}" class="nav-group-item">{}</a>' |
| 1067 | .format(link_idx, menu_2nd)) |
| 1068 | content_block.append('<div id="content{}">'.format(link_idx)) |
koder aka kdanilov | a732a60 | 2017-02-01 20:29:56 +0200 | [diff] [blame] | 1069 | content_block.extend(" " + x.data for x in items[menu_1st][menu_2nd]) |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 1070 | content_block.append('</div>') |
| 1071 | link_idx += 1 |
| 1072 | menu_block.append('</div>') |
| 1073 | |
| 1074 | report = report_template.replace("{{{menu}}}", ("\n" + " " * 16).join(menu_block)) |
| 1075 | report = report.replace("{{{content}}}", ("\n" + " " * 16).join(content_block)) |
kdanylov aka koder | b083333 | 2017-05-13 20:39:17 +0300 | [diff] [blame] | 1076 | report_path = ctx.rstorage.put_report(report, "index.html") |
| 1077 | ctx.rstorage.put_report(css_file, "main.css") |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 1078 | logger.info("Report is stored into %r", report_path) |