diff --git a/rally-jobs/rally.yaml b/rally-jobs/rally.yaml index 65060f5271..3adeb10583 100644 --- a/rally-jobs/rally.yaml +++ b/rally-jobs/rally.yaml @@ -589,6 +589,16 @@ min: 20 max: 80 + Dummy.dummy_output: + - + runner: + type: "constant" + times: 20 + concurrency: 10 + sla: + failure_rate: + max: 0 + Dummy.dummy_with_scenario_output: - runner: diff --git a/rally/plugins/common/scenarios/dummy/dummy.py b/rally/plugins/common/scenarios/dummy/dummy.py index d6b8c5bcdb..ba9eb3df1a 100644 --- a/rally/plugins/common/scenarios/dummy/dummy.py +++ b/rally/plugins/common/scenarios/dummy/dummy.py @@ -80,6 +80,63 @@ class Dummy(scenario.Scenario): % exception_probability ) + @scenario.configure() + def dummy_output(self, random_range=25): + """Generate dummy output. + + This scenario generates example of output data. + :param random_range: max int limit for generated random values + """ + rand = lambda n: [n, random.randint(1, random_range)] + desc = "This is a description text for %s" + + self.add_output(additive={"title": "Additive Stat Table", + "description": desc % "Additive Stat Table", + "chart": "OutputStatsTable", + "items": [rand("foo stat"), rand("bar stat"), + rand("spam stat")]}) + + self.add_output(additive={"title": "Additive Foo StackedArea", + "description": ( + desc % "Additive Foo StackedArea"), + "chart": "OutputStackedAreaChart", + "items": [rand("foo 1"), rand("foo 2")]}) + + self.add_output(additive={"title": ("Additive Bar StackedArea " + "(no description)"), + "description": "", + "chart": "OutputStackedAreaChart", + "items": [rand("bar %d" % i) + for i in range(1, 7)]}) + + self.add_output(additive={"title": "Additive Spam Pie", + "description": desc % "Additive Spam Pie", + "chart": "OutputAvgChart", + "items": [rand("spam %d" % i) + for i in range(1, 4)]}, + complete={"title": "Complete StackedArea", + "description": desc % "Complete StackedArea", + "widget": "StackedArea", + "data": [ + [name, [rand(i) for i in range(30)]] + for name in ("alpha", "beta", "gamma")]}) + + self.add_output( + complete={"title": "Complete Pie (no description)", + "description": "", + "widget": "Pie", + "data": [rand("delta"), rand("epsilon"), rand("zeta"), + rand("theta"), rand("lambda"), rand("omega")]}) + + data = {"cols": ["mu column", "xi column", "pi column", + "tau column", "chi column"], + "rows": [([name + " row"] + [rand(i)[1] for i in range(4)]) + for name in ("iota", "nu", "rho", "phi", "psi")]} + self.add_output(complete={"title": "Complete Table", + "description": desc % "Complete Table", + "widget": "Table", + "data": data}) + @scenario.configure() def dummy_with_scenario_output(self): """Return a dummy scenario output. diff --git a/rally/ui/templates/base.html b/rally/ui/templates/base.html index cfc364ec8b..6fda35ac5c 100644 --- a/rally/ui/templates/base.html +++ b/rally/ui/templates/base.html @@ -12,8 +12,8 @@ p { margin:0; padding:5px 0 } p.thesis { padding:10px 0 } h1 { color:#666; margin:0 0 20px; font-size:30px; font-weight:normal } - h2 { color:#777; margin:20px 0 10px; font-size:25px; font-weight:normal } - h3 { color:#666; margin:13px 0 4px; font-size:18px; font-weight:normal } + h2, .h2 { color:#666; margin:24px 0 6px; font-size:25px; font-weight:normal } + h3, .h3 { color:#777; margin:12px 0 4px; font-size:18px; font-weight:normal } table { border-collapse:collapse; border-spacing:0; width:100%; font-size:12px; margin:0 0 10px } table th { text-align:left; padding:8px; color:#000; border:2px solid #ddd; border-width:0 0 2px 0 } table th.sortable { cursor:pointer } @@ -21,7 +21,6 @@ table.compact td { padding:4px 8px } table.striped tr:nth-child(odd) td { background:#f9f9f9 } table.linked tbody tr:hover { background:#f9f9f9; cursor:pointer } - .richcolor td { color:#036; font-weight:bold } .rich, .rich td { font-weight:bold } .code { padding:10px; font-size:13px; color:#333; background:#f6f6f6; border:1px solid #e5e5e5; border-radius:4px } diff --git a/rally/ui/templates/task/report.html b/rally/ui/templates/task/report.html index 13bfa81b3c..0ea7c92dc9 100644 --- a/rally/ui/templates/task/report.html +++ b/rally/ui/templates/task/report.html @@ -23,46 +23,41 @@ {% endblock %} {% block js_before %} +{% raw %} "use strict"; if (typeof angular === "object") { angular.module("TaskApp", []).controller( "TaskController", ["$scope", "$location", function($scope, $location) { - +{% endraw %} + $scope.source = {{ source }}; + $scope.scenarios = {{ data }}; +{% raw %} $scope.location = { - /* This is a junior brother of angular's $location, that allows non-`#' - symbol in uri, like `#/path/hash' instead of `#/path#hash' */ - _splitter: "/", + /* #/path/hash/sub/div */ normalize: function(str) { /* Remove unwanted characters from string */ if (typeof str !== "string") { return "" } return str.replace(/[^\w\-\.]/g, "") }, - _parseUri: function(uriStr) { - /* :returns: {path:string, hash:string} */ - var self = this; - var obj = {path: "", hash: ""}; - angular.forEach(uriStr.split(self._splitter), function(v){ - var s = self.normalize(v); - if (! s) { return } - if (! this.path) { this.path = s } else if (! this.hash) { this.hash = s } - }, obj) - return obj - }, uri: function(obj) { /* Getter/Setter */ - if (! obj) { return this._parseUri($location.url()) } - if (obj.path && obj.hash) { - $location.url(obj.path + this._splitter + obj.hash) - } else if (obj.path) { - $location.url(obj.path) - } else { - $location.url("/") + if (! obj) { + var uri = {path: "", hash: "", sub: "", div: ""}; + var arr = ["div", "sub", "hash", "path"]; + angular.forEach($location.url().split("/"), function(value){ + var v = $scope.location.normalize(value); + if (v) { var k = arr.pop(); if (k) { this[k] = v }} + }, uri); + return uri } + var arr = [obj.path, obj.hash, obj.sub, obj.div], res = []; + for (var i in arr) { if (! arr[i]) { break }; res.push(arr[i]) } + return $location.url("/" + res.join("/")) }, path: function(path, hash) { /* Getter/Setter */ - var uri = this.uri(); if (path === "") { return this.uri({}) } path = this.normalize(path); + var uri = this.uri(); if (! path) { return uri.path } uri.path = path; var _hash = this.normalize(hash); @@ -71,23 +66,27 @@ }, hash: function(hash) { /* Getter/Setter */ - var uri = this.uri(); - if (! hash) { return uri.hash } - return this.uri({path:uri.path, hash:hash}) + if (hash) { this.uri({path:this.uri().path, hash:hash}) } + return this.uri().hash } } /* Dispatch */ $scope.route = function(uri) { - if (! ($scope.scenarios && $scope.scenarios.length)) { - return - } + if (! $scope.scenarios_map) { return } if (uri.path in $scope.scenarios_map) { $scope.view = {is_scenario:true}; $scope.scenario = $scope.scenarios_map[uri.path]; $scope.nav_idx = $scope.nav_map[uri.path]; - $scope.showTab(uri.hash); + if ($scope.scenario.iterations.histogram.views.length) { + $scope.mainHistogram = $scope.scenario.iterations.histogram.views[0] + } + if ($scope.scenario.atomic.histogram.views.length) { + $scope.atomicHistogram = $scope.scenario.atomic.histogram.views[0] + } + $scope.outputIteration = 0; + $scope.showTab(uri); } else { $scope.scenario = null; if (uri.path === "source") { @@ -102,11 +101,7 @@ $scope.route($scope.location.uri()) }); - /* Navigation */ - - $scope.showNav = function(nav_idx) { - $scope.nav_idx = nav_idx - } + $scope.showNav = function(nav_idx) { $scope.nav_idx = nav_idx } /* Tabs */ @@ -121,8 +116,8 @@ visible: function(){ return !! $scope.scenario.atomic.pie.length } },{ id: "output", - name: "Output", - visible: function(){ return !! $scope.scenario.additive_output.length } + name: "Scenario Data", + visible: function(){ return $scope.scenario.output.length } },{ id: "failures", name: "Failures", @@ -137,19 +132,33 @@ angular.forEach($scope.tabs, function(tab){ this[tab.id] = tab }, $scope.tabs_map); - $scope.showTab = function(tab_id) { - $scope.tab = tab_id in $scope.tabs_map ? tab_id : "overview" + $scope.showTab = function(uri) { + $scope.tab = uri.hash in $scope.tabs_map ? uri.hash : "overview"; + if (! $scope.scenario.output) { + var has_additive = !! $scope.scenario.additive_output.length; + var has_complete = !! ($scope.scenario.complete_output.length + && $scope.scenario.complete_output[0].length); + $scope.scenario.output = { + has_additive: has_additive, + has_complete: has_complete, + length: has_additive + has_complete, + active: has_additive ? "additive" : (has_complete ? "complete" : "") + } + } + if (uri.hash === "output") { + if (uri.sub && $scope.scenario.output["has_" + uri.sub]) { + $scope.scenario.output.active = uri.sub + } + } } for (var i in $scope.tabs) { if ($scope.tabs[i].id === $scope.location.hash()) { $scope.tab = $scope.tabs[i].id } - $scope.tabs[i].isVisible = function(){ + $scope.tabs[i].isVisible = function() { if ($scope.scenario) { - if (this.visible()) { - return true - } + if (this.visible()) { return true } /* If tab should be hidden but is selected - show another one */ if (this.id === $scope.location.hash()) { for (var i in $scope.tabs) { @@ -165,149 +174,6 @@ } } - /* Charts */ - - var Charts = { - _render: function(selector, data, chart){ - nv.addGraph(function() { - d3.select(selector) - .datum(data) - .transition() - .duration(0) - .call(chart); - nv.utils.windowResize(chart.update) - }) - }, - /* NOTE(amaretskiy): this is actually a result of - d3.scale.category20().range(), excluding red color (#d62728) - which is reserved for errors */ - _colors: ["#1f77b4", "#aec7e8", "#ff7f0e", "#ffbb78", "#2ca02c", - "#98df8a", "#ff9896", "#9467bd", "#c5b0d5", "#8c564b", - "#c49c94", "#e377c2", "#f7b6d2", "#7f7f7f", "#c7c7c7", - "#bcbd22", "#dbdb8d", "#17becf", "#9edae5"], - pie: function(selector, data){ - var chart = nv.models.pieChart() - .x(function(d) { return d.key }) - .y(function(d) { return d.values }) - .showLabels(true) - .labelType("percent") - .donut(true) - .donutRatio(0.25) - .donutLabelsOutside(true) - .color(function(d){ - if (d.data && d.data.color) { - return d.data.color - } - }); - - var data_ = [], - colors = [], - colors_map = {errors: "#d62728"}; - for (var i in data) { - var key = data[i][0]; - if (! (key in colors_map)) { - if (! colors.length) { colors = this._colors.slice() } - colors_map[key] = colors.shift() - } - data_.push({key:key, values:data[i][1], color:colors_map[key]}) - } - this._render(selector, data_, chart) - }, - stack: function(selector, data, conf){ - var chart = nv.models.stackedAreaChart() - .x(function(d) { return d[0] }) - .y(function(d) { return d[1] }) - .clipEdge(true) - .showControls(conf.controls) - .useInteractiveGuideline(conf.guide); - chart.xAxis - .axisLabel(conf.xLabel || "") - .showMaxMin(false) - .tickFormat(d3.format(conf.xFormat || "d")); - chart.yAxis - .axisLabel(conf.yLabel || "") - .tickFormat(d3.format(conf.yFormat || ",.3f")); - var data_ = []; - for (var i in data) { - var d = {key:data[i][0], values:data[i][1]}; - if (d.key === "failed_duration") { - d.color = "#d62728" - } - data_.push(d) - } - this._render(selector, data_, chart) - }, - histogram: function(selector, data){ - var chart = nv.models.multiBarChart() - .reduceXTicks(true) - .showControls(false) - .transitionDuration(0) - .groupSpacing(0.05); - chart.legend - .radioButtonMode(true) - chart.xAxis - .axisLabel("Duration (seconds)") - .tickFormat(d3.format(",.2f")); - chart.yAxis - .axisLabel("Iterations (frequency)") - .tickFormat(d3.format("d")); - this._render(selector, data, chart) - } - }; - - $scope.renderTotal = function() { - if (! $scope.scenario) { - return - } - - Charts.stack( - "#total-stack", $scope.scenario.iterations.iter, - {xLabel: "Iteration sequence number", - controls: true, - guide: true}); - - if ($scope.scenario.load_profile.length) { - Charts.stack( - "#load-profile-stack", - $scope.scenario.load_profile, - {xLabel: "Timeline (seconds)", - xFormat: ",.2f", yFormat: "d"}) - } - - Charts.pie("#total-pie", $scope.scenario.iterations.pie); - - if ($scope.scenario.iterations.histogram.data.length) { - var idx = this.totalHistogramModel.id; - Charts.histogram("#total-histogram", - $scope.scenario.iterations.histogram.data[idx]) - } - } - - $scope.renderDetails = function() { - if (! $scope.scenario) { - return - } - Charts.stack( - "#atomic-stack", - $scope.scenario.atomic.iter, - {xLabel: "Iteration sequence number", - controls: true, - guide: true}); - if ($scope.scenario.atomic.pie) { - Charts.pie("#atomic-pie", $scope.scenario.atomic.pie) - } - if ($scope.scenario.atomic.histogram.data.length) { - var idx = this.atomicHistogramModel.id; - Charts.histogram("#atomic-histogram", $scope.scenario.atomic.histogram.data[idx]) - } - } - - $scope.renderOutput = function() { - if ($scope.scenario && $scope.scenario.additive_output.length) { - Charts.stack("#output-stack", $scope.scenario.additive_output[0].data, {}) - } - } - $scope.showError = function(message) { return (function (e) { e.style.display = "block"; @@ -318,23 +184,16 @@ /* Initialization */ angular.element(document).ready(function(){ - $scope.source = {{ source }}; - $scope.scenarios = {{ data }}; if (! $scope.scenarios.length) { return $scope.showError("No data...") } - $scope.totalHistogramModel = {label:'', id:0}; - $scope.atomicHistogramModel = {label:'', id:0}; /* Compose data mapping */ $scope.nav = []; $scope.nav_map = {}; $scope.scenarios_map = {}; - var scenario_ref = $scope.location.path(); - var met = []; - var itr = 0; - var cls_idx = 0; + var met = [], itr = 0, cls_idx = 0; var prev_cls, prev_met; for (var idx in $scope.scenarios) { @@ -350,21 +209,13 @@ cls_idx += 1 } - if (prev_met !== sc.met) { - itr = 1 - } - + if (prev_met !== sc.met) { itr = 1 }; sc.ref = $scope.location.normalize(sc.cls+"."+sc.met+(itr > 1 ? "-"+itr : "")); $scope.scenarios_map[sc.ref] = sc; $scope.nav_map[sc.ref] = cls_idx; - var current_ref = $scope.location.path(); - if (sc.ref === current_ref) { - scenario_ref = sc.ref - } - met.push({name:sc.name, itr:itr, idx:idx, ref:sc.ref}); prev_met = sc.met; - itr += 1 + itr += 1; } if (met.length) { @@ -374,11 +225,161 @@ /* Start */ var uri = $scope.location.uri(); - uri.path = scenario_ref; + uri.path = $scope.location.path(); $scope.route(uri); $scope.$digest() }); - }])} + }]) + .directive("widget", function($compile) { + + var Chart = { + _render: function(node, data, chart){ + nv.addGraph(function() { + d3.select(node).datum(data).transition().duration(0).call(chart); + nv.utils.windowResize(chart.update) + }) + }, + /* NOTE(amaretskiy): this is actually a result of + d3.scale.category20().range(), excluding red color (#d62728) + which is reserved for errors */ + _colors: ["#1f77b4", "#aec7e8", "#ff7f0e", "#ffbb78", "#2ca02c", + "#98df8a", "#ff9896", "#9467bd", "#c5b0d5", "#8c564b", + "#c49c94", "#e377c2", "#f7b6d2", "#7f7f7f", "#c7c7c7", + "#bcbd22", "#dbdb8d", "#17becf", "#9edae5"], + _widgets: { + Pie: "pie", + StackedArea: "stack", + Histogram: "histogram" + }, + get_chart: function(widget) { + if (widget in this._widgets) { + var name = this._widgets[widget]; + return Chart[name] + } + return function() { console.log("Error: unexpected widget:", widget) } + }, + pie: function(node, data) { + var chart = nv.models.pieChart() + .x(function(d) { return d.key }) + .y(function(d) { return d.values }) + .showLabels(true) + .labelType("percent") + .donut(true) + .donutRatio(0.25) + .donutLabelsOutside(true) + .color(function(d){ + if (d.data && d.data.color) { return d.data.color } + }); + + var data_ = [], colors = [], colors_map = {errors: "#d62728"}; + for (var i in data) { + var key = data[i][0]; + if (! (key in colors_map)) { + if (! colors.length) { colors = Chart._colors.slice() } + colors_map[key] = colors.shift() + } + data_.push({key:key, values:data[i][1], color:colors_map[key]}) + } + Chart._render(node, data_, chart) + }, + stack: function(node, data, opts) { + var chart = nv.models.stackedAreaChart() + .x(function(d) { return d[0] }) + .y(function(d) { return d[1] }) + .useInteractiveGuideline(opts.guide) + .showControls(opts.controls) + .clipEdge(true); + chart.xAxis + .tickFormat(d3.format(opts.xformat || "d")) + .axisLabel(opts.xname || "") + .showMaxMin(false); + chart.yAxis + .tickFormat(d3.format(opts.yformat || ",.3f")) + .axisLabel(opts.yname || ""); + var data_ = []; + for (var i in data) { + var d = {key:data[i][0], values:data[i][1]}; + if (d.key === "failed_duration") { + d.color = "#d62728" + } + data_.push(d) + } + Chart._render(node, data_, chart) + }, + histogram: function(node, data) { + var chart = nv.models.multiBarChart() + .reduceXTicks(true) + .showControls(false) + .transitionDuration(0) + .groupSpacing(0.05); + chart + .legend.radioButtonMode(true); + chart.xAxis + .axisLabel("Duration (seconds)") + .tickFormat(d3.format(",.2f")); + chart.yAxis + .axisLabel("Iterations (frequency)") + .tickFormat(d3.format("d")); + Chart._render(node, data, chart) + } + }; + + return { + restrict: "A", + scope: { data: "=" }, + link: function(scope, element, attrs) { + scope.$watch("data", function(data) { + if (! data) { return console.log("Chart has no data to render!") } + if (attrs.widget === "Table") { + var ng_class = attrs.lastrowClass ? " ng-class='{"+attrs.lastrowClass+":$last}'" : ""; + var template = "
{{i}} |
---|
{{i}}" + + " |