Skip to content

Commit

Permalink
Merge pull request #1516 from taojy123/master
Browse files Browse the repository at this point in the history
 Add Feature: Download Report File
  • Loading branch information
cyberw authored Aug 18, 2020
2 parents 207d223 + ee069eb commit e13b536
Show file tree
Hide file tree
Showing 7 changed files with 397 additions and 7 deletions.
5 changes: 4 additions & 1 deletion examples/use_as_lib.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import gevent
from locust import HttpUser, task, between
from locust.env import Environment
from locust.stats import stats_printer
from locust.stats import stats_printer, stats_history
from locust.log import setup_logging

setup_logging("INFO", None)
Expand Down Expand Up @@ -29,6 +29,9 @@ def task_404(self):
# start a greenlet that periodically outputs the current stats
gevent.spawn(stats_printer(env.stats))

# start a greenlet that save current stats to history
gevent.spawn(stats_history(env.runner))

# start the test
env.runner.start(1, spawn_rate=10)

Expand Down
5 changes: 4 additions & 1 deletion locust/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from .env import Environment
from .log import setup_logging, greenlet_exception_logger
from . import stats
from .stats import print_error_report, print_percentile_stats, print_stats, stats_printer
from .stats import print_error_report, print_percentile_stats, print_stats, stats_printer, stats_history
from .stats import StatsCSV, StatsCSVFileWriter
from .user import User
from .user.inspectuser import get_task_ratio_dict, print_task_ratio
Expand Down Expand Up @@ -336,6 +336,8 @@ def timelimit_stop():
if options.csv_prefix:
gevent.spawn(stats_csv_writer.stats_writer).link_exception(greenlet_exception_handler)

gevent.spawn(stats_history, runner)

def shutdown():
"""
Shut down locust by firing quitting event, printing/writing stats and exiting
Expand Down Expand Up @@ -364,6 +366,7 @@ def shutdown():
print_percentile_stats(runner.stats)

print_error_report(runner.stats)

sys.exit(code)

# install SIGTERM handler
Expand Down
24 changes: 23 additions & 1 deletion locust/stats.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import hashlib
import time
from collections import namedtuple, OrderedDict
Expand All @@ -18,10 +19,14 @@
"""Default interval for how frequently results are written to console."""
CONSOLE_STATS_INTERVAL_SEC = 2

"""Default interval for how frequently results are written to history."""
HISTORY_STATS_INTERVAL_SEC = 5

"""Default interval for how frequently CSV files are written if this option is configured."""
CSV_STATS_INTERVAL_SEC = 1
CSV_STATS_FLUSH_INTERVAL_SEC = 10


"""
Default window size/resolution - in seconds - when calculating the current
response time percentile
Expand Down Expand Up @@ -112,6 +117,7 @@ def __init__(self, use_response_times_cache=True):
self.entries = {}
self.errors = {}
self.total = StatsEntry(self, "Aggregated", None, use_response_times_cache=self.use_response_times_cache)
self.history = []

@property
def num_requests(self):
Expand Down Expand Up @@ -167,6 +173,7 @@ def reset_all(self):
self.errors = {}
for r in self.entries.values():
r.reset()
self.history = []

