Add atomic action names tracking

Here we change the format of the scenario results so that it now stores
the atomic actions data as a dict:

    Before:
        {
            "duration": ...,
            "error": ...,
            "atomic_actions": [
                {"action": "a1", "duration": 0.3},
                {"action": "a2", "duration": 0.1},
                {"action": "a1", "duration": 0.25}
            ],
            ...
        }

    After:
        {
            "duration": ...,
            "error": ...,
            "atomic_actions": {
                "a1": 0.3,
                "a1 (2)": 0.25,
                "a2": 0.1
            },
            ...
        }

We also solve 2 connected problems here:

1. The statistics table that shows up in the CLI after benchmarks complete now
   always has rows for all the atomic actions that were invoked in benchmark
   scenarios. Before this patch, this was not the case since occasionally the
   atomic actions data could not be written due to Exceptions.

2. We've removed lots of ugly code in benchmark/processing/plot.py, since now
   there is no need to make passes through the atomic actions data to collect
   their names.

This patch also:
* contains some removing of code duplicate between plot & CLI output generation;
* adds missing UTs for raw benchmark data processing;
* fixes a minor bug in histograms bins number calculation.

Change-Id: I17f563051bd1c2ec2fb47a385d4cc652895e1f9e
This commit is contained in:
Mikhail Dubov 2014-09-22 10:45:05 +04:00
parent 9f016ee773
commit b55d6dfc63
21 changed files with 164 additions and 164 deletions

View File

@ -42,7 +42,7 @@ class Histogram:
def _calculate_bin_width(self):
"""Calculate the bin width using a given number of bins."""
return (self.max_data - self.min_data) / self.number_of_bins
return (self.max_data - self.min_data) // self.number_of_bins
def _calculate_x_axis(self):
"""Return a list with the values of the x axis."""

View File

