Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customization Feature for Percentile Display on Statistics Page #2550

Merged
merged 25 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9b2e6e4
Add .venv to .gitignore for Python virtual environment exclusion
FooQoo Jan 14, 2024
7db0504
Implement user-editable columns in statics screen
FooQoo Jan 14, 2024
e7282f5
Remove ninetyNinthResponseTime
FooQoo Jan 14, 2024
374e10a
Correct statics to statistics
FooQoo Jan 15, 2024
bb6cfb2
Merge branch 'master' into feature/user-editable-statics-columns
FooQoo Jan 15, 2024
f0ff2e7
Apply ruff formatting
FooQoo Jan 15, 2024
f6ee8ad
Add unittests for normalize_decimal
FooQoo Jan 15, 2024
1bb11b4
Add integration test
FooQoo Jan 15, 2024
b95a03b
Apply ruff formatting for test_main.py
FooQoo Jan 15, 2024
a8d483c
Revert changes to percentile display from 50%ile back to original
FooQoo Jan 15, 2024
3981277
Update configuration docs
FooQoo Jan 15, 2024
c47c2a6
Implement column visibility toggle feature in modern UI
FooQoo Jan 15, 2024
75ffdd5
Remove export of tableStructure
FooQoo Jan 15, 2024
2d6ced9
Add validation for PERCENTILES_TO_STATISTICS
FooQoo Jan 15, 2024
e02b0b6
Remove unnecessary comment
FooQoo Jan 15, 2024
061dbcb
Reverting table component to original
FooQoo Jan 15, 2024
6458cde
Added function to useSelectViewColumns hook for excluding columns
FooQoo Jan 15, 2024
1966c67
Build webui
FooQoo Jan 15, 2024
5c5d17a
Add unittest for filterStructure
FooQoo Jan 15, 2024
0cb9a62
Revert legacy UI
FooQoo Jan 16, 2024
4b5d2ac
Update configuration docs of PERCENTILES_TO_STATISTICS
FooQoo Jan 16, 2024
29974c3
Capitalize description of parameters in configuration docs
FooQoo Jan 16, 2024
07b7ae7
Format import
FooQoo Jan 16, 2024
c214bec
Aggregate table structure filtering logic within hook
FooQoo Jan 16, 2024
5cd7d62
Build frontend
FooQoo Jan 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ __pycache__
.sass-cache/
.env
yarn-error.log
.venv
2 changes: 2 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,6 @@ The list of statistics parameters that can be modified is:
+-------------------------------------------+--------------------------------------------------------------------------------------+
| PERCENTILES_TO_CHART | The list of response time percentiles for response time chart |
+-------------------------------------------+--------------------------------------------------------------------------------------+
| PERCENTILES_TO_STATISTICS | The list of response time percentiles for response time statistics |
Copy link
Collaborator

@cyberw cyberw Jan 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest you change this to "list of response time percentiles to show in the statistics table"

And maybe remove "The" from all descriptions, I dont know why that is in there :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead maybe add something about this and PERCENTILES_TO_CHART being UI settings (just to make it clear)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cyberw
I've updated the descriptions for PERCENTILES_TO_CHART and PERCENTILES_TO_STATISTICS as suggested.
3981277

+-------------------------------------------+--------------------------------------------------------------------------------------+

9 changes: 7 additions & 2 deletions locust/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def resize_handler(signum: int, frame: FrameType | None):

PERCENTILES_TO_REPORT = [0.50, 0.66, 0.75, 0.80, 0.90, 0.95, 0.98, 0.99, 0.999, 0.9999, 1.0]

PERCENTILES_TO_STATISTICS = [0.95, 0.99]
PERCENTILES_TO_CHART = [0.50, 0.95]
MODERN_UI_PERCENTILES_TO_CHART = [0.95]

Expand Down Expand Up @@ -681,6 +682,11 @@ def _cache_response_times(self, t: int) -> None:
self.response_times_cache.popitem(last=False)

