diff --git a/rally/task/processing/plot.py b/rally/task/processing/plot.py index 0f5b183206..ee1463300e 100644 --- a/rally/task/processing/plot.py +++ b/rally/task/processing/plot.py @@ -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"]) diff --git a/rally/ui/templates/task/directive_widget.js b/rally/ui/templates/task/directive_widget.js index 9acdc4feaf..d3dd24dbb5 100644 --- a/rally/ui/templates/task/directive_widget.js +++ b/rally/ui/templates/task/directive_widget.js @@ -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) { diff --git a/rally/ui/templates/task/trends.html b/rally/ui/templates/task/trends.html index 516901afeb..79ac0d55cf 100644 --- a/rally/ui/templates/task/trends.html +++ b/rally/ui/templates/task/trends.html @@ -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 @@ @@ -336,18 +337,18 @@ + ng-class="{single:w.length === 1}"> {{w.ref}} - {{w.seq}} + {{w.length}} - - - {{w.stat.min | number:4}} + - + {{w.stat.min | number:4}} - - - {{w.stat.max | number:4}} + - + {{w.stat.max | number:4}} - - - {{w.stat.avg | number:4}} + - + {{w.stat.avg | number:4}} @@ -371,46 +372,58 @@
- diff --git a/tests/unit/task/processing/test_plot.py b/tests/unit/task/processing/test_plot.py index 570cd2524b..6dbcf9cf5b 100644 --- a/tests/unit/task/processing/test_plot.py +++ b/tests/unit/task/processing/test_plot.py @@ -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": ["", "", "", ""]} 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):