Add task result graphic ploter command

$ rally task plot2html --task-id <uuid> [--out filename.html] [--open]

* Get results of task with <uuid>
* Process it
* Write results to filename.html if it is specified or
  to the <uuid>.html
* If --open is specified file with results will be opened
  in your web browser

In HTML file with results we have:
* Select box that allows to specify benchmark from task
* Dump of JSON config for this task
* Stacked area graphic with duration and idle_duration for every iteration
* Pie with ratio errors/success
* Stacked area graphic with duration of all actions
* Pie with ration between avarage time of actions

This is only first step. In future we should:
* Add results in text represenation (e.g. our CLI task detailed)
* Add raw results
* Add histogram graphic that will show probability / duration for duration and atomic
  actions.

bp data-processing

Change-Id: I1168c6c85816bb26ef1637a385c8ccc558d8d1d7
This commit is contained in:
Boris Pavlovic
2014-02-12 19:41:20 +04:00
parent 3c05953eea
commit 22a6ffd3b5
7 changed files with 457 additions and 0 deletions

View File

View File

@@ -0,0 +1,123 @@
# Copyright 2014: Mirantis Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import json
import os
import mako.template
def _process_main_time(result):
pie = filter(lambda t: not t["error"], result["result"])
stacked_area = map(
lambda t: {"idle_time": 0, "time": 0} if t["error"] else t,
result["result"])
return {
"pie": [
{"key": "success", "value": len(pie)},
{"key": "errors",
"value": len(result["result"]) - len(pie)}
],
"iter": [
{
"key": "duration",
"values": [[i + 1, v["time"]]
for i, v in enumerate(stacked_area)]
},
{
"key": "idle_duration",
"values": [[i + 1, v["idle_time"]]
for i, v in enumerate(stacked_area)]
}
]
}
def _process_atomic_time(result):
def avg(lst, key=None):
lst = lst if not key else map(lambda x: x[key], lst)
return sum(lst) / float(len(lst))
# NOTE(boris-42): In our result["result"] we have next structure:
# {"error": NoneOrDict,
# "atomic_actions_time": [
# {"action": String, "duration": Float},
# ...
# ]}
# Our goal is to get next structure:
# [{"key": $atomic_actions_time.action,
# "values": [[order, $atomic_actions_time.duration
# if not $error else 0], ...}]
#
# Order of actions in "atomic_action_time" is similiar for
# all iteration. So we should take first non "error"
# iteration. And get in atomitc_iter list:
# [{"key": "action", "values":[]}]
stacked_area = []
for r in result["result"]:
if not r["error"]:
for action in r["atomic_actions_time"]:
stacked_area.append({"key": action["action"], "values": []})
break
# NOTE(boris-42): pie is similiar to stacked_area, only difference is in
# structure of values. In case of $error we shouldn't put
# anything in pie. In case of non error we should put just
# $atomic_actions_time.duration (without order)
pie = []
if stacked_area:
pie = copy.deepcopy(stacked_area)
for i, data in enumerate(result["result"]):
# in case of error put (order, 0.0) to all actions of stacked area
if data["error"]:
for k in range(len(stacked_area)):
stacked_area[k]["values"].append([i + 1, 0.0])
continue
# in case of non error put real durations to pie and stacked area
for j, action in enumerate(data["atomic_actions_time"]):
pie[j]["values"].append(action["duration"])
stacked_area[j]["values"].append([i + 1, action["duration"]])
return {
"iter": stacked_area,
"pie": map(lambda x: {"key": x["key"], "value": avg(x["values"])}, pie)
}
def _process_results(results):
output = []
for result in results:
info = result["key"]
output.append({
"name": "%s (task #%d)" % (info["name"], info["pos"]),
"config": info["kw"],
"time": _process_main_time(result),
"atomic": _process_atomic_time(result)
})
return output
def plot(results):
results = _process_results(results)
abspath = os.path.dirname(__file__)
with open("%s/src/index.mako" % abspath) as index:
template = mako.template.Template(index.read())
return template.render(data=json.dumps(results),
tasks=map(lambda r: r["name"], results))