def to_dict(self, escape_string_values=False):
response_time_percentiles = {
f"response_time_percentile_{percentile}": self.get_response_time_percentile(percentile)
for percentile in PERCENTILES_TO_STATISTICS
}

return {
"method": escape(self.method or "") if escape_string_values else self.method,
"name": escape(self.name) if escape_string_values else self.name,
Expand All @@ -693,8 +699,7 @@ def to_dict(self, escape_string_values=False):
"current_rps": self.current_rps,
"current_fail_per_sec": self.current_fail_per_sec,
"median_response_time": self.median_response_time,
andrewbaldwin44 marked this conversation as resolved.
Show resolved Hide resolved
"ninetieth_response_time": self.get_response_time_percentile(0.9),
"ninety_ninth_response_time": self.get_response_time_percentile(0.99),
**response_time_percentiles,
"avg_content_length": self.avg_content_length,
}

Expand Down
14 changes: 8 additions & 6 deletions locust/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,9 @@ <h2>Edit running load test</h2>
<th class="stats_label numeric nowrap" href="#" data-sortkey="num_requests" title="Number of requests"># Requests</th>
<th class="stats_label numeric nowrap" href="#" data-sortkey="num_failures" title="Number of failures"># Fails</th>
<th class="stats_label numeric" href="#" data-sortkey="median_response_time" title="Median response time">Median (ms)</th>
<th class="stats_label numeric" href="#" data-sortkey="ninetieth_response_time" title="Ninetieth percentile response time, because the response time greater than 100ms is rounded, you may see it greater than the max response time">90%ile (ms)</th>
<th class="stats_label numeric" href="#" data-sortkey="ninety_ninth_response_time"
title="Ninety-ninth percentile response time, because the response time greater than 100ms is rounded, you may see it greater than the max response time">
99%ile (ms)</th>
{% for percentile in percentiles_to_statistics %}
<th class="stats_label numeric" href="#" data-sortkey="{{percentile}}_response_time" title="{{percentile}} percentile response time, because the response time greater than 100ms is rounded, you may see it greater than the max response time">{{percentile}}%ile (ms)</th>
{% endfor %}
<th class="stats_label numeric" href="#" data-sortkey="avg_response_time" title="Average response time">Average (ms)</th>
<th class="stats_label numeric" href="#" data-sortkey="min_response_time" title="Min response time">Min (ms)</th>
<th class="stats_label numeric" href="#" data-sortkey="max_response_time" title="Max response time">Max (ms)</th>
Expand Down Expand Up @@ -319,8 +318,11 @@ <h2>Version <a href="https://github.com/locustio/locust/releases/tag/{{version}}
<td class="numeric"><%= this.num_requests %></td>
<td class="numeric"><%= this.num_failures %></td>
<td class="numeric"><%= Math.round(this.median_response_time) %></td>
<td class="numeric"><%= Math.round(this.ninetieth_response_time) %></td>
<td class="numeric"><%= Math.round(this.ninety_ninth_response_time) %></td>
<% for (const [key, value] of Object.entries(this)) { %>
<% if (key.startsWith("response_time_percentile")) { %>
<td class="numeric"><%= Math.round(value) %></td>
<% } %>
<% } %>
<td class="numeric"><%= Math.round(this.avg_response_time) %></td>
<td class="numeric"><%= this.min_response_time %></td>
<td class="numeric"><%= this.max_response_time %></td>
Expand Down
26 changes: 26 additions & 0 deletions locust/test/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,32 @@ def my_task(self):
stdout, stderr = proc.communicate()
self.assertIn("Starting web interface at", stderr)

def test_percentiles_to_statistics(self):
port = get_free_tcp_port()
with temporary_file(
content=textwrap.dedent(
"""
from locust import User, task, constant, events
from locust.stats import PERCENTILES_TO_STATISTICS
PERCENTILES_TO_STATISTICS = [0.9, 0.99]
class TestUser(User):
wait_time = constant(3)
@task
def my_task(self):
print("running my_task()")
"""
)
) as file_path:
proc = subprocess.Popen(
["locust", "-f", file_path, "--web-port", str(port), "--autostart"], stdout=PIPE, stderr=PIPE, text=True
)
gevent.sleep(1)
response = requests.get(f"http://localhost:{port}/")
self.assertEqual(200, response.status_code)
proc.send_signal(signal.SIGTERM)
stdout, stderr = proc.communicate()
self.assertIn("Starting web interface at", stderr)