@ -39,13 +39,9 @@ def _prepare_data(data, reduce_rows=1000):
for k, v in d1.iteritems():
v[-1] = (v[-1] + d2[k]) / 2.0
zero_atomic_actions = {}
for row in data["result"]:
# find first non-error result to get atomic actions names
if not row["error"] and "atomic_actions" in row:
zero_atomic_actions = dict([(a["action"], 0)
for a in row["atomic_actions"]])
break
atomic_action_names = (data["result"][0]["atomic_actions"].keys()
if data["result"] else [])
zero_atomic_actions = dict([(a, 0) for a in atomic_action_names])
total_durations = {"duration": [], "idle_duration": []}
atomic_durations = dict([(a, []) for a in zero_atomic_actions])
@ -74,8 +70,9 @@ def _prepare_data(data, reduce_rows=1000):
"duration": row["duration"],
"idle_duration": row["idle_duration"],
}
new_row_atomic = dict([(a["action"], a["duration"])
for a in row["atomic_actions"]])
new_row_atomic = {}
for k, v in row["atomic_actions"].iteritems():
new_row_atomic[k] = v if v else 0
if store < 1:
_append(total_durations, new_row_total)
_append(atomic_durations, new_row_atomic)
@ -136,10 +133,11 @@ def _process_atomic(result, data):
# NOTE(boris-42): In our result["result"] we have next structure:
# {"error": NoneOrDict,
# "atomic_actions": [
# {"action": String, "duration": Float},
# ...
# ]}
# "atomic_actions": {
# "action1": <duration>,
# "action2": <duration>
# }
# }
# Our goal is to get next structure:
# [{"key": $atomic_actions.action,
# "values": [[order, $atomic_actions.duration
@ -149,12 +147,9 @@ def _process_atomic(result, data):
# 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"]:
stacked_area.append({"key": action["action"], "values": []})
break
stacked_area = ([{"key": a, "values": []}
for a in result["result"][0]["atomic_actions"]]
if result["result"] else [])
# NOTE(boris-42): pie is similiar to stacked_area, only difference is in
# structure of values. In case of $error we shouldn't put
@ -173,9 +168,15 @@ def _process_atomic(result, data):
continue
# in case of non error put real durations to pie and stacked area
for j, action in enumerate(res["atomic_actions"]):
pie[j]["values"].append(action["duration"])
histogram_data[j]["values"].append(action["duration"])
for j, action in enumerate(res["atomic_actions"].keys()):
# in case any single atomic action failed, put 0
action_duration = res["atomic_actions"][action] or 0.0
pie[j]["values"].append(action_duration)
histogram_data[j]["values"].append(action_duration)
# filter out empty action lists in pie / histogram to avoid errors
pie = filter(lambda x: x["values"], pie)
histogram_data = filter(lambda x: x["values"], histogram_data)
histograms = [[] for atomic_action in range(len(histogram_data))]
for i, atomic_action in enumerate(histogram_data):
@ -211,28 +212,10 @@ def _process_atomic(result, data):
def _get_atomic_action_durations(result):
raw = result.get('result', [])
atomic_actions_names = []
for r in raw:
if 'atomic_actions' in r:
for a in r['atomic_actions']:
atomic_actions_names.append(a["action"])
break
action_durations = {}
for atomic_action in atomic_actions_names:
action_durations[atomic_action] = utils.get_durations(
raw,
lambda r: next(a["duration"] for a in r["atomic_actions"]
if a["action"] == atomic_action),
lambda r: any((a["action"] == atomic_action)
for a in r["atomic_actions"]))
actions_data = utils.get_atomic_actions_data(raw)
table = []
actions_list = action_durations.keys()
action_durations["total"] = utils.get_durations(
raw, lambda x: x["duration"], lambda r: not r["error"])
actions_list.append("total")
for action in actions_list:
durations = action_durations[action]
for action in actions_data:
durations = actions_data[action]
if durations:
data = [action,
round(min(durations), 3),

View File

@ -52,16 +52,20 @@ def percentile(values, percent):
return (d0 + d1)
def get_durations(raw_data, get_duration, is_successful):
"""Retrieve the benchmark duration data from a list of records.
def get_atomic_actions_data(raw_data):
"""Retrieve detailed (by atomic actions & total runtime) benchmark data.
:parameter raw_data: list of records
:parameter get_duration: function that retrieves the duration data from
a given record
:parameter is_successful: function that returns True if the record contains
a successful benchmark result, False otherwise
:parameter raw_data: list of raw records (scenario runner output)
:returns: list of float values corresponding to benchmark durations
:returns: dictionary containing atomic action + total duration lists
for all atomic action keys
"""
data = [get_duration(run) for run in raw_data if is_successful(run)]
return data
atomic_actions = raw_data[0]["atomic_actions"].keys() if raw_data else []
actions_data = {}
for atomic_action in atomic_actions:
actions_data[atomic_action] = [
r["atomic_actions"][atomic_action]
for r in raw_data
if r["atomic_actions"][atomic_action] is not None]
actions_data["total"] = [r["duration"] for r in raw_data if not r["error"]]
return actions_data

View File

@ -38,7 +38,7 @@ def format_result_on_timeout(exc, timeout):
"duration": timeout,
"idle_duration": 0,
"scenario_output": {"errors": "", "data": {}},
"atomic_actions": [],
"atomic_actions": {},
"error": utils.format_exc(exc)
}
@ -117,14 +117,9 @@ class ScenarioRunnerResult(dict):
"additionalProperties": False
},
"atomic_actions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"action": {"type": "string"},
"duration": {"type": "number"}
},
"additionalProperties": False
"type": "object",
"patternProperties": {
".*": {"type": ["number", "null"]}
}
},
"error": {

View File

@ -55,7 +55,7 @@ class Scenario(object):
self._admin_clients = admin_clients
self._clients = clients
self._idle_duration = 0
self._atomic_actions = []
self._atomic_actions = {}
# TODO(amaretskiy): consider about prefix part of benchmark uuid
@classmethod
@ -217,10 +217,17 @@ class Scenario(object):
"""Returns duration of all sleep_between."""
return self._idle_duration
def _register_atomic_action(self, name):
"""Registers an atomic action by its name."""
self._atomic_actions[name] = None
def _atomic_action_registered(self, name):
"""Checks whether an atomic action has been already registered."""
return name in self._atomic_actions
def _add_atomic_actions(self, name, duration):
"""Adds the duration of an atomic action by its 'name'."""
self._atomic_actions.append(
{'action': name, 'duration': duration})
"""Adds the duration of an atomic action by its name."""
self._atomic_actions[name] = duration
def atomic_actions(self):
"""Returns the content of each atomic action."""
@ -236,9 +243,8 @@ def atomic_action_timer(name):
def wrap(func):
@functools.wraps(func)
def func_atomic_actions(self, *args, **kwargs):
with utils.Timer() as timer:
with AtomicAction(self, name):
f = func(self, *args, **kwargs)
self._add_atomic_actions(name, timer.duration())
return f
return func_atomic_actions
return wrap
@ -264,8 +270,25 @@ class AtomicAction(utils.Timer):
"""
super(AtomicAction, self).__init__()
self.scenario_instance = scenario_instance
self.name = name
self.name = self._get_atomic_action_name(name)
self.scenario_instance._register_atomic_action(self.name)
def _get_atomic_action_name(self, name):
if not self.scenario_instance._atomic_action_registered(name):
return name
else:
name_template = name + " (%i)"
atomic_action_iteration = 2
with open("1.txt", "a") as f:
f.write("Enter\n")
f.write(str(dir(self.scenario_instance)) + "\n")
while self.scenario_instance._atomic_action_registered(
name_template % atomic_action_iteration):
atomic_action_iteration += 1
return name_template % atomic_action_iteration
def __exit__(self, type, value, tb):
super(AtomicAction, self).__exit__(type, value, tb)
self.scenario_instance._add_atomic_actions(self.name, self.duration())
if type is None:
self.scenario_instance._add_atomic_actions(self.name,
self.duration())

View File

@ -143,23 +143,6 @@ class TaskCommands(object):
formatters=formatters)
print()
def _get_atomic_action_durations(raw):
atomic_actions_names = []
for r in raw:
if 'atomic_actions' in r:
for a in r['atomic_actions']:
atomic_actions_names.append(a["action"])
break
result = {}
for atomic_action in atomic_actions_names:
result[atomic_action] = utils.get_durations(
raw,
lambda r: next(a["duration"] for a in r["atomic_actions"]
if a["action"] == atomic_action),
lambda r: any((a["action"] == atomic_action)
for a in r["atomic_actions"]))
return result
if task_id == "last":
task = db.task_get_detailed_last()
task_id = task.uuid
@ -210,13 +193,9 @@ class TaskCommands(object):
for col in float_cols]))
table_rows = []
action_durations = _get_atomic_action_durations(raw)
actions_list = action_durations.keys()
action_durations["total"] = utils.get_durations(
raw, lambda x: x["duration"], lambda r: not r["error"])
actions_list.append("total")
for action in actions_list:
durations = action_durations[action]
actions_data = utils.get_atomic_actions_data(raw)
for action in actions_data:
durations = actions_data[action]
if durations:
data = [action,
min(durations),

View File

@ -90,17 +90,20 @@ class PlotTestCase(test.TestCase):
{
"error": [],
"duration": 1,
"idle_duration": 2
"idle_duration": 2,
"atomic_actions": {}
},
{
"error": True,
"duration": 1,
"idle_duration": 1
"idle_duration": 1,
"atomic_actions": {}
},
{
"error": [],
"duration": 2,
"idle_duration": 3
"idle_duration": 3,
"atomic_actions": {}
}
]
}
@ -153,24 +156,24 @@ class PlotTestCase(test.TestCase):
"result": [
{
"error": [],
"atomic_actions": [
{"action": "action1", "duration": 1},
{"action": "action2", "duration": 2}
]
"atomic_actions": {
"action1": 1,
"action2": 2
}
},
{
"error": ["some", "error", "occurred"],
"atomic_actions": [
{"action": "action1", "duration": 1},
{"action": "action2", "duration": 2}
]
"atomic_actions": {
"action1": 1,
"action2": 2
}
},
{
"error": [],
"atomic_actions": [
{"action": "action1", "duration": 3},
{"action": "action2", "duration": 4}
]
"atomic_actions": {
"action1": 3,
"action2": 4
}
}
]
}
@ -262,10 +265,10 @@ class PlotTestCase(test.TestCase):
data = []
for i in range(100):
atomic_actions = [
{"action": "a1", "duration": i + 0.1},
{"action": "a2", "duration": i + 0.8},
]
atomic_actions = {
"a1": i + 0.1,
"a2": i + 0.8
}
row = {
"duration": i * 3.14,
"idle_duration": i * 0.2,

View File

@ -18,7 +18,7 @@ from rally import exceptions
from tests import test
class ProcessingUtilsTestCase(test.TestCase):
class MathTestCase(test.TestCase):
def test_percentile(self):
lst = range(1, 101)
@ -43,3 +43,43 @@ class ProcessingUtilsTestCase(test.TestCase):
lst = []
self.assertRaises(exceptions.InvalidArgumentsException,
utils.mean, lst)
class AtomicActionsDataTestCase(test.TestCase):
def test_get_atomic_actions_data(self):
raw_data = [
{
"error": [],
"duration": 3,
"atomic_actions": {
"action1": 1,
"action2": 2
}
},
{
"error": ["some", "error", "occurred"],
"duration": 1.9,
"atomic_actions": {
"action1": 0.5,
"action2": 1.4
}
},
{
"error": [],
"duration": 8,
"atomic_actions": {
"action1": 4,
"action2": 4
}
}
]
atomic_actions_data = {
"action1": [1, 0.5, 4],
"action2": [2, 1.4, 4],
"total": [3, 8]
}
output = utils.get_atomic_actions_data(raw_data)
self.assertEqual(output, atomic_actions_data)

View File

@ -34,7 +34,7 @@ class ScenarioHelpersTestCase(test.TestCase):
"duration": 100,
"idle_duration": 0,
"scenario_output": {"errors": "", "data": {}},
"atomic_actions": [],
"atomic_actions": {},
"error": mock_format_exc.return_value
}
@ -94,7 +94,7 @@ class ScenarioHelpersTestCase(test.TestCase):
"idle_duration": 0,
"error": [],
"scenario_output": {"errors": "", "data": {}},
"atomic_actions": []
"atomic_actions": {}
}
self.assertEqual(expected_result, result)
@ -112,7 +112,7 @@ class ScenarioHelpersTestCase(test.TestCase):
"idle_duration": 0,
"error": [],
"scenario_output": fakes.FakeScenario().with_output(),
"atomic_actions": []
"atomic_actions": {}
}
self.assertEqual(expected_result, result)
@ -128,7 +128,7 @@ class ScenarioHelpersTestCase(test.TestCase):
"duration": fakes.FakeTimer().duration(),
"idle_duration": 0,
"scenario_output": {"errors": "", "data": {}},
"atomic_actions": []
"atomic_actions": {}
}
self.assertEqual(expected_result, result)
self.assertEqual(expected_error[:2],
@ -146,7 +146,7 @@ class ScenarioRunnerResultTestCase(test.TestCase):
"data": {"test": 1.0},
"errors": "test error string 1"
},
"atomic_actions": [{"action": "test1", "duration": 1.0}],
"atomic_actions": {"test1": 1.0},
"error": []
},
{
@ -156,7 +156,7 @@ class ScenarioRunnerResultTestCase(test.TestCase):
"data": {"test": 2.0},
"errors": "test error string 2"
},
"atomic_actions": [{"action": "test2", "duration": 2.0}],
"atomic_actions": {"test2": 2.0},
"error": ["a", "b", "c"]
}
]

