Skip to content

Commit

Permalink
Changing the approach to calculation Ratio per User class and Total r…
Browse files Browse the repository at this point in the history
…atio,

now it is considered based on the real number of users.
On the Task tab (web ui) the data is updated every 1 second, so it is possible
to see the actual ratio changing.
For command-line arguments --show-task-ratio, --show-task-ratio-json,
the behavior is also changed - the ratio pre-calculation based on passed num_users.
If there is no fixed_count users and num_users argument is None,
the old behaviour occurs.
  • Loading branch information
EzR1d3r committed Dec 29, 2021
1 parent 8007d44 commit 6fefd74
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 64 deletions.
12 changes: 9 additions & 3 deletions locust/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import datetime
from itertools import chain
from .stats import sort_stats
from .user.inspectuser import get_task_ratio_dict
from .user.inspectuser import get_actual_ratio
from html import escape
from json import dumps
from .runners import MasterRunner


def render_template(file, **kwargs):
Expand Down Expand Up @@ -62,9 +63,14 @@ def get_html_report(environment, show_download_link=True):
static_css.append(f.read())
static_css.extend(["", ""])

is_distributed = isinstance(environment.runner, MasterRunner)
user_spawned = (
environment.runner.reported_user_classes_count if is_distributed else environment.runner.user_classes_count
)

task_data = {
"per_class": get_task_ratio_dict(environment.user_classes),
"total": get_task_ratio_dict(environment.user_classes, total=True),
"per_class": get_actual_ratio(environment.user_classes, user_spawned, False),
"total": get_actual_ratio(environment.user_classes, user_spawned, True),
}

