Merge "Add task result graphic ploter command"

This commit is contained in:
Jenkins 2014-02-23 18:53:14 +00:00 committed by Gerrit Code Review
commit 9c94a72fbf
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]]
}
]
})