View File

@ -29,7 +29,7 @@ class SerialScenarioRunnerTestCase(test.TestCase):
def test_run_scenario(self, mock_run_once):
times = 5
result = {"duration": 10, "idle_duration": 0, "error": [],
"scenario_output": {}, "atomic_actions": []}
"scenario_output": {}, "atomic_actions": {}}
mock_run_once.return_value = result
expected_results = [result for i in range(times)]

View File

@ -15,7 +15,6 @@
import mock
from rally.benchmark.scenarios.ceilometer import utils
from tests.benchmark.scenarios import test_base
from tests import fakes
from tests import test
@ -30,8 +29,7 @@ class CeilometerScenarioTestCase(test.TestCase):
return_value=fakes.FakeCeilometerClient())
def _test_atomic_action_timer(self, atomic_actions_time, name):
action_duration = test_base.get_atomic_action_timer_value_by_name(
atomic_actions_time, name)
action_duration = atomic_actions_time.get(name)
self.assertIsNotNone(action_duration)
self.assertIsInstance(action_duration, float)

View File

@ -18,7 +18,6 @@ from oslo.config import cfg
from oslotest import mockpatch
from rally.benchmark.scenarios.cinder import utils
from tests.benchmark.scenarios import test_base
from tests import test
BM_UTILS = 'rally.benchmark.utils'
@ -43,8 +42,7 @@ class CinderScenarioTestCase(test.TestCase):
self.scenario = utils.CinderScenario()
def _test_atomic_action_timer(self, atomic_actions, name):
action_duration = test_base.get_atomic_action_timer_value_by_name(
atomic_actions, name)
action_duration = atomic_actions.get(name)
self.assertIsNotNone(action_duration)
self.assertIsInstance(action_duration, float)

