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

Add PERCENTILES_TO_CHART param in stats.py to make the Response Time Chart configurable #2313

Merged
merged 13 commits into from Mar 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/configuration.rst
Expand Up @@ -201,4 +201,6 @@ The list of statistics parameters that can be modified is:
+-------------------------------------------+--------------------------------------------------------------------------------------+
| PERCENTILES_TO_REPORT | The list of response time percentiles to be calculated & reported |
+-------------------------------------------+--------------------------------------------------------------------------------------+
| PERCENTILES_TO_CHART | The list of response time percentiles for response time chart |
+-------------------------------------------+--------------------------------------------------------------------------------------+

3 changes: 3 additions & 0 deletions locust/html.py
Expand Up @@ -4,6 +4,7 @@
import datetime
from itertools import chain
from .stats import sort_stats
from . import stats as stats_module
from .user.inspectuser import get_ratio
from html import escape
from json import dumps
Expand Down Expand Up @@ -94,6 +95,8 @@ def get_html_report(environment, show_download_link=True):
show_download_link=show_download_link,
locustfile=environment.locustfile,
tasks=dumps(task_data),
percentile1=stats_module.PERCENTILES_TO_CHART[0],
percentile2=stats_module.PERCENTILES_TO_CHART[1],
)

return res
20 changes: 19 additions & 1 deletion locust/main.py
Expand Up @@ -100,6 +100,24 @@ def main():
user_classes[key] = value
available_user_classes[key] = value

if len(stats.PERCENTILES_TO_CHART) != 2:
logging.error("stats.PERCENTILES_TO_CHART parameter should be 2 parameters \n")
sys.exit(1)

def is_valid_percentile(parameter):
try:
if 0 < float(parameter) < 1:
return True
return False
except ValueError:
return False

for percentile in stats.PERCENTILES_TO_CHART:
if not is_valid_percentile(percentile):
logging.error(
"stats.PERCENTILES_TO_CHART parameter need to be float and value between. 0 < percentile < 1 Eg 0.95\n"
)
sys.exit(1)
# parse all command line options
options = parse_options()

Expand Down Expand Up @@ -160,7 +178,6 @@ def main():
user_classes = list(user_classes.values())

if os.name != "nt" and not options.master:

try:
import resource

Expand All @@ -180,6 +197,7 @@ def main():

# create locust Environment
locustfile_path = None if not locustfile else os.path.basename(locustfile)

