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 @@