From 71d12cc8415284a1ba3508d8be150343f8fddc9a Mon Sep 17 00:00:00 2001 From: Alexander Nyurenberg Date: Wed, 29 Dec 2021 00:43:47 +0300 Subject: [PATCH] Changing the approach to calculation Ratio per User class and Total ratio, 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. --- locust/html.py | 12 +++-- locust/main.py | 13 ++---- locust/static/tasks.js | 16 ++++--- locust/test/test_stats.py | 14 +++--- locust/test/test_taskratio.py | 6 +-- locust/user/inspectuser.py | 82 +++++++++++++++++++++++------------ locust/web.py | 23 ++++++---- 7 files changed, 102 insertions(+), 64 deletions(-) diff --git a/locust/html.py b/locust/html.py index a3735bff65..50b0d812da 100644 --- a/locust/html.py +++ b/locust/html.py @@ -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): @@ -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( diff --git a/locust/main.py b/locust/main.py index 8ad6840656..45bdf0b455 100644 --- a/locust/main.py +++ b/locust/main.py @@ -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 @@ -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: diff --git a/locust/static/tasks.js b/locust/static/tasks.js index 4fc8a63cff..06373f2787 100644 --- a/locust/static/tasks.js +++ b/locust/static/tasks.js @@ -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(); \ No newline at end of file +updateTasks(); diff --git a/locust/test/test_stats.py b/locust/test/test_stats.py index eeac90493e..94b9b05674 100644 --- a/locust/test/test_stats.py +++ b/locust/test/test_stats.py @@ -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 @@ -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"]) diff --git a/locust/test/test_taskratio.py b/locust/test/test_taskratio.py index 09ab864542..e0fc60f034 100644 --- a/locust/test/test_taskratio.py +++ b/locust/test/test_taskratio.py @@ -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): @@ -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( { @@ -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( { diff --git a/locust/user/inspectuser.py b/locust/user/inspectuser.py index e70af1670b..5bc8280e0d 100644 --- a/locust/user/inspectuser.py +++ b/locust/user/inspectuser.py @@ -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 @@ -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 diff --git a/locust/web.py b/locust/web.py index bed99ed31f..00186cd8d5 100644 --- a/locust/web.py +++ b/locust/web.py @@ -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 @@ -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) @@ -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 = { @@ -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, }