Merge "[Reports] Use timestamps on X axis in trends report"

This commit is contained in:
Jenkins 2016-09-13 14:39:37 +00:00 committed by Gerrit Code Review
commit bea20ca6ec
4 changed files with 257 additions and 200 deletions

View File

@ -167,15 +167,15 @@ def trends(tasks_results):
class Trends(object):
"""Process tasks results and make trends data.
"""Process workloads results and make trends data.
Group tasks results by their input configuration,
Group workloads results by their input configuration,
calculate statistics for these groups and prepare it
for displaying in trends HTML report.
"""
def __init__(self):
self._tasks = {}
self._data = {}
def _to_str(self, obj):
"""Convert object into string."""
@ -197,29 +197,39 @@ class Trends(object):
def add_result(self, result):
key = self._make_hash(result["key"]["kw"])
if key not in self._tasks:
name = result["key"]["name"]
self._tasks[key] = {"seq": 1,
"name": name,
"cls": name.split(".")[0],
"met": name.split(".")[1],
"data": {},
"total": None,
"atomic": [],
"stat": {},
"sla_failures": 0,
"config": json.dumps(result["key"]["kw"],
indent=2)}
else:
self._tasks[key]["seq"] += 1
if key not in self._data:
self._data[key] = {
"actions": {},
"sla_failures": 0,
"name": result["key"]["name"],
"config": json.dumps(result["key"]["kw"], indent=2)}
for sla in result["sla"]:
self._tasks[key]["sla_failures"] += not sla["success"]
self._data[key]["sla_failures"] += not sla["success"]
task = {row[0]: dict(zip(result["info"]["stat"]["cols"], row))
stat = {row[0]: dict(zip(result["info"]["stat"]["cols"], row))
for row in result["info"]["stat"]["rows"]}
ts = int(result["info"]["tstamp_start"] * 1000)
for action in stat:
# NOTE(amaretskiy): some atomic actions can be missed due to
# failures. We can ignore that because we use NVD3 lineChart()
# for displaying trends, which is safe for missed points
if action not in self._data[key]["actions"]:
self._data[key]["actions"][action] = {
"durations": {"min": [], "median": [], "90%ile": [],
"95%ile": [], "max": [], "avg": []},
"success": []}
try:
success = float(stat[action]["Success"].rstrip("%"))
except ValueError:
# Got "n/a" for some reason
success = 0
self._data[key]["actions"][action]["success"].append(
(ts, success))
for k in task:
for tgt, src in (("min", "Min (sec)"),
("median", "Median (sec)"),
("90%ile", "90%ile (sec)"),
@ -227,49 +237,44 @@ class Trends(object):
("max", "Max (sec)"),
("avg", "Avg (sec)")):
# NOTE(amaretskiy): some atomic actions can be
# missed due to failures. We can ignore that
# because we use NVD3 lineChart() for displaying
# trends, which is safe for missed points
if k not in self._tasks[key]["data"]:
self._tasks[key]["data"][k] = {"min": [],
"median": [],
"90%ile": [],
"95%ile": [],
"max": [],
"avg": [],
"success": []}
self._tasks[key]["data"][k][tgt].append(
(self._tasks[key]["seq"], task[k][src]))
try:
success = float(task[k]["Success"].rstrip("%"))
except ValueError:
# Got "n/a" for some reason
success = 0
self._tasks[key]["data"][k]["success"].append(
(self._tasks[key]["seq"], success))
self._data[key]["actions"][action]["durations"][tgt].append(
(ts, stat[action][src]))
def get_data(self):
for key, value in self._tasks.items():
total = None
for k, v in value["data"].items():
success = [("success", v.pop("success"))]
if k == "total":
total = {"values": v, "success": success}
trends = []
for wload in self._data.values():
trend = {"stat": {},
"name": wload["name"],
"cls": wload["name"].split(".")[0],
"met": wload["name"].split(".")[1],
"sla_failures": wload["sla_failures"],
"config": wload["config"],
"actions": []}
for action, data in wload["actions"].items():
action_durs = [(k, sorted(v))
for k, v in data["durations"].items()]
if action == "total":
trend.update(
{"length": len(data["success"]),
"durations": action_durs,
"success": [("success", sorted(data["success"]))]})
else:
self._tasks[key]["atomic"].append(
{"name": k, "values": list(v.items()),
"success": success})
trend["actions"].append(
{"name": action,
"durations": action_durs,
"success": [("success", sorted(data["success"]))]})
for stat, comp in (("min", charts.streaming.MinComputation()),
("max", charts.streaming.MaxComputation()),
("avg", charts.streaming.MeanComputation())):
for k, v in total["values"][stat]:
if isinstance(v, (float,) + six.integer_types):
comp.add(v)
self._tasks[key]["stat"][stat] = comp.result()
del self._tasks[key]["data"]
total["values"] = list(total["values"].items())
self._tasks[key]["total"] = total
self._tasks[key]["single"] = self._tasks[key]["seq"] < 2
return sorted(self._tasks.values(), key=lambda s: s["name"])
for k, v in trend["durations"]:
for i in v:
if isinstance(i[1], (float,) + six.integer_types):
comp.add(i[1])
trend["stat"][stat] = comp.result()
trends.append(trend)
return sorted(trends, key=lambda i: i["name"])

View File

@ -73,9 +73,9 @@ var widgetDirective = function($compile) {
.showControls(opts.controls)
.clipEdge(true);
chart.xAxis
.tickFormat(d3.format(opts.xformat || "d"))
.axisLabel(opts.xname || "")
.showMaxMin(false);
.axisLabel(opts.xname)
.tickFormat(opts.xformat)
.showMaxMin(opts.showmaxmin);
chart.yAxis
.orient("left")
.tickFormat(d3.format(opts.yformat || ",.3f"));
@ -92,9 +92,10 @@ var widgetDirective = function($compile) {
.useInteractiveGuideline(opts.guide)
.clipEdge(true);
chart.xAxis
.tickFormat(d3.format(opts.xformat || "d"))
.axisLabel(opts.xname || "")
.showMaxMin(false);
.axisLabel(opts.xname)
.tickFormat(opts.xformat)
.rotateLabels(opts.xrotate)
.showMaxMin(opts.showmaxmin);
chart.yAxis
.orient("left")
.tickFormat(d3.format(opts.yformat || ",.3f"));
@ -184,14 +185,20 @@ var widgetDirective = function($compile) {
}
}
var options = {
var opts = {
xname: attrs.nameX || "",
xformat: attrs.formatX || "d",
xrotate: attrs.rotateX || 0,
yformat: attrs.formatY || ",.3f",
controls: attrs.controls === "true",
guide: attrs.guide === "true"
guide: attrs.guide === "true",
showmaxmin: attrs.showmaxmin === "true"
};
Chart.get_chart(attrs.widget)(el, data, options, do_after);
if (attrs.formatDateX) {
opts.xformat = function(d) { return d3.time.format(attrs.formatDateX)(new Date(d)) }
} else {
opts.xformat = d3.format(attrs.formatX || "d")
}
Chart.get_chart(attrs.widget)(el, data, opts, do_after);
}
if (attrs.nameY) {

View File

@ -97,9 +97,9 @@
name: "Total",
visible: function(){ return true }
}, {
id: "atomic",
id: "actions",
name: "Atomic actions",
visible: function(){ return (! $scope.wload.single) && $scope.wload.atomic.length }
visible: function(){ return ($scope.wload.length !== 1) && $scope.wload.actions.length }
}, {
id: "config",
name: "Configuration",
@ -134,6 +134,8 @@
}
}
/* Other helpers */
$scope.showError = function(message) {
return (function (e) {
e.style.display = "block";
@ -173,8 +175,7 @@
w.order_idx = itr > 1 ? " ["+itr+"]" : ""
$scope.wload_map[w.ref] = w;
$scope.nav_map[w.ref] = cls_idx;
met.push({name:w.met, itr:itr, idx:idx, order_idx:w.order_idx,
ref:w.ref, single:w.single});
met.push({name:w.met, itr:itr, idx:idx, order_idx:w.order_idx, ref:w.ref});
prev_met = w.met;
itr += 1;
}
@ -271,7 +272,7 @@
<div class="navmet"
title="{{m.name}}{{m.order_idx}}"
ng-show="n.idx==nav_idx"
ng-class="{active:wload && m.ref==wload.ref, single:m.single}"
ng-class="{active:wload && m.ref==wload.ref, single:m.length === 1}"
ng-click="location.path(m.ref)"
ng-repeat="m in n.met track by $index"
ng-repeat-end>{{m.name}}{{m.order_idx}}</div>
@ -336,18 +337,18 @@
<tbody>
<tr ng-repeat="w in data | orderBy:ov_srt:ov_dir"
ng-click="location.path(w.ref)"
ng-class="{single:w.single}">
ng-class="{single:w.length === 1}">
<td>{{w.ref}}
<td>{{w.seq}}
<td>{{w.length}}
<td>
<span ng-if="w.single">-</span>
<span ng-if="!w.single">{{w.stat.min | number:4}}</span>
<span ng-if="w.length === 1">-</span>
<span ng-if="w.length !== 1">{{w.stat.min | number:4}}</span>
<td>
<span ng-if="w.single">-</span>
<span ng-if="!w.single">{{w.stat.max | number:4}}</span>
<span ng-if="w.length === 1">-</span>
<span ng-if="w.length !== 1">{{w.stat.max | number:4}}</span>
<td>
<span ng-if="w.single">-</span>
<span ng-if="!w.single">{{w.stat.avg | number:4}}</span>
<span ng-if="w.length === 1">-</span>
<span ng-if="w.length !== 1">{{w.stat.avg | number:4}}</span>
<td title="{{w.sla_failures ? 'Failures: ' + w.sla_failures : 'No failures'}}">
<span ng-hide="w.sla_failures" class="status-pass">&#x2714;</span>
<span ng-show="w.sla_failures" class="status-fail">&#x2716;</span>
@ -371,46 +372,58 @@
<div ng-include="tab"></div>
<script type="text/ng-template" id="total">
<div ng-if="wload.single">
<div ng-if="wload.length === 1">
<div style="margin:30px 0 10px; font-size:14px; color:#ff6622">
This workload has single run so trends can not be displayed.<br>
There should be at least two workload results with the same configuration
</div>
</div>
<div ng-if="!wload.single">
<div ng-if="wload.length !== 1">
<h2>Total durations</h2>
<div widget="Lines"
data="wload.total.values"
name-x="Task run sequence number"
data="wload.durations"
controls="true"
guide="true">
guide="true"
showmaxmin="true"
rotate-x="-70"
format-date-x="%Y-%m-%d %H:%M:%S"
style="height:370px">
</div>
<h2>Total success rate</h2>
<div widget="Lines"
data="wload.total.success"
name-x="Task run sequence number"
data="wload.success"
controls="true"
guide="true">
guide="true"
showmaxmin="true"
rotate-x="-70"
format-date-x="%Y-%m-%d %H:%M:%S"
style="height:370px">
</div>
</div>
</script>
<script type="text/ng-template" id="atomic">
<script type="text/ng-template" id="actions">
<h2>Atomic actions durations / success rate</h2>
<div ng-repeat="chart in wload.atomic track by $index">
<div ng-repeat="chart in wload.actions track by $index">
<span id="{{chart.name}}"></span>
<div class="chart-title">{{chart.name}}</div>
<div widget="Lines"
data="chart.values"
name-x="Task run sequence number"
data="chart.durations"
controls="true"
guide="true">
guide="true"
showmaxmin="true"
rotate-x="-70"
format-date-x="%Y-%m-%d %H:%M:%S"
style="height:370px">
</div>
<div widget="Lines"
data="chart.success"
name-x="Task run sequence number"
controls="true"
guide="true">
guide="true"
showmaxmin="true"
rotate-x="-70"
format-date-x="%Y-%m-%d %H:%M:%S"
style="height:400px">
</div>
</div>
</script>

View File

@ -178,7 +178,7 @@ class TrendsTestCase(test.TestCase):
def test___init__(self):
trends = plot.Trends()
self.assertEqual({}, trends._tasks)
self.assertEqual({}, trends._data)
self.assertRaises(TypeError, plot.Trends, 42)
@ddt.data({"args": [None], "result": "None"},
@ -227,11 +227,12 @@ class TrendsTestCase(test.TestCase):
atomic = {"a": 123, "b": 456}
stat_rows = [["a", 0.7, 0.85, 0.9, 0.87, 1.25, 0.67, "100.0%", 4],
["b", 0.5, 0.75, 0.85, 0.9, 1.1, 0.58, "100.0%", 4],
["total", 1.2, 1.55, 1.7, 1.9, 1.5, 1.6, "100.0%", 4]]
["total", 1.2, 1.55, 1.7, 1.8, 1.5, 0.8, "100.0%", 4]]
return {
"key": {"kw": salt + "_kw", "name": "Scenario.name_%s" % salt},
"key": {"kw": "kw_%d" % salt, "name": "Scenario.name_%d" % salt},
"sla": [{"success": sla_success}],
"info": {"iterations_count": 4, "atomic": atomic,
"tstamp_start": 123456.789 + salt,
"stat": {"rows": stat_rows,
"cols": ["Action", "Min (sec)", "Median (sec)",
"90%ile (sec)", "95%ile (sec)",
@ -240,120 +241,151 @@ class TrendsTestCase(test.TestCase):
"iterations": ["<iter-0>", "<iter-1>", "<iter-2>", "<iter-3>"]}
def _sort_trends(self, trends_result):
for r_idx, res in enumerate(trends_result):
trends_result[r_idx]["total"]["values"].sort()
for a_idx, dummy in enumerate(res["atomic"]):
trends_result[r_idx]["atomic"][a_idx]["values"].sort()
for idx in range(len(trends_result)):
trends_result[idx]["durations"].sort()
for a_idx in range(len(trends_result[idx]["actions"])):
trends_result[idx]["actions"][a_idx]["durations"].sort()
return trends_result
def test_add_result_and_get_data(self):
trends = plot.Trends()
for i in 0, 1:
trends.add_result(self._make_result(str(i)))
trends.add_result(self._make_result(i))
expected = [
{"atomic": [
{"name": "a",
"success": [("success", [(1, 100.0)])],
"values": [("90%ile", [(1, 0.9)]), ("95%ile", [(1, 0.87)]),
("avg", [(1, 0.67)]), ("max", [(1, 1.25)]),
("median", [(1, 0.85)]), ("min", [(1, 0.7)])]},
{"name": "b",
"success": [("success", [(1, 100.0)])],
"values": [("90%ile", [(1, 0.85)]), ("95%ile", [(1, 0.9)]),
("avg", [(1, 0.58)]), ("max", [(1, 1.1)]),
("median", [(1, 0.75)]), ("min", [(1, 0.5)])]}],
"cls": "Scenario", "config": "\"0_kw\"", "met": "name_0",
"name": "Scenario.name_0", "seq": 1, "single": True,
"sla_failures": 0, "stat": {"avg": 1.6, "max": 1.5, "min": 1.2},
"total": {"success": [("success", [(1, 100.0)])],
"values": [("90%ile", [(1, 1.7)]),
("95%ile", [(1, 1.9)]),
("avg", [(1, 1.6)]),
("max", [(1, 1.5)]),
("median", [(1, 1.55)]),
("min", [(1, 1.2)])]}},
{"atomic": [
{"name": "a",
"success": [("success", [(1, 100.0)])],
"values": [("90%ile", [(1, 0.9)]), ("95%ile", [(1, 0.87)]),
("avg", [(1, 0.67)]), ("max", [(1, 1.25)]),
("median", [(1, 0.85)]), ("min", [(1, 0.7)])]},
{"name": "b",
"success": [("success", [(1, 100.0)])],
"values": [("90%ile", [(1, 0.85)]), ("95%ile", [(1, 0.9)]),
("avg", [(1, 0.58)]), ("max", [(1, 1.1)]),
("median", [(1, 0.75)]), ("min", [(1, 0.5)])]}],
"cls": "Scenario", "config": "\"1_kw\"", "met": "name_1",
"name": "Scenario.name_1", "seq": 1, "single": True,
"sla_failures": 0, "stat": {"avg": 1.6, "max": 1.5, "min": 1.2},
"total": {"success": [("success", [(1, 100.0)])],
"values": [("90%ile", [(1, 1.7)]),
("95%ile", [(1, 1.9)]),
("avg", [(1, 1.6)]),
("max", [(1, 1.5)]),
("median", [(1, 1.55)]),
("min", [(1, 1.2)])]}}]
{"actions": [{"durations": [("90%ile", [(123456789, 0.9)]),
("95%ile", [(123456789, 0.87)]),
("avg", [(123456789, 0.67)]),
("max", [(123456789, 1.25)]),
("median", [(123456789, 0.85)]),
("min", [(123456789, 0.7)])],
"name": "a",
"success": [("success", [(123456789, 100.0)])]},
{"durations": [("90%ile", [(123456789, 0.85)]),
("95%ile", [(123456789, 0.9)]),
("avg", [(123456789, 0.58)]),
("max", [(123456789, 1.1)]),
("median", [(123456789, 0.75)]),
("min", [(123456789, 0.5)])],
"name": "b",
"success": [("success", [(123456789, 100.0)])]}],
"cls": "Scenario",
"config": "\"kw_0\"",
"durations": [("90%ile", [(123456789, 1.7)]),
("95%ile", [(123456789, 1.8)]),
("avg", [(123456789, 0.8)]),
("max", [(123456789, 1.5)]),
("median", [(123456789, 1.55)]),
("min", [(123456789, 1.2)])],
"length": 1,
"met": "name_0",
"name": "Scenario.name_0",
"sla_failures": 0,
"stat": {"avg": 1.425, "max": 1.8, "min": 0.8},
"success": [("success", [(123456789, 100.0)])]},
{"actions": [{"durations": [("90%ile", [(123457789, 0.9)]),
("95%ile", [(123457789, 0.87)]),
("avg", [(123457789, 0.67)]),
("max", [(123457789, 1.25)]),
("median", [(123457789, 0.85)]),
("min", [(123457789, 0.7)])],
"name": "a",
"success": [("success", [(123457789, 100.0)])]},
{"durations": [("90%ile", [(123457789, 0.85)]),
("95%ile", [(123457789, 0.9)]),
("avg", [(123457789, 0.58)]),
("max", [(123457789, 1.1)]),
("median", [(123457789, 0.75)]),
("min", [(123457789, 0.5)])],
"name": "b",
"success": [("success", [(123457789, 100.0)])]}],
"cls": "Scenario",
"config": "\"kw_1\"",
"durations": [("90%ile", [(123457789, 1.7)]),
("95%ile", [(123457789, 1.8)]),
("avg", [(123457789, 0.8)]),
("max", [(123457789, 1.5)]),
("median", [(123457789, 1.55)]),
("min", [(123457789, 1.2)])],
"length": 1,
"met": "name_1",
"name": "Scenario.name_1",
"sla_failures": 0,
"stat": {"avg": 1.425, "max": 1.8, "min": 0.8},
"success": [("success", [(123457789, 100.0)])]}]
self.assertEqual(expected, self._sort_trends(trends.get_data()))
def test_add_result_once_and_get_data(self):
trends = plot.Trends()
trends.add_result(self._make_result("foo", sla_success=False))
trends.add_result(self._make_result(42, sla_success=False))
expected = [
{"atomic": [
{"name": "a",
"success": [("success", [(1, 100.0)])],
"values": [("90%ile", [(1, 0.9)]), ("95%ile", [(1, 0.87)]),
("avg", [(1, 0.67)]), ("max", [(1, 1.25)]),
("median", [(1, 0.85)]), ("min", [(1, 0.7)])]},
{"name": "b",
"success": [("success", [(1, 100.0)])],
"values": [("90%ile", [(1, 0.85)]), ("95%ile", [(1, 0.9)]),
("avg", [(1, 0.58)]), ("max", [(1, 1.1)]),
("median", [(1, 0.75)]), ("min", [(1, 0.5)])]}],
"cls": "Scenario", "config": "\"foo_kw\"", "met": "name_foo",
"name": "Scenario.name_foo", "seq": 1, "single": True,
"sla_failures": 1, "stat": {"avg": 1.6, "max": 1.5, "min": 1.2},
"total": {"success": [("success", [(1, 100.0)])],
"values": [("90%ile", [(1, 1.7)]),
("95%ile", [(1, 1.9)]),
("avg", [(1, 1.6)]),
("max", [(1, 1.5)]),
("median", [(1, 1.55)]),
("min", [(1, 1.2)])]}}]
{"actions": [{"durations": [("90%ile", [(123498789, 0.9)]),
("95%ile", [(123498789, 0.87)]),
("avg", [(123498789, 0.67)]),
("max", [(123498789, 1.25)]),
("median", [(123498789, 0.85)]),
("min", [(123498789, 0.7)])],
"name": "a",
"success": [("success", [(123498789, 100.0)])]},
{"durations": [("90%ile", [(123498789, 0.85)]),
("95%ile", [(123498789, 0.9)]),
("avg", [(123498789, 0.58)]),
("max", [(123498789, 1.1)]),
("median", [(123498789, 0.75)]),
("min", [(123498789, 0.5)])],
"name": "b",
"success": [("success", [(123498789, 100.0)])]}],
"cls": "Scenario",
"config": "\"kw_42\"",
"durations": [("90%ile", [(123498789, 1.7)]),
("95%ile", [(123498789, 1.8)]),
("avg", [(123498789, 0.8)]),
("max", [(123498789, 1.5)]),
("median", [(123498789, 1.55)]),
("min", [(123498789, 1.2)])],
"length": 1,
"met": "name_42",
"name": "Scenario.name_42",
"sla_failures": 1,
"stat": {"avg": 1.425, "max": 1.8, "min": 0.8},
"success": [("success", [(123498789, 100.0)])]}]
self.assertEqual(expected, self._sort_trends(trends.get_data()))
def test_add_result_with_na_and_get_data(self):
trends = plot.Trends()
trends.add_result(self._make_result("foo",
sla_success=False, with_na=True))
trends.add_result(
self._make_result(42, sla_success=False, with_na=True))
expected = [
{"atomic": [{"name": "a",
"success": [("success", [(1, 0)])],
"values": [("90%ile", [(1, "n/a")]),
("95%ile", [(1, "n/a")]),
("avg", [(1, "n/a")]),
("max", [(1, "n/a")]),
("median", [(1, "n/a")]),
("min", [(1, "n/a")])]},
{"name": "b",
"success": [("success", [(1, 0)])],
"values": [("90%ile", [(1, "n/a")]),
("95%ile", [(1, "n/a")]),
("avg", [(1, "n/a")]),
("max", [(1, "n/a")]),
("median", [(1, "n/a")]),
("min", [(1, "n/a")])]}],
"cls": "Scenario", "config": "\"foo_kw\"", "met": "name_foo",
"name": "Scenario.name_foo", "seq": 1, "single": True,
"sla_failures": 1, "stat": {"avg": None, "max": None,
"min": None},
"total": {"success": [("success", [(1, 0)])],
"values": [("90%ile", [(1, "n/a")]),
("95%ile", [(1, "n/a")]),
("avg", [(1, "n/a")]),
("max", [(1, "n/a")]),
("median", [(1, "n/a")]),
("min", [(1, "n/a")])]}}]
{"actions": [{"durations": [("90%ile", [(123498789, "n/a")]),
("95%ile", [(123498789, "n/a")]),
("avg", [(123498789, "n/a")]),
("max", [(123498789, "n/a")]),
("median", [(123498789, "n/a")]),
("min", [(123498789, "n/a")])],
"name": "a",
"success": [("success", [(123498789, 0)])]},
{"durations": [("90%ile", [(123498789, "n/a")]),
("95%ile", [(123498789, "n/a")]),
("avg", [(123498789, "n/a")]),
("max", [(123498789, "n/a")]),
("median", [(123498789, "n/a")]),
("min", [(123498789, "n/a")])],
"name": "b",
"success": [("success", [(123498789, 0)])]}],
"cls": "Scenario",
"config": "\"kw_42\"",
"durations": [("90%ile", [(123498789, "n/a")]),
("95%ile", [(123498789, "n/a")]),
("avg", [(123498789, "n/a")]),
("max", [(123498789, "n/a")]),
("median", [(123498789, "n/a")]),
("min", [(123498789, "n/a")])],
"length": 1,
"met": "name_42",
"name": "Scenario.name_42",
"sla_failures": 1,
"stat": {"avg": None, "max": None, "min": None},
"success": [("success", [(123498789, 0)])]}]
self.assertEqual(expected, self._sort_trends(trends.get_data()))
def test_get_data_no_results_added(self):