environment = create_environment(
user_classes,
options,
Expand Down
14 changes: 7 additions & 7 deletions locust/static/locust.js
Expand Up @@ -219,8 +219,8 @@ function update_stats_charts(){
responseTimeChart.chart.setOption({
xAxis: {data: stats_history["time"]},
series: [
{data: stats_history["response_time_percentile_50"], markLine: createMarkLine()},
{data: stats_history["response_time_percentile_95"]},
{data: stats_history["response_time_percentile_1"], markLine: createMarkLine()},
{data: stats_history["response_time_percentile_2"]},
]
});

Expand All @@ -235,7 +235,7 @@ function update_stats_charts(){

// init charts
var rpsChart = new LocustLineChart($(".charts-container"), "Total Requests per Second", ["RPS", "Failures/s"], "reqs/s", ['#00ca5a', '#ff6d6d']);
var responseTimeChart = new LocustLineChart($(".charts-container"), "Response Times (ms)", ["Median Response Time", "95% percentile"], "ms");
var responseTimeChart = new LocustLineChart($(".charts-container"), "Response Times (ms)", [(percentile1 * 100).toString() + "th percentile" , (percentile2 * 100).toString() + "th percentile"], "ms");
var usersChart = new LocustLineChart($(".charts-container"), "Number of Users", ["Users"], "users");
charts.push(rpsChart, responseTimeChart, usersChart);
echarts.connect([rpsChart.chart,responseTimeChart.chart,usersChart.chart])
Expand Down Expand Up @@ -264,8 +264,8 @@ function updateStats() {
stats_history["user_count"].push({"value": null});
stats_history["current_rps"].push({"value": null});
stats_history["current_fail_per_sec"].push({"value": null});
stats_history["response_time_percentile_50"].push({"value": null});
stats_history["response_time_percentile_95"].push({"value": null});
stats_history["response_time_percentile_1"].push({"value": null});
stats_history["response_time_percentile_2"].push({"value": null});
}

// update stats chart to ensure the stop spacing appears as part
Expand Down Expand Up @@ -301,8 +301,8 @@ function updateStats() {
stats_history["user_count"].push({"value": report.user_count});
stats_history["current_rps"].push({"value": total.current_rps, "users": report.user_count});
stats_history["current_fail_per_sec"].push({"value": total.current_fail_per_sec, "users": report.user_count});
stats_history["response_time_percentile_50"].push({"value": report.current_response_time_percentile_50, "users": report.user_count});
stats_history["response_time_percentile_95"].push({"value": report.current_response_time_percentile_95, "users": report.user_count});
stats_history["response_time_percentile_1"].push({"value": report.current_response_time_percentile_1, "users": report.user_count});
stats_history["response_time_percentile_2"].push({"value": report.current_response_time_percentile_2, "users": report.user_count});
update_stats_charts();

} catch(i){
Expand Down
8 changes: 6 additions & 2 deletions locust/stats.py
Expand Up @@ -133,6 +133,8 @@ def resize_handler(signum: int, frame: Optional[FrameType]):

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_CHART = [0.50, 0.95]


class RequestStatsAdditionError(Exception):
pass
Expand Down Expand Up @@ -888,8 +890,10 @@ def stats_history(runner: "Runner") -> None:
"time": datetime.datetime.now(tz=datetime.timezone.utc).strftime("%H:%M:%S"),
"current_rps": stats.total.current_rps or 0,
"current_fail_per_sec": stats.total.current_fail_per_sec or 0,
"response_time_percentile_95": stats.total.get_current_response_time_percentile(0.95) or 0,
"response_time_percentile_50": stats.total.get_current_response_time_percentile(0.5) or 0,
"response_time_percentile_1": stats.total.get_current_response_time_percentile(PERCENTILES_TO_CHART[0])
or 0,
"response_time_percentile_2": stats.total.get_current_response_time_percentile(PERCENTILES_TO_CHART[1])
or 0,
"user_count": runner.user_count or 0,
}
stats.history.append(r)
Expand Down
2 changes: 2 additions & 0 deletions locust/templates/index.html
Expand Up @@ -357,6 +357,8 @@ <h2>Version <a href="https://github.com/locustio/locust/releases/tag/{{version}}
]]>
</script>
<script type="text/javascript">
var percentile1 = {{ percentile1|tojson }};
var percentile2 = {{ percentile2|tojson }};
{% include 'stats_data.html' %}
</script>
<script type="text/javascript" src="./static/chart.js?v={{ version }}"></script>
Expand Down
8 changes: 5 additions & 3 deletions locust/templates/report.html
Expand Up @@ -198,8 +198,10 @@ <h2>Final ratio</h2>

<script>
{% include 'stats_data.html' %}
var percentile1 = {{ percentile1|tojson }}
var percentile2 = {{ percentile2|tojson }}
var rpsChart = new LocustLineChart($(".charts-container"), "Total Requests per Second", ["RPS", "Failures/s"], "reqs/s", ['#00ca5a', '#ff6d6d']);
var responseTimeChart = new LocustLineChart($(".charts-container"), "Response Times (ms)", ["Median Response Time", "95% percentile"], "ms");
var responseTimeChart = new LocustLineChart($(".charts-container"), "Response Times (ms)", [(percentile1*100).toString() + "th percentile" , (percentile2*100).toString() + "th percentile"], "ms");
var usersChart = new LocustLineChart($(".charts-container"), "Number of Users", ["Users"], "users");

if(stats_history["time"].length > 0){
Expand All @@ -214,8 +216,8 @@ <h2>Final ratio</h2>
responseTimeChart.chart.setOption({
xAxis: {data: stats_history["time"]},
series: [
{data: stats_history["response_time_percentile_50"]},
{data: stats_history["response_time_percentile_95"]},
{data: stats_history["response_time_percentile_1"]},
cyberw marked this conversation as resolved.
Show resolved Hide resolved
{data: stats_history["response_time_percentile_2"]},
]
});

Expand Down
6 changes: 3 additions & 3 deletions locust/templates/stats_data.html
@@ -1,10 +1,10 @@
{% set time_data = [] %}{% set user_count_data = [] %}{% set current_rps_data = [] %}{% set current_fail_per_sec_data = [] %}{% set response_time_percentile_50_data = [] %}{% set response_time_percentile_95_data = [] %}{% for r in history %}{% do time_data.append(r.time) %}{% do user_count_data.append({"value": r.user_count}) %}{% do current_rps_data.append({"value": r.current_rps, "users": r.user_count}) %}{% do current_fail_per_sec_data.append({"value": r.current_fail_per_sec, "users": r.user_count}) %}{% do response_time_percentile_50_data.append({"value": r.response_time_percentile_50, "users": r.user_count}) %}{% do response_time_percentile_95_data.append({"value": r.response_time_percentile_95, "users": r.user_count}) %}{% endfor %}
{% set time_data = [] %}{% set user_count_data = [] %}{% set current_rps_data = [] %}{% set current_fail_per_sec_data = [] %}{% set response_time_percentile_2_data = [] %}{% set response_time_percentile_1_data = [] %}{% for r in history %}{% do time_data.append(r.time) %}{% do user_count_data.append({"value": r.user_count}) %}{% do current_rps_data.append({"value": r.current_rps, "users": r.user_count}) %}{% do current_fail_per_sec_data.append({"value": r.current_fail_per_sec, "users": r.user_count}) %}{% do response_time_percentile_2_data.append({"value": r.response_time_percentile_2, "users": r.user_count}) %}{% do response_time_percentile_1_data.append({"value": r.response_time_percentile_1, "users": r.user_count}) %}{% endfor %}
var stats_history = {
"time": {{ time_data | tojson }}.map(server_time => new Date(new Date().setUTCHours(...(server_time.split(":")))).toLocaleTimeString()),
"user_count": {{ user_count_data | tojson }},
"current_rps": {{ current_rps_data | tojson }},
"current_fail_per_sec": {{ current_fail_per_sec_data | tojson }},
"response_time_percentile_50": {{ response_time_percentile_50_data | tojson }},
"response_time_percentile_95": {{ response_time_percentile_95_data | tojson }},
"response_time_percentile_1": {{ response_time_percentile_1_data | tojson }},
"response_time_percentile_2": {{ response_time_percentile_2_data | tojson }},
"markers": [],
};
49 changes: 49 additions & 0 deletions locust/test/test_main.py
@@ -1,6 +1,7 @@
import json
import os
import platform

import pty
import signal
import subprocess
Expand Down Expand Up @@ -191,6 +192,54 @@ def my_task(self):
self.assertIn("Shutting down (exit code 0)", stderr)
self.assertEqual(0, proc.returncode)

def test_percentile_parameter(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_CHART
PERCENTILES_TO_CHART[0] = 0.9
PERCENTILES_TO_CHART[1] = 0.4
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(
"""
from locust import User, task, constant, events
from locust.stats import PERCENTILES_TO_CHART
PERCENTILES_TO_CHART[0] = 1.2
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, "--autostart"], stdout=PIPE, stderr=PIPE, text=True)
gevent.sleep(1)
stdout, stderr = proc.communicate()
self.assertIn("parameter need to be float and value between. 0 < percentile < 1 Eg 0.95", stderr)
self.assertEqual(1, proc.returncode)

def test_webserver_multiple_locustfiles(self):
with mock_locustfile(content=MOCK_LOCUSTFILE_CONTENT_A) as mocked1:
with mock_locustfile(content=MOCK_LOCUSTFILE_CONTENT_B) as mocked2:
Expand Down
19 changes: 13 additions & 6 deletions locust/web.py
Expand Up @@ -3,6 +3,7 @@
import logging
import os.path
from functools import wraps

from html import escape
from io import StringIO
from json import dumps
Expand Down Expand Up @@ -339,8 +340,8 @@ def request_stats() -> Response:
"errors": errors,
"total_rps": 0.0,
"fail_ratio": 0.0,
"current_response_time_percentile_95": None,
"current_response_time_percentile_50": None,
"current_response_time_percentile_1": None,
"current_response_time_percentile_2": None,
"state": STATE_MISSING,
"user_count": 0,
}
Expand Down Expand Up @@ -388,11 +389,15 @@ def request_stats() -> Response:
report["total_rps"] = stats[len(stats) - 1]["current_rps"]
report["fail_ratio"] = environment.runner.stats.total.fail_ratio
report[
"current_response_time_percentile_95"
] = environment.runner.stats.total.get_current_response_time_percentile(0.95)
"current_response_time_percentile_1"
] = environment.runner.stats.total.get_current_response_time_percentile(
stats_module.PERCENTILES_TO_CHART[0]
)
report[
"current_response_time_percentile_50"
] = environment.runner.stats.total.get_current_response_time_percentile(0.5)
"current_response_time_percentile_2"
] = environment.runner.stats.total.get_current_response_time_percentile(
stats_module.PERCENTILES_TO_CHART[1]
)

if isinstance(environment.runner, MasterRunner):
workers = []
Expand Down Expand Up @@ -555,6 +560,8 @@ def update_template_args(self):
"show_userclass_picker": self.userclass_picker_is_active,
"available_user_classes": available_user_classes,
"available_shape_classes": available_shape_classes,
"percentile1": stats_module.PERCENTILES_TO_CHART[0],
"percentile2": stats_module.PERCENTILES_TO_CHART[1],
}

def _update_shape_class(self, shape_class_name):
Expand Down