def test_invalid_percentile_parameter(self):
with temporary_file(
content=textwrap.dedent(
Expand Down
7 changes: 6 additions & 1 deletion locust/test/test_util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import unittest
from locust.util.timespan import parse_timespan
from locust.util.rounding import proper_round
from locust.util.rounding import proper_round, normalize_decimal


class TestParseTimespan(unittest.TestCase):
Expand Down Expand Up @@ -30,3 +30,8 @@ def test_rounding_up(self):
self.assertEqual(4, proper_round(3.5))
self.assertEqual(5, proper_round(4.5))
self.assertEqual(6, proper_round(5.5))

def test_normalize_decimal(self):
self.assertEqual(1, normalize_decimal(1))
self.assertEqual(0.99, normalize_decimal(0.99))
self.assertEqual("a", normalize_decimal("a"))
andrewbaldwin44 marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 7 additions & 0 deletions locust/util/rounding.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
def proper_round(val, digits=0):
return round(val + 10 ** (-len(str(val)) - 1), digits)


def normalize_decimal(value):
if isinstance(value, float) and value.is_integer():
return int(value)
else:
return value
6 changes: 6 additions & 0 deletions locust/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from .util.timespan import parse_timespan
from .html import get_html_report
from flask_cors import CORS
from .util.rounding import normalize_decimal

if TYPE_CHECKING:
from .env import Environment
Expand Down Expand Up @@ -613,11 +614,16 @@ def update_template_args(self):
if self.modern_ui:
percentiles = {
"percentiles_to_chart": stats_module.MODERN_UI_PERCENTILES_TO_CHART,
"percentiles_to_statistics": stats_module.PERCENTILES_TO_STATISTICS,
}
else:
percentiles = {
"percentile1": stats_module.PERCENTILES_TO_CHART[0],
"percentile2": stats_module.PERCENTILES_TO_CHART[1],
# Convert percentiles to percent notation for use in templates
"percentiles_to_statistics": [
normalize_decimal(percentile * 100) for percentile in stats_module.PERCENTILES_TO_STATISTICS
],
andrewbaldwin44 marked this conversation as resolved.
Show resolved Hide resolved
}

self.template_args = {
Expand Down
1 change: 1 addition & 0 deletions locust/webui/dist/assets/auth-3a8f3d1c.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion locust/webui/dist/assets/auth-3cd88f78.js.map

This file was deleted.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion locust/webui/dist/auth.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<meta name="theme-color" content="#000000" />

<title>Locust</title>
<script type="module" crossorigin src="/assets/index-ea7131ad.js"></script>
<script type="module" crossorigin src="/assets/index-fd9ede73.js"></script>
</head>
<body>
<div id="root"></div>
Expand Down
2 changes: 1 addition & 1 deletion locust/webui/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<meta name="theme-color" content="#000000" />

<title>Locust</title>
<script type="module" crossorigin src="/assets/index-ea7131ad.js"></script>
<script type="module" crossorigin src="/assets/index-fd9ede73.js"></script>
</head>
<body>
<div id="root"></div>
Expand Down
11 changes: 9 additions & 2 deletions locust/webui/src/components/StatsTable/StatsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { connect } from 'react-redux';

import Table from 'components/Table/Table';
import { swarmTemplateArgs } from "constants/swarm";
import useSortByField from 'hooks/useSortByField';
import { IRootState } from 'redux/store';
import { ISwarmStat } from 'types/ui.types';

const percentilesToStatisticsRows = swarmTemplateArgs.percentilesToStatistics
? swarmTemplateArgs.percentilesToStatistics.map(percentile => ({
title: `${percentile * 100}%ile (ms)`,
key: `responseTimePercentile${percentile}` as keyof ISwarmStat,
}))
: [];

const tableStructure = [
{ key: 'method', title: 'Type' },
{ key: 'name', title: 'Name' },
{ key: 'numRequests', title: '# Requests' },
{ key: 'numFailures', title: '# Fails' },
{ key: 'medianResponseTime', title: 'Median (ms)', round: 2 },
{ key: 'ninetiethResponseTime', title: '90%ile (ms)' },
{ key: 'ninetyNinthResponseTime', title: '99%ile (ms)' },
...percentilesToStatisticsRows,
{ key: 'avgResponseTime', title: 'Average (ms)', round: 2 },
{ key: 'minResponseTime', title: 'Min (ms)' },
{ key: 'maxResponseTime', title: 'Max (ms)' },
Expand Down
1 change: 1 addition & 0 deletions locust/webui/src/redux/slice/swarm.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface ISwarmState {
numUsers: number | null;
overrideHostWarning: boolean;
percentilesToChart: number[];
percentilesToStatistics: number[];
runTime?: string | number;
showUserclassPicker: boolean;
spawnRate: number | null;
Expand Down
16 changes: 8 additions & 8 deletions locust/webui/src/test/mocks/statsRequest.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export const statsResponseMock = {
method: 'GET',
min_response_time: 0.0,
name: '/',
ninetieth_response_time: 0,
ninety_ninth_response_time: 1,
"response_time_percentile_0.9": 0,
"response_time_percentile_0.99": 1,
num_failures: 12652,
num_requests: 12652,
safe_name: '/',
Expand All @@ -40,8 +40,8 @@ export const statsResponseMock = {
method: '',
min_response_time: 0.0,
name: 'Aggregated',
ninetieth_response_time: 0,
ninety_ninth_response_time: 1,
"response_time_percentile_0.9": 0,
"response_time_percentile_0.99": 1,
num_failures: 12652,
num_requests: 12652,
safe_name: 'Aggregated',
Expand Down Expand Up @@ -113,8 +113,8 @@ export const statsResponseTransformed = {
method: 'GET',
minResponseTime: 0,
name: '/',
ninetiethResponseTime: 0,
ninetyNinthResponseTime: 1,
"responseTimePercentile0.9": 0,
"responseTimePercentile0.99": 1,
numFailures: 12652,
numRequests: 12652,
safeName: '/',
Expand All @@ -129,8 +129,8 @@ export const statsResponseTransformed = {
method: '',
minResponseTime: 0,
name: 'Aggregated',
ninetiethResponseTime: 0,
ninetyNinthResponseTime: 1,
"responseTimePercentile0.9": 0,
"responseTimePercentile0.99": 1,
numFailures: 12652,
numRequests: 12652,
safeName: 'Aggregated',
Expand Down
2 changes: 2 additions & 0 deletions locust/webui/src/test/mocks/swarmState.mock.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const percentilesToChart = [0.5, 0.95];
export const percentilesToStatistics = [0.5, 0.95, 0.99];

export const swarmStateMock = {
availableShapeClasses: ['Default'],
Expand All @@ -13,6 +14,7 @@ export const swarmStateMock = {
overrideHostWarning: false,
percentile1: 0.95,
percentile2: 0.99,
percentilesToStatistics: percentilesToStatistics,
showUserclassPicker: false,
spawnRate: null,
state: 'ready',
Expand Down
1 change: 1 addition & 0 deletions locust/webui/src/types/swarm.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ export interface IReportTemplateArgs extends IReport {
history: IHistory[];
isReport?: boolean;
percentilesToChart: number[];
percentilesToStatistics: number[];
}
3 changes: 1 addition & 2 deletions locust/webui/src/types/ui.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ export interface ISwarmStat {
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
minResponseTime: number;
name: string;
ninetiethResponseTime: number;
ninetyNinthResponseTime: number;
[key: `responseTimePercentile${number}`]: number;
numFailures: number;
numRequests: number;
safeName: string;
Expand Down