Skip to content

Commit

Permalink
Add PERCENTILES_TO_CHART param in stats.py to make the Response Time …
Browse files Browse the repository at this point in the history
…Chart configurable (#2313)

* Added command line parameters to speciufy percentiles for chart

Percentiles were originally 50% and 95%. Command line parameter can
specify chart and report percentiles. Defalt value is 50% and 95%.

* Added new command line argument --percentiles

Old parameters --percemtile1 and percentile2 were removed.
Added test case in test_main for -percentiles parameter

* Updated format by black

* Updated web.py format by black

* Removed command line argument for percentiles and test

Updated to get percentiles info from stats.py::

* Added test in test_main. Updated response_time_percentile variable name

* updated response_time_percentile variable name.

* Fixed stats.py format and test_main.py unnecessary comment out

* Grouped stats import and reorderd response_time_percentile variable in
javascript and html file

Modified test in test_main.py

* updated report.html and locust.js "% percentile" to "th percentile" in
response time chart

* updated percentiles related test in test_main.py

* reformated test_main.py

* change order of PERCENTILES_TO_CHART param
  • Loading branch information
A1BOCO authored Mar 12, 2023
1 parent 09227b8 commit 0e56a8b
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 21 deletions.
2 changes: 2 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
19 changes: 19 additions & 0 deletions locust/main.py
Original file line number Diff line number Diff line change
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 @@ -179,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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"]},
{data: stats_history["response_time_percentile_2"]},
]
});

Expand Down
6 changes: 3 additions & 3 deletions locust/templates/stats_data.html
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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

0 comments on commit 0e56a8b

Please sign in to comment.