add yardstick iruya 9.0.0 release notes
[yardstick.git] / yardstick / benchmark / core / report.py
1 ##############################################################################
2 # Copyright (c) 2017 Rajesh Kudaka <4k.rajesh@gmail.com>
3 # Copyright (c) 2018-2019 Intel Corporation.
4 #
5 # All rights reserved. This program and the accompanying materials
6 # are made available under the terms of the Apache License, Version 2.0
7 # which accompanies this distribution, and is available at
8 # http://www.apache.org/licenses/LICENSE-2.0
9 ##############################################################################
10
11 """ Handler for yardstick command 'report' """
12
13 import re
14 import six
15 import uuid
16
17 import jinja2
18 from api.utils import influx
19 from oslo_utils import uuidutils
20 from yardstick.common import constants as consts
21 from yardstick.common.utils import cliargs
22
23
24 class JSTree(object):
25     """Data structure to parse data for use with the JS library jsTree"""
26     def __init__(self):
27         self._created_nodes = ['#']
28         self.jstree_data = []
29
30     def _create_node(self, _id):
31         """Helper method for format_for_jstree to create each node.
32
33         Creates the node (and any required parents) and keeps track
34         of the created nodes.
35
36         :param _id: (string) id of the node to be created
37         :return: None
38         """
39         components = _id.split(".")
40
41         if len(components) == 1:
42             text = components[0]
43             parent_id = "#"
44         else:
45             text = components[-1]
46             parent_id = ".".join(components[:-1])
47             # make sure the parent has been created
48             if not parent_id in self._created_nodes:
49                 self._create_node(parent_id)
50
51         self.jstree_data.append({"id": _id, "text": text, "parent": parent_id})
52         self._created_nodes.append(_id)
53
54     def format_for_jstree(self, data):
55         """Format the data into the required format for jsTree.
56
57         The data format expected is a list of metric names e.g.:
58
59             ['tg__0.DropPackets', 'tg__0.LatencyAvg.5']
60
61         This data is converted into the format required for jsTree to group and
62         display the metrics in a hierarchial fashion, including creating a
63         number of parent nodes e.g.::
64
65             [{"id": "tg__0", "text": "tg__0", "parent": "#"},
66              {"id": "tg__0.DropPackets", "text": "DropPackets", "parent": "tg__0"},
67              {"id": "tg__0.LatencyAvg", "text": "LatencyAvg", "parent": "tg__0"},
68              {"id": "tg__0.LatencyAvg.5", "text": "5", "parent": "tg__0.LatencyAvg"},]
69
70         :param data: (list) data to be converted
71         :return: list
72         """
73         self._created_nodes = ['#']
74         self.jstree_data = []
75
76         for metric in data:
77             self._create_node(metric)
78
79         return self.jstree_data
80
81
82 class Report(object):
83     """Report commands.
84
85     Set of commands to manage reports.
86     """
87
88     def __init__(self):
89         self.Timestamp = []
90         self.yaml_name = ""
91         self.task_id = ""
92
93     def _validate(self, yaml_name, task_id):
94         if re.match(r"^[\w-]+$", yaml_name):
95             self.yaml_name = yaml_name
96         else:
97             raise ValueError("invalid yaml_name", yaml_name)
98
99         if uuidutils.is_uuid_like(task_id):
100             task_id = '{' + task_id + '}'
101             task_uuid = (uuid.UUID(task_id))
102             self.task_id = task_uuid
103         else:
104             raise ValueError("invalid task_id", task_id)
105
106     def _get_fieldkeys(self):
107         fieldkeys_cmd = "show field keys from \"%s\""
108         fieldkeys_query = fieldkeys_cmd % (self.yaml_name)
109         query_exec = influx.query(fieldkeys_query)
110         if query_exec:
111             return query_exec
112         else:
113             raise KeyError("Test case not found.")
114
115     def _get_metrics(self):
116         metrics_cmd = "select * from \"%s\" where task_id = '%s'"
117         metrics_query = metrics_cmd % (self.yaml_name, self.task_id)
118         query_exec = influx.query(metrics_query)
119         if query_exec:
120             return query_exec
121         else:
122             raise KeyError("Task ID or Test case not found.")
123
124     def _get_task_start_time(self):
125         # The start time should come from the task or the metadata table.
126         # The first entry into influx for a task will be AFTER the first TC
127         # iteration
128         cmd = "select * from \"%s\" where task_id='%s' ORDER BY time ASC limit 1"
129         task_query = cmd % (self.yaml_name, self.task_id)
130
131         query_exec = influx.query(task_query)
132         start_time = query_exec[0]['time']
133         return start_time
134
135     def _get_task_end_time(self):
136         # NOTE(elfoley): when using select first() and select last() for the
137         # DB query, the timestamp returned is 0, so later queries try to
138         # return metrics from 1970
139         cmd = "select * from \"%s\" where task_id='%s' ORDER BY time DESC limit 1"
140         task_query = cmd % (self.yaml_name, self.task_id)
141         query_exec = influx.query(task_query)
142         end_time = query_exec[0]['time']
143         return end_time
144
145     def _get_baro_metrics(self):
146         start_time = self._get_task_start_time()
147         end_time = self._get_task_end_time()
148         metric_list = [
149                 "cpu_value", "cpufreq_value", "intel_pmu_value",
150                  "virt_value", "memory_value"]
151         metrics = {}
152         times = []
153         query_exec = {}
154         for metric in metric_list:
155             cmd = "select * from \"%s\" where time >= '%s' and time <= '%s'"
156             query = cmd % (metric, start_time, end_time)
157             query_exec[metric] = influx.query(query, db='collectd')
158             print("query_exec: {}".format(query_exec))
159
160         for metric in query_exec:
161             print("metric in query_exec: {}".format(metric))
162             met_values = query_exec[metric]
163             print("met_values: {}".format(met_values))
164             for x in met_values:
165                 x['name'] = metric
166                 metric_name = str('.'.join(
167                     [x[f] for f in [
168                         'host', 'name', 'type', 'type_instance', 'instance'
169                          ] if x.get(f)]))
170
171                 if not metrics.get(metric_name):
172                     metrics[metric_name] = {}
173                 metric_time = self._get_trimmed_timestamp(x['time'])
174                 times.append(metric_time)
175                 time = metric_time
176                 metrics[metric_name][time] = x['value']
177
178         times = sorted(list(set(times)))
179
180         metrics['Timestamp'] = times
181         print("metrics: {}".format(metrics))
182         return metrics
183
184     def _get_trimmed_timestamp(self, metric_time, resolution=4):
185         if not isinstance(metric_time, str):
186             metric_time = metric_time.encode('utf8') # PY2: unicode to str
187         metric_time = metric_time[11:]               # skip date, keep time
188         head, _, tail = metric_time.partition('.')   # split HH:MM:SS & nsZ
189         metric_time = head + '.' + tail[:resolution] # join HH:MM:SS & .us
190         return metric_time
191
192     def _get_timestamps(self, metrics, resolution=6):
193         # Extract the timestamps from a list of metrics
194         timestamps = []
195         for metric in metrics:
196             metric_time = self._get_trimmed_timestamp(
197                 metric['time'], resolution)
198             timestamps.append(metric_time)               # HH:MM:SS.micros
199         return timestamps
200
201     def _format_datasets(self, metric_name, metrics):
202         values = []
203         for metric in metrics:
204             val = metric.get(metric_name, None)
205             if val is None:
206                 # keep explicit None or missing entry as is
207                 pass
208             elif isinstance(val, (int, float)):
209                 # keep plain int or float as is
210                 pass
211             elif six.PY2 and isinstance(val,
212                         long):  # pylint: disable=undefined-variable
213                 # PY2: long value would be rendered with trailing L,
214                 # which JS does not support, so convert it to float
215                 val = float(val)
216             elif isinstance(val, six.string_types):
217                 s = val
218                 if not isinstance(s, str):
219                     s = s.encode('utf8')            # PY2: unicode to str
220                 try:
221                     # convert until failure
222                     val = s
223                     val = float(s)
224                     val = int(s)
225                     if six.PY2 and isinstance(val,
226                                 long):  # pylint: disable=undefined-variable
227                         val = float(val)            # PY2: long to float
228                 except ValueError:
229                     # value may have been converted to a number
230                     pass
231                 finally:
232                     # if val was not converted into a num, then it must be
233                     # text, which shouldn't end up in the report
234                     if isinstance(val, six.string_types):
235                         val = None
236             else:
237                 raise ValueError("Cannot convert %r" % val)
238             values.append(val)
239         return values
240
241     @cliargs("task_id", type=str, help=" task id", nargs=1)
242     @cliargs("yaml_name", type=str, help=" Yaml file Name", nargs=1)
243     def _generate_common(self, args):
244         """Actions that are common to both report formats.
245
246         Create the necessary data structure for rendering
247         the report templates.
248         """
249         self._validate(args.yaml_name[0], args.task_id[0])
250
251         db_fieldkeys = self._get_fieldkeys()
252         # list of dicts of:
253         # - PY2: unicode key and unicode value
254         # - PY3: str key and str value
255
256         db_metrics = self._get_metrics()
257         # list of dicts of:
258         # - PY2: unicode key and { None | unicode | float | long | int } value
259         # - PY3: str key and { None | str | float | int } value
260
261         # extract fieldKey entries, and convert them to str where needed
262         field_keys = [key if isinstance(key, str)       # PY3: already str
263                           else key.encode('utf8')       # PY2: unicode to str
264                       for key in
265                           [field['fieldKey']
266                            for field in db_fieldkeys]]
267
268         # extract timestamps
269         self.Timestamp = self._get_timestamps(db_metrics)
270
271         # prepare return values
272         datasets = []
273         table_vals = {'Timestamp': self.Timestamp}
274
275         # extract and convert field values
276         for key in field_keys:
277             values = self._format_datasets(key, db_metrics)
278             datasets.append({'label': key, 'data': values})
279             table_vals[key] = values
280
281         return datasets, table_vals
282
283     @cliargs("task_id", type=str, help=" task id", nargs=1)
284     @cliargs("yaml_name", type=str, help=" Yaml file Name", nargs=1)
285     def generate(self, args):
286         """Start report generation."""
287         datasets, table_vals = self._generate_common(args)
288
289         template_dir = consts.YARDSTICK_ROOT_PATH + "yardstick/common"
290         template_environment = jinja2.Environment(
291             autoescape=False,
292             loader=jinja2.FileSystemLoader(template_dir))
293
294         context = {
295             "datasets": datasets,
296             "Timestamps": self.Timestamp,
297             "task_id": self.task_id,
298             "table": table_vals,
299         }
300
301         template_html = template_environment.get_template("report.html.j2")
302
303         with open(consts.DEFAULT_HTML_FILE, "w") as file_open:
304             file_open.write(template_html.render(context))
305
306         print("Report generated. View %s" % consts.DEFAULT_HTML_FILE)
307
308     def _combine_times(self, *args):
309         times = []
310         # Combines an arbitrary number of lists
311         [times.extend(x) for x in args]
312         times = list(set(times))
313         times.sort()
314         return times
315
316     def _combine_metrics(self, *args):
317         baro_data, baro_time, yard_data, yard_time = args
318         combo_time = self._combine_times(baro_time, yard_time)
319
320         data = {}
321         [data.update(x) for x in (baro_data, yard_data)]
322
323         table_data = {}
324         table_data['Timestamp'] = combo_time
325         combo = {}
326         keys = sorted(data.keys())
327         for met_name in data:
328             dataset = []
329             for point in data[met_name]:
330                  dataset.append({'x': point, 'y': data[met_name][point]})
331             # the metrics need to be ordered by time
332             combo[met_name] = sorted(dataset, key=lambda i: i['x'])
333         for met_name in data:
334             table_data[met_name] = []
335             for t in combo_time:
336                 table_data[met_name].append(data[met_name].get(t, ''))
337         return combo, keys, table_data
338
339     @cliargs("task_id", type=str, help=" task id", nargs=1)
340     @cliargs("yaml_name", type=str, help=" Yaml file Name", nargs=1)
341     def generate_nsb(self, args):
342         """Start NSB report generation."""
343         _, report_data = self._generate_common(args)
344         report_time = report_data.pop('Timestamp')
345         report_meta = {
346             "testcase": self.yaml_name,
347             "task_id": self.task_id,
348         }
349
350         yardstick_data = {}
351         for i, t in enumerate(report_time):
352             for m in report_data:
353                 if not yardstick_data.get(m):
354                    yardstick_data[m] = {}
355                 yardstick_data[m][t] = report_data[m][i]
356
357         baro_data = self._get_baro_metrics()
358         baro_timestamps = baro_data.pop('Timestamp')
359
360         yard_timestamps = report_time
361         report_time = self._combine_times(yard_timestamps, baro_timestamps)
362
363         combo_metrics, combo_keys, combo_table = self._combine_metrics(
364             baro_data, baro_timestamps, yardstick_data, yard_timestamps)
365         combo_time = self._combine_times(baro_timestamps, yard_timestamps)
366         combo_tree = JSTree().format_for_jstree(combo_keys)
367
368         template_dir = consts.YARDSTICK_ROOT_PATH + "yardstick/common"
369         template_environment = jinja2.Environment(
370             autoescape=False,
371             loader=jinja2.FileSystemLoader(template_dir),
372             lstrip_blocks=True)
373
374         combo_data = combo_metrics
375         context = {
376             "report_meta": report_meta,
377             "report_data": combo_data,
378             "report_time": combo_time,
379             "report_keys": combo_keys,
380             "report_tree": combo_tree,
381             "table_data": combo_table,
382         }
383
384         template_html = template_environment.get_template("nsb_report.html.j2")
385
386         with open(consts.DEFAULT_HTML_FILE, "w") as file_open:
387             file_open.write(template_html.render(context))
388
389         print("Report generated. View %s" % consts.DEFAULT_HTML_FILE)