View File

@ -17,7 +17,6 @@
import mock
from rally.benchmark.scenarios.designate import utils
from tests.benchmark.scenarios import test_base
from tests import test
@ -31,8 +30,7 @@ class DesignateScenarioTestCase(test.TestCase):
self.domain = mock.Mock()
def _test_atomic_action_timer(self, atomic_actions_time, name):
action_duration = test_base.get_atomic_action_timer_value_by_name(
atomic_actions_time, name)
action_duration = atomic_actions_time.get(name)
self.assertIsNotNone(action_duration)
self.assertIsInstance(action_duration, float)

View File

@ -18,7 +18,6 @@ from oslotest import mockpatch
from rally.benchmark.scenarios.glance import utils
from rally.benchmark import utils as butils
from rally import exceptions as rally_exceptions
from tests.benchmark.scenarios import test_base
from tests import fakes
from tests import test
@ -53,8 +52,7 @@ class GlanceScenarioTestCase(test.TestCase):
image_manager.create('fails', 'url', 'cf', 'df'))
def _test_atomic_action_timer(self, atomic_actions, name):
action_duration = test_base.get_atomic_action_timer_value_by_name(
atomic_actions, name)
action_duration = atomic_actions.get(name)
self.assertIsNotNone(action_duration)
self.assertIsInstance(action_duration, float)

