Merge "Add task result graphic ploter command"
This commit is contained in:
commit
9c94a72fbf
0
rally/benchmark/processing/__init__.py
Normal file
0
rally/benchmark/processing/__init__.py
Normal file
123
rally/benchmark/processing/plot.py
Normal file
123
rally/benchmark/processing/plot.py
Normal 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))
|
157
rally/benchmark/processing/src/index.mako
Normal file
157
rally/benchmark/processing/src/index.mako
Normal 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>
|
@ -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):
|
||||
|
@ -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
|
||||
|
0
tests/benchmark/processing/__init__.py
Normal file
0
tests/benchmark/processing/__init__.py
Normal file
157
tests/benchmark/processing/test_plot.py
Normal file
157
tests/benchmark/processing/test_plot.py
Normal 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]]
|
||||
}
|
||||
]
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user