locust 2.30.1.dev17__py3-none-any.whl → 2.30.1.dev25__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- locust/_version.py +2 -2
- locust/argument_parser.py +7 -0
- locust/dispatch.py +27 -48
- locust/env.py +2 -0
- locust/html.py +6 -18
- locust/main.py +6 -1
- locust/web.py +15 -7
- locust/webui/dist/assets/index-7dcacb4b.js +240 -0
- locust/webui/dist/auth.html +1 -1
- locust/webui/dist/index.html +1 -1
- locust/webui/dist/report.html +233 -3
- {locust-2.30.1.dev17.dist-info → locust-2.30.1.dev25.dist-info}/METADATA +1 -1
- {locust-2.30.1.dev17.dist-info → locust-2.30.1.dev25.dist-info}/RECORD +17 -17
- locust/webui/dist/assets/index-d9c29bed.js +0 -250
- /locust/webui/dist/assets/{logo.png → logo-10854faa.png} +0 -0
- {locust-2.30.1.dev17.dist-info → locust-2.30.1.dev25.dist-info}/LICENSE +0 -0
- {locust-2.30.1.dev17.dist-info → locust-2.30.1.dev25.dist-info}/WHEEL +0 -0
- {locust-2.30.1.dev17.dist-info → locust-2.30.1.dev25.dist-info}/entry_points.txt +0 -0
locust/_version.py
CHANGED
@@ -14,7 +14,7 @@ __version_tuple__: VERSION_TUPLE
|
|
14
14
|
version_tuple: VERSION_TUPLE
|
15
15
|
|
16
16
|
|
17
|
-
__version__ = "2.30.1.
|
17
|
+
__version__ = "2.30.1.dev25"
|
18
18
|
version = __version__
|
19
|
-
__version_tuple__ = (2, 30, 1, "
|
19
|
+
__version_tuple__ = (2, 30, 1, "dev25")
|
20
20
|
version_tuple = __version_tuple__
|
locust/argument_parser.py
CHANGED
@@ -496,6 +496,13 @@ def setup_parser_arguments(parser):
|
|
496
496
|
help="Enable select boxes in the web interface to choose from all available User classes and Shape classes",
|
497
497
|
env_var="LOCUST_USERCLASS_PICKER",
|
498
498
|
)
|
499
|
+
web_ui_group.add_argument(
|
500
|
+
"--build-path",
|
501
|
+
type=str,
|
502
|
+
default="",
|
503
|
+
help=configargparse.SUPPRESS,
|
504
|
+
env_var="LOCUST_BUILD_PATH",
|
505
|
+
)
|
499
506
|
web_ui_group.add_argument(
|
500
507
|
"--legacy-ui",
|
501
508
|
default=False,
|
locust/dispatch.py
CHANGED
@@ -5,7 +5,7 @@ import itertools
|
|
5
5
|
import math
|
6
6
|
import time
|
7
7
|
from collections import defaultdict
|
8
|
-
from collections.abc import
|
8
|
+
from collections.abc import Iterator
|
9
9
|
from heapq import heapify, heapreplace
|
10
10
|
from math import log2
|
11
11
|
from operator import attrgetter
|
@@ -17,41 +17,31 @@ if TYPE_CHECKING:
|
|
17
17
|
from locust import User
|
18
18
|
from locust.runners import WorkerNode
|
19
19
|
|
20
|
+
from collections.abc import Generator, Iterable
|
21
|
+
from typing import TypeVar
|
20
22
|
|
21
|
-
|
22
|
-
# place `@profile` on the functions/methods you wish to profile. Then, in the unit test you are
|
23
|
-
# running, use `from locust.dispatch import profile; profile.print_stats()` at the end of the unit test.
|
24
|
-
# Placing it in a `finally` block is recommended.
|
25
|
-
# import line_profiler
|
26
|
-
#
|
27
|
-
# profile = line_profiler.LineProfiler()
|
23
|
+
T = TypeVar("T")
|
28
24
|
|
29
25
|
|
30
|
-
def _kl_generator(users:
|
26
|
+
def _kl_generator(users: Iterable[tuple[T, float]]) -> Iterator[T | None]:
|
31
27
|
"""Generator based on Kullback-Leibler divergence
|
32
28
|
|
33
29
|
For example, given users A, B with weights 5 and 1 respectively,
|
34
30
|
this algorithm will yield AAABAAAAABAA.
|
35
31
|
"""
|
36
|
-
|
32
|
+
heap = [(x * log2(x / (x + 1.0)), x + 1.0, x, name) for name, x in users]
|
33
|
+
if not heap:
|
37
34
|
while True:
|
38
35
|
yield None
|
39
36
|
|
40
|
-
names = [u[0].__name__ for u in users]
|
41
|
-
weights = [u[1] for u in users]
|
42
|
-
generated = weights.copy()
|
43
|
-
|
44
|
-
heap = [(x * log2(x / (x + 1.0)), i) for i, x in enumerate(generated)]
|
45
37
|
heapify(heap)
|
46
|
-
|
47
38
|
while True:
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
kl_diff = weights[i] * log2(x / (x + 1.0))
|
39
|
+
_, x, weight, name = heap[0]
|
40
|
+
# (divergence diff, number of generated elements + initial weight, initial weight, name) = heap[0]
|
41
|
+
yield name
|
42
|
+
kl_diff = weight * log2(x / (x + 1.0))
|
53
43
|
# calculate how much choosing element i for (x + 1)th time decreases divergence
|
54
|
-
heapreplace(heap, (kl_diff,
|
44
|
+
heapreplace(heap, (kl_diff, x + 1.0, weight, name))
|
55
45
|
|
56
46
|
|
57
47
|
class UsersDispatcher(Iterator):
|
@@ -378,35 +368,24 @@ class UsersDispatcher(Iterator):
|
|
378
368
|
return users_on_workers, user_gen, worker_gen, active_users
|
379
369
|
|
380
370
|
def _user_gen(self) -> Iterator[str | None]:
|
381
|
-
|
382
|
-
|
383
|
-
fixed_users_gen = _kl_generator([(u, u.fixed_count) for u in fixed_users.values()])
|
384
|
-
weighted_users_gen = _kl_generator([(u, u.weight) for u in self._user_classes if not u.fixed_count])
|
371
|
+
weighted_users_gen = _kl_generator((u.__name__, u.weight) for u in self._user_classes if not u.fixed_count)
|
385
372
|
|
386
|
-
# Spawn users
|
387
373
|
while True:
|
388
|
-
if self._try_dispatch_fixed:
|
374
|
+
if self._try_dispatch_fixed: # Fixed_count users are spawned before weight users.
|
375
|
+
# Some peoples treat this implementation detail as a feature.
|
389
376
|
self._try_dispatch_fixed = False
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
if self._try_dispatch_fixed:
|
403
|
-
current_fixed_users_count = {u: self._get_user_current_count(u) for u in fixed_users}
|
404
|
-
spawned_classes.clear()
|
405
|
-
self._try_dispatch_fixed = False
|
406
|
-
else:
|
407
|
-
spawned_classes.add(user_name)
|
408
|
-
|
409
|
-
yield next(weighted_users_gen)
|
377
|
+
fixed_users_missing = [
|
378
|
+
(u.__name__, miss)
|
379
|
+
for u in self._user_classes
|
380
|
+
if u.fixed_count and (miss := u.fixed_count - self._get_user_current_count(u.__name__)) > 0
|
381
|
+
]
|
382
|
+
total_miss = sum(miss for _, miss in fixed_users_missing)
|
383
|
+
fixed_users_gen = _kl_generator(fixed_users_missing) # type: ignore[arg-type]
|
384
|
+
# https://mypy.readthedocs.io/en/stable/common_issues.html#variance
|
385
|
+
for _ in range(total_miss):
|
386
|
+
yield next(fixed_users_gen)
|
387
|
+
else:
|
388
|
+
yield next(weighted_users_gen)
|
410
389
|
|
411
390
|
@staticmethod
|
412
391
|
def _fast_users_on_workers_copy(users_on_workers: dict[str, dict[str, int]]) -> dict[str, dict[str, int]]:
|
locust/env.py
CHANGED
@@ -171,6 +171,7 @@ class Environment:
|
|
171
171
|
stats_csv_writer: StatsCSV | None = None,
|
172
172
|
delayed_start=False,
|
173
173
|
userclass_picker_is_active=False,
|
174
|
+
build_path: str | None = None,
|
174
175
|
) -> WebUI:
|
175
176
|
"""
|
176
177
|
Creates a :class:`WebUI <locust.web.WebUI>` instance for this Environment and start running the web server
|
@@ -197,6 +198,7 @@ class Environment:
|
|
197
198
|
stats_csv_writer=stats_csv_writer,
|
198
199
|
delayed_start=delayed_start,
|
199
200
|
userclass_picker_is_active=userclass_picker_is_active,
|
201
|
+
build_path=build_path,
|
200
202
|
)
|
201
203
|
return self.web_ui
|
202
204
|
|
locust/html.py
CHANGED
@@ -5,7 +5,8 @@ from html import escape
|
|
5
5
|
from itertools import chain
|
6
6
|
from json import dumps
|
7
7
|
|
8
|
-
from jinja2 import Environment
|
8
|
+
from jinja2 import Environment as JinjaEnvironment
|
9
|
+
from jinja2 import FileSystemLoader
|
9
10
|
|
10
11
|
from . import stats as stats_module
|
11
12
|
from .runners import STATE_STOPPED, STATE_STOPPING, MasterRunner
|
@@ -14,13 +15,11 @@ from .user.inspectuser import get_ratio
|
|
14
15
|
from .util.date import format_utc_timestamp
|
15
16
|
|
16
17
|
PERCENTILES_FOR_HTML_REPORT = [0.50, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99, 1.0]
|
17
|
-
|
18
|
-
BUILD_PATH = os.path.join(ROOT_PATH, "webui", "dist")
|
19
|
-
STATIC_PATH = os.path.join(BUILD_PATH, "assets")
|
18
|
+
DEFAULT_BUILD_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "webui", "dist")
|
20
19
|
|
21
20
|
|
22
|
-
def
|
23
|
-
env =
|
21
|
+
def render_template_from(file, build_path=DEFAULT_BUILD_PATH, **kwargs):
|
22
|
+
env = JinjaEnvironment(loader=FileSystemLoader(build_path))
|
24
23
|
template = env.get_template(file)
|
25
24
|
return template.render(**kwargs)
|
26
25
|
|
@@ -56,16 +55,6 @@ def get_html_report(
|
|
56
55
|
update_stats_history(environment.runner)
|
57
56
|
history = stats.history
|
58
57
|
|
59
|
-
static_js = []
|
60
|
-
js_files = [os.path.basename(filepath) for filepath in glob.glob(os.path.join(STATIC_PATH, "*.js"))]
|
61
|
-
|
62
|
-
for js_file in js_files:
|
63
|
-
path = os.path.join(STATIC_PATH, js_file)
|
64
|
-
static_js.append("// " + js_file + "\n")
|
65
|
-
with open(path, encoding="utf8") as f:
|
66
|
-
static_js.append(f.read())
|
67
|
-
static_js.extend(["", ""])
|
68
|
-
|
69
58
|
is_distributed = isinstance(environment.runner, MasterRunner)
|
70
59
|
user_spawned = (
|
71
60
|
environment.runner.reported_user_classes_count if is_distributed else environment.runner.user_classes_count
|
@@ -79,7 +68,7 @@ def get_html_report(
|
|
79
68
|
"total": get_ratio(environment.user_classes, user_spawned, True),
|
80
69
|
}
|
81
70
|
|
82
|
-
return
|
71
|
+
return render_template_from(
|
83
72
|
"report.html",
|
84
73
|
template_args={
|
85
74
|
"is_report": True,
|
@@ -107,5 +96,4 @@ def get_html_report(
|
|
107
96
|
"percentiles_to_chart": stats_module.PERCENTILES_TO_CHART,
|
108
97
|
},
|
109
98
|
theme=theme,
|
110
|
-
static_js="\n".join(static_js),
|
111
99
|
)
|
locust/main.py
CHANGED
@@ -36,11 +36,15 @@ from .user.inspectuser import print_task_ratio, print_task_ratio_json
|
|
36
36
|
from .util.load_locustfile import load_locustfile
|
37
37
|
from .util.timespan import parse_timespan
|
38
38
|
|
39
|
+
# import external plugins if installed to allow for registering custom arguments etc
|
39
40
|
try:
|
40
|
-
# import locust_plugins if it is installed, to allow it to register custom arguments etc
|
41
41
|
import locust_plugins # pyright: ignore[reportMissingImports]
|
42
42
|
except ModuleNotFoundError:
|
43
43
|
pass
|
44
|
+
try:
|
45
|
+
import locust_cloud # pyright: ignore[reportMissingImports]
|
46
|
+
except ModuleNotFoundError:
|
47
|
+
pass
|
44
48
|
|
45
49
|
version = locust.__version__
|
46
50
|
|
@@ -482,6 +486,7 @@ See https://github.com/locustio/locust/wiki/Installation#increasing-maximum-numb
|
|
482
486
|
stats_csv_writer=stats_csv_writer,
|
483
487
|
delayed_start=True,
|
484
488
|
userclass_picker_is_active=options.class_picker,
|
489
|
+
build_path=options.build_path,
|
485
490
|
)
|
486
491
|
else:
|
487
492
|
web_ui = None
|
locust/web.py
CHANGED
@@ -33,12 +33,13 @@ from gevent import pywsgi
|
|
33
33
|
from . import __version__ as version
|
34
34
|
from . import argument_parser
|
35
35
|
from . import stats as stats_module
|
36
|
-
from .html import
|
36
|
+
from .html import DEFAULT_BUILD_PATH, get_html_report, render_template_from
|
37
37
|
from .log import get_logs, greenlet_exception_logger
|
38
38
|
from .runners import STATE_MISSING, STATE_RUNNING, MasterRunner
|
39
39
|
from .stats import StatsCSV, StatsCSVFileWriter, StatsErrorDict, sort_stats
|
40
40
|
from .user.inspectuser import get_ratio
|
41
41
|
from .util.cache import memoize
|
42
|
+
from .util.date import format_utc_timestamp
|
42
43
|
from .util.timespan import parse_timespan
|
43
44
|
|
44
45
|
if TYPE_CHECKING:
|
@@ -96,6 +97,7 @@ class WebUI:
|
|
96
97
|
stats_csv_writer: StatsCSV | None = None,
|
97
98
|
delayed_start=False,
|
98
99
|
userclass_picker_is_active=False,
|
100
|
+
build_path: str | None = None,
|
99
101
|
):
|
100
102
|
"""
|
101
103
|
Create WebUI instance and start running the web server in a separate greenlet (self.greenlet)
|
@@ -124,14 +126,11 @@ class WebUI:
|
|
124
126
|
self.app = app
|
125
127
|
app.jinja_env.add_extension("jinja2.ext.do")
|
126
128
|
app.debug = True
|
127
|
-
app.root_path = ROOT_PATH
|
128
|
-
self.webui_build_path = BUILD_PATH
|
129
129
|
self.greenlet: gevent.Greenlet | None = None
|
130
130
|
self._swarm_greenlet: gevent.Greenlet | None = None
|
131
131
|
self.template_args = {}
|
132
132
|
self.auth_args = {}
|
133
|
-
self.app.template_folder =
|
134
|
-
self.app.static_folder = STATIC_PATH
|
133
|
+
self.app.template_folder = build_path or DEFAULT_BUILD_PATH
|
135
134
|
self.app.static_url_path = "/assets/"
|
136
135
|
# ensures static js files work on Windows
|
137
136
|
mimetypes.add_type("application/javascript", ".js")
|
@@ -158,7 +157,13 @@ class WebUI:
|
|
158
157
|
|
159
158
|
@app.route("/assets/<path:path>")
|
160
159
|
def send_assets(path):
|
161
|
-
|
160
|
+
directory = (
|
161
|
+
os.path.join(self.app.template_folder, "assets")
|
162
|
+
if os.path.exists(os.path.join(app.template_folder, "assets", path))
|
163
|
+
else os.path.join(DEFAULT_BUILD_PATH, "assets")
|
164
|
+
)
|
165
|
+
|
166
|
+
return send_from_directory(directory, path)
|
162
167
|
|
163
168
|
@app.route("/")
|
164
169
|
@self.auth_required_if_enabled
|
@@ -499,7 +504,7 @@ class WebUI:
|
|
499
504
|
if not self.web_login:
|
500
505
|
return redirect(url_for("index"))
|
501
506
|
|
502
|
-
return
|
507
|
+
return render_template_from(
|
503
508
|
"auth.html",
|
504
509
|
auth_args=self.auth_args,
|
505
510
|
)
|
@@ -613,6 +618,8 @@ class WebUI:
|
|
613
618
|
else None
|
614
619
|
)
|
615
620
|
|
621
|
+
start_time = format_utc_timestamp(stats.start_time)
|
622
|
+
|
616
623
|
self.template_args = {
|
617
624
|
"locustfile": self.environment.locustfile,
|
618
625
|
"state": self.environment.runner.state,
|
@@ -630,6 +637,7 @@ class WebUI:
|
|
630
637
|
and not (self.userclass_picker_is_active or self.environment.shape_class.use_common_options)
|
631
638
|
),
|
632
639
|
"stats_history_enabled": options and options.stats_history_enabled,
|
640
|
+
"start_time": start_time,
|
633
641
|
"tasks": dumps({}),
|
634
642
|
"extra_options": extra_options,
|
635
643
|
"run_time": options and options.run_time,
|