View File

@ -17,7 +17,6 @@ import mock
from oslotest import mockpatch
from rally.benchmark.scenarios.heat import utils
from tests.benchmark.scenarios import test_base
from tests import test
BM_UTILS = 'rally.benchmark.utils'
@ -43,8 +42,7 @@ class HeatScenarioTestCase(test.TestCase):
self.scenario = utils.HeatScenario()
def _test_atomic_action_timer(self, atomic_actions, name):
action_duration = test_base.get_atomic_action_timer_value_by_name(
atomic_actions, name)
action_duration = atomic_actions.get(name)
self.assertIsNotNone(action_duration)
self.assertIsInstance(action_duration, float)

View File

@ -16,7 +16,6 @@
import mock
from rally.benchmark.scenarios.keystone import utils
from tests.benchmark.scenarios import test_base
from tests import fakes
from tests import test
@ -52,8 +51,7 @@ class KeystoneUtilsTestCase(test.TestCase):
class KeystoneScenarioTestCase(test.TestCase):
def _test_atomic_action_timer(self, atomic_actions, name):
action_duration = test_base.get_atomic_action_timer_value_by_name(
atomic_actions, name)
action_duration = atomic_actions.get(name)
self.assertIsNotNone(action_duration)
self.assertIsInstance(action_duration, float)