View File

@@ -0,0 +1,157 @@
<html>
<head>
<link href="https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.1.13-beta/nv.d3.min.css"
rel="stylesheet"
type="text/css" />
<!-- Remove jQuery and use d3.select in futuer -->
<script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.0/jquery.min.js"
charset="utf-8">
</script>
<script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.1/d3.min.js"
charset="utf-8">
</script>
<script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.1.13-beta/nv.d3.min.js"
charset="utf-8">
</script>
<style>
#task_choser select {
width: 700px;
}
#results svg{
height: 350px;
width: 650px;
float: left;
}
#results svg.pie{
width: 350px;
}
div.atomic {
clear: both;
}
#results {
min-width: 1000px;
overflow: scroll;
}
</style>
<script>
var DATA = ${data}
function draw_stacked(where, source){
nv.addGraph(function() {
var chart = nv.models.stackedAreaChart()
.x(function(d) { return d[0] })
.y(function(d) { return d[1] })
.margin({left: 75})
.useInteractiveGuideline(true)
.clipEdge(true);
chart.xAxis
.axisLabel("Iteration (order number of method's call)")
.showMaxMin(false)
.tickFormat(d3.format('d'));
chart.yAxis
.axisLabel("Duration (seconds)")
.tickFormat(d3.format(',.2f'));
d3.select(where)
.datum(source)
.transition().duration(500).call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
}
function draw_pie(where, source){
nv.addGraph(function() {
var chart = nv.models.pieChart()
.x(function(d) { return d.key })
.y(function(d) { return d.value })
.showLabels(true)
.labelType("percent")
.labelThreshold(.05)
.donut(true);
d3.select(where)
.datum(source)
.transition().duration(1200)
.call(chart);
return chart;
});
}
$(function(){
$("#task_choser").change(function(){
var d = DATA[parseInt($(this).find("option:selected").val())]
$("#results")
.empty()
.append($("#template .results").clone())
.find(".results .config")
.html("<pre>" + JSON.stringify(d["config"], "", 4) + "</pre>")
.end()
draw_stacked("#results .total_time .stackedarea", function(){
return d["time"]["iter"]
})
draw_pie("#results .total_time .pie", function(){
return d["time"]["pie"]
})
draw_pie("#results .atomic .pie", function(){
return d["atomic"]["pie"]
})
draw_stacked("#results .atomic .stackedarea", function(){
return d["atomic"]["iter"]
})
$("#template").hide()
}).change();
});
</script>
</head>
<body>
<div id="task_choser">
Select benchmark task:
<select>
% for i, name in enumerate(tasks):
<option value=${i}>${name}</option>
% endfor
</select>
</div>
<div id="results"> </div>
<div id="template">
<div class="results">
<div class="config"> </div>
<div class="total_time">
<svg class="stackedarea"></svg>
<svg class="pie"> </svg>
</div>
<div class="atomic">
<svg class="stackedarea"></svg>
<svg class="pie"> </svg>
</div>
</div>
</div>
</body>
</html>

View File

@@ -20,10 +20,13 @@ from __future__ import print_function
import collections
import json
import math
import os
import pprint
import prettytable
import sys
import webbrowser
from rally.benchmark.processing import plot
from rally.cmd import cliutils
from rally.cmd import envutils
from rally import db
@@ -243,6 +246,22 @@ class TaskCommands(object):
print(table)
@cliutils.args('--task-id', type=str, dest='task_id', help='uuid of task')
@cliutils.args('--out', type=str, dest='out', required=False,
help='Path to output file.')
@cliutils.args('--open', dest='open_it', action='store_true',
help='Open it in browser.')
def plot2html(self, task_id, out=None, open_it=False):
results = map(lambda x: {"key": x["key"], 'result': x['data']['raw']},
db.task_result_get_all_by_uuid(task_id))
output_file = out or ("%s.html" % task_id)
with open(output_file, "w+") as f:
f.write(plot.plot(results))
if open_it:
webbrowser.open_new_tab("file://" + os.path.realpath(output_file))
@cliutils.args('--task-id', type=str, dest='task_id', help='uuid of task')
@cliutils.args('--force', action='store_true', help='force delete')
def delete(self, task_id, force):

View File

@@ -7,6 +7,7 @@ netaddr>=0.7.6
oslo.config>=1.2.0
paramiko>=1.9.0
pbr>=0.6,<1.0
Mako>=0.4.0
PrettyTable>=0.7,<0.8
python-glanceclient>=0.9.0
python-keystoneclient>=0.6.0

View File

View File

@@ -0,0 +1,157 @@
# Copyright 2014: Mirantis Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import mock
from rally.benchmark.processing import plot
from tests import test
class PlotTestCase(test.TestCase):
@mock.patch("rally.benchmark.processing.plot.open", create=True)
@mock.patch("rally.benchmark.processing.plot.mako.template.Template")
@mock.patch("rally.benchmark.processing.plot.os.path.dirname")
@mock.patch("rally.benchmark.processing.plot._process_results")
def test_plot(self, mock_proc_results, mock_dirname, mock_template,
mock_open):
mock_dirname.return_value = "abspath"
mock_open.return_value = mock_open
mock_open.__enter__.return_value = mock_open
mock_open.read.return_value = "some_template"
templ = mock.MagicMock()
templ.render.return_value = "output"
mock_template.return_value = templ
mock_proc_results.return_value = [{"name": "a"}, {"name": "b"}]
result = plot.plot(["abc"])
self.assertEqual(result, templ.render.return_value)
templ.render.assert_called_once_with(
data=json.dumps(mock_proc_results.return_value),
tasks=map(lambda r: r["name"], mock_proc_results.return_value)
)
mock_template.assert_called_once_with(mock_open.read.return_value)
mock_open.assert_called_once_with("%s/src/index.mako"
% mock_dirname.return_value)
@mock.patch("rally.benchmark.processing.plot._process_atomic_time")
@mock.patch("rally.benchmark.processing.plot._process_main_time")
def test__process_results(self, mock_main_time, mock_atomic_time):
results = [
{"key": {"name": "n1", "pos": 1, "kw": "config1"}},
{"key": {"name": "n2", "pos": 2, "kw": "config2"}}
]
mock_main_time.return_value = "main_time"
mock_atomic_time.return_value = "main_atomic"
output = plot._process_results(results)
for i, r in enumerate(results):
self.assertEqual(output[i], {
"name": "%s (task #%d)" % (r["key"]["name"], r["key"]["pos"]),
"config": r["key"]["kw"],
"time": mock_main_time.return_value,
"atomic": mock_atomic_time.return_value
})
def test__process_main_time(self):
result = {
"result": [
{
"error": None,
"time": 1,
"idle_time": 2
},
{
"error": True,
"time": 1,
"idle_time": 1
},
{
"error": None,
"time": 2,
"idle_time": 3
}
]
}
output = plot._process_main_time(result)
self.assertEqual(output, {
"pie": [
{"key": "success", "value": 2},
{"key": "errors", "value": 1}
],
"iter": [
{
"key": "duration",
"values": [[1, 1], [2, 0], [3, 2]]
},
{
"key": "idle_duration",
"values": [[1, 2], [2, 0], [3, 3]]
}
]
})
def test__process_atomic_time(self):
result = {
"result": [
{
"error": None,
"atomic_actions_time": [
{"action": "action1", "duration": 1},
{"action": "action2", "duration": 2}
]
},
{
"error": True,
"atomic_actions_time": [
{"action": "action1", "duration": 1},
{"action": "action2", "duration": 2}
]
},
{
"error": None,
"atomic_actions_time": [
{"action": "action1", "duration": 3},
{"action": "action2", "duration": 4}
]
}
]
}
output = plot._process_atomic_time(result)
self.assertEqual(output, {
"pie": [
{"key": "action1", "value": 2.0},
{"key": "action2", "value": 3.0}
],
"iter": [
{
"key": "action1",
"values": [[1, 1], [2, 0], [3, 3]]
},
{
"key": "action2",
"values": [[1, 2], [2, 0], [3, 4]]
}
]
})