res = render_template(
Expand Down
13 changes: 4 additions & 9 deletions locust/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from .stats import print_error_report, print_percentile_stats, print_stats, stats_printer, stats_history
from .stats import StatsCSV, StatsCSVFileWriter
from .user import User
from .user.inspectuser import get_task_ratio_dict, print_task_ratio
from .user.inspectuser import print_task_ratio, print_task_ratio_json
from .util.timespan import parse_timespan
from .exception import AuthCredentialsError
from .shape import LoadTestShape
Expand Down Expand Up @@ -218,18 +218,13 @@ def main():
if options.show_task_ratio:
print("\n Task ratio per User class")
print("-" * 80)
print_task_ratio(user_classes)
print_task_ratio(user_classes, options.num_users, False)
print("\n Total task ratio")
print("-" * 80)
print_task_ratio(user_classes, total=True)
print_task_ratio(user_classes, options.num_users, True)
sys.exit(0)
if options.show_task_ratio_json:

task_data = {
"per_class": get_task_ratio_dict(user_classes),
"total": get_task_ratio_dict(user_classes, total=True),
}
print(dumps(task_data))
print_task_ratio_json(user_classes, options.num_users)
sys.exit(0)

if options.master:
Expand Down
16 changes: 9 additions & 7 deletions locust/static/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ function _getTasks_div(root, title) {
}


function initTasks() {
var tasks = $('#tasks .tasks')
var tasksData = tasks.data('tasks');
console.log(tasksData);
tasks.append(_getTasks_div(tasksData.per_class, 'Ratio per User class'));
tasks.append(_getTasks_div(tasksData.total, 'Total ratio'));
function updateTasks() {
$.get('/tasks', function (data) {
var tasks = $('#tasks .tasks');
tasks.empty();
tasks.append(_getTasks_div(data.per_class, 'Ratio per User class'));
tasks.append(_getTasks_div(data.total, 'Total ratio'));
setTimeout(updateTasks, 1000);
});
}
initTasks();
updateTasks();
14 changes: 7 additions & 7 deletions locust/test/test_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
from locust.stats import StatsCSVFileWriter
from locust.stats import stats_history
from locust.test.testcases import LocustTestCase
from locust.user.inspectuser import get_task_ratio_dict
from locust.user.inspectuser import _get_task_ratio

from .testcases import WebserverTestCase
from .test_runners import mocked_rpc
from locust.test.testcases import WebserverTestCase
from locust.test.test_runners import mocked_rpc


_TEST_CSV_STATS_INTERVAL_SEC = 0.2
Expand Down Expand Up @@ -794,16 +794,16 @@ def task2(self):


class TestInspectUser(unittest.TestCase):
def test_get_task_ratio_dict_relative(self):
ratio = get_task_ratio_dict([MyTaskSet])
def test_get_task_ratio_relative(self):
ratio = _get_task_ratio([MyTaskSet], False, 1.0)
self.assertEqual(1.0, ratio["MyTaskSet"]["ratio"])
self.assertEqual(0.75, ratio["MyTaskSet"]["tasks"]["root_task"]["ratio"])
self.assertEqual(0.25, ratio["MyTaskSet"]["tasks"]["MySubTaskSet"]["ratio"])
self.assertEqual(0.5, ratio["MyTaskSet"]["tasks"]["MySubTaskSet"]["tasks"]["task1"]["ratio"])
self.assertEqual(0.5, ratio["MyTaskSet"]["tasks"]["MySubTaskSet"]["tasks"]["task2"]["ratio"])

def test_get_task_ratio_dict_total(self):
ratio = get_task_ratio_dict([MyTaskSet], total=True)
def test_get_task_ratio_total(self):
ratio = _get_task_ratio([MyTaskSet], True, 1.0)
self.assertEqual(1.0, ratio["MyTaskSet"]["ratio"])
self.assertEqual(0.75, ratio["MyTaskSet"]["tasks"]["root_task"]["ratio"])
self.assertEqual(0.25, ratio["MyTaskSet"]["tasks"]["MySubTaskSet"]["ratio"])
Expand Down
6 changes: 3 additions & 3 deletions locust/test/test_taskratio.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import unittest

from locust.user import User, TaskSet, task
from locust.user.inspectuser import get_task_ratio_dict
from locust.user.inspectuser import get_actual_ratio, _get_task_ratio


class TestTaskRatio(unittest.TestCase):
Expand All @@ -24,7 +24,7 @@ def task2(self):
class MyUser(User):
tasks = [Tasks]

ratio_dict = get_task_ratio_dict(Tasks.tasks, total=True)
ratio_dict = _get_task_ratio(Tasks.tasks, True, 1.0)

self.assertEqual(
{
Expand Down Expand Up @@ -52,7 +52,7 @@ class MoreLikelyUser(User):
weight = 3
tasks = [Tasks]

ratio_dict = get_task_ratio_dict([UnlikelyUser, MoreLikelyUser], total=True)
ratio_dict = get_actual_ratio([UnlikelyUser, MoreLikelyUser], {"UnlikelyUser": 1, "MoreLikelyUser": 3}, True)

self.assertDictEqual(
{
Expand Down
82 changes: 55 additions & 27 deletions locust/user/inspectuser.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,43 @@
from collections import defaultdict
import inspect
from json import dumps

from .task import TaskSet
from .users import User


def print_task_ratio(user_classes, total=False, level=0, parent_ratio=1.0):
d = get_task_ratio_dict(user_classes, total=total, parent_ratio=parent_ratio)
def print_task_ratio(user_classes, num_users, total):
"""
This function calculates the task ratio of users based on the user total count.
"""
d = get_actual_ratio(user_classes, _calc_distribution(user_classes, num_users), total)
_print_task_ratio(d)


def print_task_ratio_json(user_classes, num_users):
d = _calc_distribution(user_classes, num_users)
task_data = {
"per_class": get_actual_ratio(user_classes, d, False),
"total": get_actual_ratio(user_classes, d, True),
}

print(dumps(task_data, indent=4))


def _calc_distribution(user_classes, num_users):
fixed_count = sum([u.fixed_count for u in user_classes if u.fixed_count])
total_weight = sum([u.weight for u in user_classes if not u.fixed_count])
num_users = num_users or (total_weight if not fixed_count else 1)
weighted_count = num_users - fixed_count
weighted_count = weighted_count if weighted_count > 0 else 0
user_classes_count = {}

for u in user_classes:
count = u.fixed_count if u.fixed_count else (u.weight / total_weight) * weighted_count
user_classes_count[u.__name__] = round(count)

return user_classes_count


def _print_task_ratio(x, level=0):
for k, v in x.items():
padding = 2 * " " * level
Expand All @@ -18,33 +47,32 @@ def _print_task_ratio(x, level=0):
_print_task_ratio(v["tasks"], level + 1)


def get_task_ratio_dict(tasks, total=False, parent_ratio=1.0):
"""
Return a dict containing task execution ratio info
"""
if len(tasks) > 0 and hasattr(tasks[0], "weight"):
divisor = sum(t.weight for t in tasks)
else:
divisor = len(tasks) / parent_ratio
ratio = {}
def get_actual_ratio(user_classes, user_spawned, total):
user_count = sum(user_spawned.values()) or 1
ratio_percent = {u: user_spawned.get(u.__name__, 0) / user_count for u in user_classes}

task_dict = {}
for u, r in ratio_percent.items():
d = {"ratio": r}
d["tasks"] = _get_task_ratio(u.tasks, total, r)
task_dict[u.__name__] = d

return task_dict


def _get_task_ratio(tasks, total, parent_ratio):
parent_ratio = parent_ratio if total else 1.0
ratio = defaultdict(int)
for task in tasks:
ratio.setdefault(task, 0)
ratio[task] += task.weight if hasattr(task, "weight") else 1
ratio[task] += 1

# get percentage
ratio_percent = dict((k, float(v) / divisor) for k, v in ratio.items())
ratio_percent = {t: r * parent_ratio / len(tasks) for t, r in ratio.items()}

task_dict = {}
for locust, ratio in ratio_percent.items():
d = {"ratio": ratio}
if inspect.isclass(locust):
if issubclass(locust, (User, TaskSet)):
T = locust.tasks
if total:
d["tasks"] = get_task_ratio_dict(T, total, ratio)
else:
d["tasks"] = get_task_ratio_dict(T, total)

task_dict[locust.__name__] = d
for t, r in ratio_percent.items():
d = {"ratio": r}
if inspect.isclass(t) and issubclass(t, TaskSet):
d["tasks"] = _get_task_ratio(t.tasks, total, r)
task_dict[t.__name__] = d

return task_dict
23 changes: 15 additions & 8 deletions locust/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from .stats import sort_stats
from . import stats as stats_module, __version__ as version, argument_parser
from .stats import StatsCSV
from .user.inspectuser import get_task_ratio_dict
from .user.inspectuser import get_actual_ratio
from .util.cache import memoize
from .util.rounding import proper_round
from .util.timespan import parse_timespan
Expand Down Expand Up @@ -344,6 +344,19 @@ def exceptions_csv():
self.stats_csv_writer.exceptions_csv(writer)
return _download_csv_response(data.getvalue(), "exceptions")

@app.route("/tasks")
@self.auth_required_if_enabled
def tasks():
is_distributed = isinstance(self.environment.runner, MasterRunner)
runner = self.environment.runner
user_spawned = runner.reported_user_classes_count if is_distributed else runner.user_classes_count

task_data = {
"per_class": get_actual_ratio(self.environment.user_classes, user_spawned, False),
"total": get_actual_ratio(self.environment.user_classes, user_spawned, True),
}
return task_data

def start(self):
self.greenlet = gevent.spawn(self.start_server)
self.greenlet.link_exception(greenlet_exception_handler)
Expand Down Expand Up @@ -411,12 +424,6 @@ def update_template_args(self):
worker_count = 0

stats = self.environment.runner.stats

task_data = {
"per_class": get_task_ratio_dict(self.environment.user_classes),
"total": get_task_ratio_dict(self.environment.user_classes, total=True),
}

extra_options = argument_parser.ui_extra_args_dict()

self.template_args = {
Expand All @@ -433,6 +440,6 @@ def update_template_args(self):
"worker_count": worker_count,
"is_shape": self.environment.shape_class,
"stats_history_enabled": options and options.stats_history_enabled,
"tasks": dumps(task_data),
"tasks": dumps({}),
"extra_options": extra_options,
}

0 comments on commit 6fefd74

Please sign in to comment.