def clear_all(self):
"""
Expand All @@ -175,6 +182,7 @@ def clear_all(self):
self.total = StatsEntry(self, "Aggregated", None, use_response_times_cache=self.use_response_times_cache)
self.entries = {}
self.errors = {}
self.history = []

def serialize_stats(self):
return [self.entries[key].get_stripped_report() for key in self.entries.keys() if not (self.entries[key].num_requests == 0 and self.entries[key].num_failures == 0)]
Expand Down Expand Up @@ -736,17 +744,31 @@ def print_error_report(stats):
console_logger.info("-" * (80 + STATS_NAME_WIDTH))
console_logger.info("")


def stats_printer(stats):
def stats_printer_func():
while True:
print_stats(stats)
gevent.sleep(CONSOLE_STATS_INTERVAL_SEC)
return stats_printer_func


def sort_stats(stats):
return [stats[key] for key in sorted(stats.keys())]

def stats_history(runner):
"""Save current stats info to history for charts of report."""
while True:
stats = runner.stats
r = {
'time': datetime.datetime.now().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,
'user_count': runner.user_count or 0,
}
stats.history.append(r)
gevent.sleep(HISTORY_STATS_INTERVAL_SEC)

class StatsCSV():
"""Write statistics to csv_writer stream."""
Expand Down
3 changes: 2 additions & 1 deletion locust/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ <h2>Edit running load test</h2>
<a href="./stats/requests_full_history/csv">Download full request statistics history CSV</a><br>
{% endif %}
<a href="./stats/failures/csv">Download failures CSV</a><br>
<a href="./exceptions/csv">Download exceptions CSV</a>
<a href="./exceptions/csv">Download exceptions CSV</a><br>
<a href="./stats/report" target="_blank">Download Report</a><br>
</div>
</div>
<div style="display:none;">
Expand Down
257 changes: 257 additions & 0 deletions locust/templates/report.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Report</title>
<style>
.container {
width: 1000px;
margin: 0 auto;
padding: 10px;
background: #173529;
font-family: Arial, Helvetica, sans-serif;
font-size: 14px;
color: #fff;
}

.info span{
color: #b3c3bc;
}

table {
border-collapse: collapse;
text-align: center;
width: 100%;
}

td, th {
border: 1px solid #cad9ea;
color: #666;
height: 30px;
}

thead th {
background-color: #cce8eb;
width: 100px;
}

tr:nth-child(odd) {
background: #fff;
}

tr:nth-child(even) {
background: #f5fafa;
}

.charts-container .chart {
width: 100%;
height: 350px;
margin-bottom: 30px;
}

.download {
float: right;
}

.download a {
color: #00ca5a;
}
</style>
</head>
<body>
<div class="container">
<h1>Locust Test Report</h1>

<div class="info">
<p class="download"><a href="?download=1">Download the Report</a></p>
<p>During: <span>{{ start_time }} - {{ end_time }}</span></p>
<p>Target Host: <span>{{ host }}</span></p>
</div>

<div class="requests">
<h2>Request Statistics</h2>
<table>
<thead>
<tr>
<th>Method</th>
<th>Name</th>
<th># Requests</th>
<th># Fails</th>
<th>Average (ms)</th>
<th>Min (ms)</th>
<th>Max (ms)</th>
<th>Average size (bytes)</th>
<th>RPS</th>
<th>Failures/s</th>
</tr>
</thead>
<tbody>
{% for s in requests_statistics %}
<tr>
<td>{{ s.method or "" }}</td>
<td>{{ s.name }}</td>
<td>{{ int(s.num_requests) }}</td>
<td>{{ int(s.num_failures) }}</td>
<td>{{ int(s.avg_response_time) }}</td>
<td>{{ int(s.min_response_time or 0) }}</td>
<td>{{ int(s.max_response_time) }}</td>
<td>{{ int(s.avg_content_length) }}</td>
<td>{{ round(s.total_rps, 1) }}</td>
<td>{{ round(s.total_fail_per_sec, 1) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

<div class="responses">
<h2>Response Time Statistics</h2>
<table>
<thead>
<tr>
<th>Method</th>
<th>Name</th>
<th>50%ile (ms)</th>
<th>60%ile (ms)</th>
<th>70%ile (ms)</th>
<th>80%ile (ms)</th>
<th>90%ile (ms)</th>
<th>95%ile (ms)</th>
<th>99%ile (ms)</th>
<th>100%ile (ms)</th>
</tr>
</thead>
<tbody>
{% for s in requests_statistics %}
<tr>
<td>{{ s.method or "" }}</td>
<td>{{ s.name }}</td>
<td>{{ int(s.get_response_time_percentile(0.5)) }}</td>
<td>{{ int(s.get_response_time_percentile(0.6)) }}</td>
<td>{{ int(s.get_response_time_percentile(0.7)) }}</td>
<td>{{ int(s.get_response_time_percentile(0.8)) }}</td>
<td>{{ int(s.get_response_time_percentile(0.9)) }}</td>
<td>{{ int(s.get_response_time_percentile(0.95)) }}</td>
<td>{{ int(s.get_response_time_percentile(0.99)) }}</td>
<td>{{ int(s.get_response_time_percentile(1)) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

{% if failures_statistics %}
<div class="failures">
<h2>Failures Statistics</h2>
<table>
<thead>
<tr>
<th>Method</th>
<th>Name</th>
<th>Error</th>
<th>Occurrences</th>
</tr>
</thead>
<tbody>
{% for s in failures_statistics %}
<tr>
<td>{{ s.method or "" }}</td>
<td>{{ s.name }}</td>
<td>{{ s.error }}</td>
<td>{{ s.occurrences }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}

{% if exceptions_statistics %}
<div class="exceptions">
<h2>Exceptions Statistics</h2>
<table>
<thead>
<tr>
<th>Count</th>
<th>Message</th>
<th>Traceback</th>
<th>Nodes</th>
</tr>
</thead>
<tbody>
{% for s in exceptions_statistics %}
<tr>
<td>{{ s.count }}</td>
<td>{{ s.msg }}</td>
<td>{{ s.traceback }}</td>
<td>{{ s.nodes }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}

{% if history %}
<div class="charts-container">
<h2>Charts</h2>
</div>
{% endif %}
</div>

{# <script type="text/javascript" src="/static/jquery-1.11.3.min.js"></script> #}
{# <script type="text/javascript" src="/static/echarts.common.min.js"></script> #}
{# <script type="text/javascript" src="/static/vintage.js"></script> #}
{# <script type="text/javascript" src="/static/chart.js"></script> #}
<script>
{{ static_js|safe }}
</script>

<script>

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 usersChart = new LocustLineChart($(".charts-container"), "Number of Users", ["Users"], "users");

rpsChart.chart.setOption({
xAxis: {
data: [ {% for r in history %}"{{ r.time }}", {% endfor %} ],
},
series: [
{
data: [ {% for r in history %}{{ r.current_rps }}, {% endfor %} ]
},
{
data: [ {% for r in history %}{{ r.current_fail_per_sec }}, {% endfor %} ]
},
]
});

responseTimeChart.chart.setOption({
xAxis: {
data: [ {% for r in history %}"{{ r.time }}", {% endfor %} ],
},
series: [
{
data: [ {% for r in history %}{{ r.response_time_percentile_50 }}, {% endfor %} ]
},
{
data: [ {% for r in history %}{{ r.response_time_percentile_95 }}, {% endfor %} ]
},
]
});

usersChart.chart.setOption({
xAxis: {
data: [ {% for r in history %}"{{ r.time }}", {% endfor %} ],
},
series: [
{
data: [ {% for r in history %}{{ r.user_count }}, {% endfor %} ]
},
]
});


</script>
</body>
</html>
Loading

0 comments on commit e13b536

Please sign in to comment.