View File

@ -17,7 +17,6 @@ import mock
import netaddr
from rally.benchmark.scenarios.neutron import utils
from tests.benchmark.scenarios import test_base
from tests import test
@ -31,8 +30,7 @@ class NeutronScenarioTestCase(test.TestCase):
self.network = mock.Mock()
def _test_atomic_action_timer(self, atomic_actions_time, name):
action_duration = test_base.get_atomic_action_timer_value_by_name(
atomic_actions_time, name)
action_duration = atomic_actions_time.get(name)
self.assertIsNotNone(action_duration)
self.assertIsInstance(action_duration, float)

View File

@ -20,7 +20,6 @@ from oslotest import mockpatch
from rally.benchmark.scenarios.nova import utils
from rally.benchmark import utils as butils
from rally import exceptions as rally_exceptions
from tests.benchmark.scenarios import test_base
from tests import fakes
from tests import test
@ -51,8 +50,7 @@ class NovaScenarioTestCase(test.TestCase):
self.useFixture(mockpatch.Patch('time.sleep'))
def _test_atomic_action_timer(self, atomic_actions, name):
action_duration = test_base.get_atomic_action_timer_value_by_name(
atomic_actions, name)
action_duration = atomic_actions.get(name)
self.assertIsNotNone(action_duration)
self.assertIsInstance(action_duration, float)

View File

@ -16,7 +16,6 @@
import mock
from rally.benchmark.scenarios.quotas import utils
from tests.benchmark.scenarios import test_base
from tests import fakes
from tests import test
@ -27,8 +26,7 @@ class QuotasScenarioTestCase(test.TestCase):
super(QuotasScenarioTestCase, self).setUp()
def _test_atomic_action_timer(self, atomic_actions_time, name):
action_duration = test_base.get_atomic_action_timer_value_by_name(
atomic_actions_time, name)
action_duration = atomic_actions_time.get(name)
self.assertIsNotNone(action_duration)
self.assertIsInstance(action_duration, float)

View File

@ -18,7 +18,6 @@ from saharaclient.api import base as sahara_base
from rally.benchmark.scenarios.sahara import utils
from rally import exceptions
from tests.benchmark.scenarios import test_base
from tests import test
@ -28,8 +27,7 @@ SAHARA_UTILS = 'rally.benchmark.scenarios.sahara.utils'
class SaharaNodeGroupTemplatesScenarioTestCase(test.TestCase):
def _test_atomic_action_timer(self, atomic_actions, name):
action_duration = test_base.get_atomic_action_timer_value_by_name(
atomic_actions, name)
action_duration = atomic_actions.get(name)
self.assertIsNotNone(action_duration)
self.assertIsInstance(action_duration, float)

View File

@ -330,24 +330,17 @@ class ScenarioTestCase(test.TestCase):
class AtomicActionTestCase(test.TestCase):
def test__init__(self):
fake_scenario_instance = mock.MagicMock()
fake_scenario_instance = fakes.FakeScenario()
c = base.AtomicAction(fake_scenario_instance, 'asdf')
self.assertEqual(c.scenario_instance, fake_scenario_instance)
self.assertEqual(c.name, 'asdf')
@mock.patch('tests.fakes.FakeScenario._add_atomic_actions')
@mock.patch('rally.utils.time')
def test__exit__(self, mock_time):
fake_scenario_instance = mock.Mock()
def test__exit__(self, mock_time, mock__add_atomic_actions):
fake_scenario_instance = fakes.FakeScenario()
self.start = mock_time.time()
with base.AtomicAction(fake_scenario_instance, "asdf"):
pass
duration = mock_time.time() - self.start
fake_scenario_instance._add_atomic_actions.assert_called_once_with(
'asdf', duration)
def get_atomic_action_timer_value_by_name(atomic_actions, name):
for action in atomic_actions:
if action['action'] == name:
return action['duration']
return None
mock__add_atomic_actions.assert_called_once_with('asdf', duration)