|
1 | 1 | from __future__ import annotations
|
2 | 2 |
|
3 | 3 | import contextlib
|
| 4 | +import functools |
4 | 5 | import itertools
|
5 | 6 | import math
|
| 7 | +import sys |
6 | 8 | import time
|
7 | 9 | from collections import defaultdict
|
8 | 10 | from operator import attrgetter
|
|
16 | 18 | from locust.runners import WorkerNode
|
17 | 19 |
|
18 | 20 |
|
| 21 | +def compatible_math_gcd(*args: int) -> int: |
| 22 | + """ |
| 23 | + This function is a workaround for the fact that `math.gcd` in: |
| 24 | + - 3.5 <= Python < 3.9 doesn't accept more than two arguments. |
| 25 | + - 3.9 <= Python can accept more than two arguments. |
| 26 | + See more at https://docs.python.org/3.9/library/math.html#math.gcd |
| 27 | + """ |
| 28 | + if (3, 5) <= sys.version_info < (3, 9): |
| 29 | + return functools.reduce(math.gcd, args) |
| 30 | + elif sys.version_info >= (3, 9): |
| 31 | + return math.gcd(*args) |
| 32 | + raise NotImplementedError("This function is only implemented for Python from 3.5") |
| 33 | + |
| 34 | + |
19 | 35 | # To profile line-by-line, uncomment the code below (i.e. `import line_profiler ...`) and
|
20 | 36 | # place `@profile` on the functions/methods you wish to profile. Then, in the unit test you are
|
21 | 37 | # running, use `from locust.dispatch import profile; profile.print_stats()` at the end of the unit test.
|
@@ -366,18 +382,37 @@ def infinite_cycle_gen(users: list[tuple[type[User], int]]) -> itertools.cycle:
|
366 | 382 | if not users:
|
367 | 383 | return itertools.cycle([None])
|
368 | 384 |
|
369 |
| - # Normalize the weights so that the smallest weight will be equal to "target_min_weight". |
370 |
| - # The value "2" was experimentally determined because it gave a better distribution especially |
371 |
| - # when dealing with weights which are close to each others, e.g. 1.5, 2, 2.4, etc. |
372 |
| - target_min_weight = 2 |
373 |
| - |
374 |
| - # 'Value' here means weight or fixed count |
| 385 | + def _get_order_of_magnitude(n: float) -> int: |
| 386 | + """Get how many times we need to multiply `n` to get an integer-like number. |
| 387 | + For example: |
| 388 | + 0.1 would return 10, |
| 389 | + 0.04 would return 100, |
| 390 | + 0.0007 would return 10000. |
| 391 | + """ |
| 392 | + if n <= 0: |
| 393 | + raise ValueError("To get the order of magnitude, the number must be greater than 0.") |
| 394 | + |
| 395 | + counter = 0 |
| 396 | + while n < 1: |
| 397 | + n *= 10 |
| 398 | + counter += 1 |
| 399 | + return 10**counter |
| 400 | + |
| 401 | + # Get maximum order of magnitude to "normalize the weights". |
| 402 | + # "Normalizing the weights" is to multiply all weights by the same number so that |
| 403 | + # they become integers. Then we can find the largest common divisor of all the |
| 404 | + # weights, divide them by it and get the smallest possible numbers with the same |
| 405 | + # ratio as the numbers originally had. |
| 406 | + max_order_of_magnitude = _get_order_of_magnitude(min(abs(u[1]) for u in users)) |
| 407 | + weights = tuple(int(u[1] * max_order_of_magnitude) for u in users) |
| 408 | + |
| 409 | + greatest_common_divisor = compatible_math_gcd(*weights) |
375 | 410 | normalized_values = [
|
376 | 411 | (
|
377 |
| - user.__name__, |
378 |
| - round(target_min_weight * value / min(u[1] for u in users)), |
| 412 | + user[0].__name__, |
| 413 | + normalized_weight // greatest_common_divisor, |
379 | 414 | )
|
380 |
| - for user, value in users |
| 415 | + for user, normalized_weight in zip(users, weights) |
381 | 416 | ]
|
382 | 417 | generation_length_to_get_proper_distribution = sum(
|
383 | 418 | normalized_val[1] for normalized_val in normalized_values
|
|
0 commit comments