From bdf37624068de8cc103a944e8db177649a791d04 Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Mon, 30 Sep 2019 10:37:22 +0200 Subject: [PATCH 01/33] Add an option to allow tasks to finish running their iteration before exiting (--task-finish-wait-time) --- locust/core.py | 5 +++++ locust/main.py | 9 +++++++++ locust/runners.py | 5 +++++ locust/test/test_runners.py | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+) diff --git a/locust/core.py b/locust/core.py index 6c5ce620f5..e88efa4af6 100644 --- a/locust/core.py +++ b/locust/core.py @@ -129,6 +129,9 @@ class Locust(object): weight = 10 """Probability of locust being chosen. The higher the weight, the greater is the chance of it being chosen.""" + exit_at_end_of_iteration = False + """Used with --task-finish-wait-time to stop a locust at end of iteration""" + client = NoClientWarningRaiser() _catch_exceptions = True _setup_has_run = False # Internal state to see if we have already run @@ -432,6 +435,8 @@ def get_wait_secs(self): return millis / 1000.0 def wait(self): + if self.locust.exit_at_end_of_iteration: + raise GreenletExit() self._sleep(self.get_wait_secs()) def _sleep(self, seconds): diff --git a/locust/main.py b/locust/main.py index a42308b485..95510087b6 100644 --- a/locust/main.py +++ b/locust/main.py @@ -292,6 +292,15 @@ def parse_options(): help="sets the exit code to post on error" ) + parser.add_option( + '--task-finish-wait-time', + action='store', + type="int", + dest='task_finish_wait_time', + default=None, + help="number of seconds to wait for a taskset to complete an iteration before exiting. default is to terminate immediately." + ) + # Finalize # Return three-tuple of parser + the output from parse_args (opt obj, args) opts, args = parser.parse_args() diff --git a/locust/runners.py b/locust/runners.py index 268f70330b..6f85ecbfd3 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -177,6 +177,11 @@ def stop(self): # if we are currently hatching locusts we need to kill the hatching greenlet first if self.hatching_greenlet and not self.hatching_greenlet.ready(): self.hatching_greenlet.kill(block=True) + if self.options.task_finish_wait_time: + for l in self.locusts: + l.args[0].exit_at_end_of_iteration = True + if not self.locusts.join(timeout=self.options.task_finish_wait_time): + logger.info("Not all locusts finished their tasks & terminated in %s seconds. Killing them..." % self.options.task_finish_wait_time) self.locusts.kill(block=True) self.state = STATE_STOPPED events.locust_stop_hatching.fire() diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 8a51c75c2c..822800f9a2 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -54,6 +54,7 @@ def __init__(self): self.master_bind_port = 5557 self.heartbeat_liveness = 3 self.heartbeat_interval = 0.01 + self.task_finish_wait_time = None def reset_stats(self): pass @@ -348,3 +349,39 @@ def test_message_serialize(self): self.assertEqual(msg.type, rebuilt.type) self.assertEqual(msg.data, rebuilt.data) self.assertEqual(msg.node_id, rebuilt.node_id) + +class TestTaskFinishWaitTime(unittest.TestCase): + def test_task_finish_wait_time(self): + short_time = 0.05 + class MyTaskSet(TaskSet): + @task + def my_task(self): + TestTaskFinishWaitTime.state = "first" + gevent.sleep(short_time) + TestTaskFinishWaitTime.state = "second" # should only run when run time + task_finish_wait_time is > short_time + gevent.sleep(short_time) + TestTaskFinishWaitTime.state = "third" # should only run when run time + task_finish_wait_time is > short_time * 2 + + class MyTestLocust(Locust): + task_set = MyTaskSet + + self.options = mocked_options() + runner = LocalLocustRunner([MyTestLocust], self.options) + runner.start_hatching(1, 1) + gevent.sleep(short_time / 2) + runner.quit() + self.assertEqual("first", TestTaskFinishWaitTime.state) + + self.options.task_finish_wait_time = short_time / 2 # exit with timeout + runner = LocalLocustRunner([MyTestLocust], self.options) + runner.start_hatching(1, 1) + gevent.sleep(short_time) + runner.quit() + self.assertEqual("second", TestTaskFinishWaitTime.state) + + self.options.task_finish_wait_time = short_time * 2 # allow task iteration to complete, with some margin + runner = LocalLocustRunner([MyTestLocust], self.options) + runner.start_hatching(1, 1) + gevent.sleep(short_time) + runner.quit() + self.assertEqual("third", TestTaskFinishWaitTime.state) From 64bd55c5e659aed9f1206f20f83494df5b5b9a06 Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Wed, 2 Oct 2019 10:33:51 +0200 Subject: [PATCH 02/33] Nobody will ever be bothered to remember the long-form parameter name. --- locust/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/main.py b/locust/main.py index 95510087b6..ad07ad4ffb 100644 --- a/locust/main.py +++ b/locust/main.py @@ -293,7 +293,7 @@ def parse_options(): ) parser.add_option( - '--task-finish-wait-time', + '-w', '--task-finish-wait-time', action='store', type="int", dest='task_finish_wait_time', From 7a2d9d723b203d0ec13bb8d4873507ab14f54cc1 Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Sun, 22 Sep 2019 10:10:20 +0200 Subject: [PATCH 03/33] Allow None response time for requests --- locust/stats.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/locust/stats.py b/locust/stats.py index b49beaf3c9..7159d75709 100644 --- a/locust/stats.py +++ b/locust/stats.py @@ -52,6 +52,8 @@ def calculate_response_time_percentile(response_times, num_requests, percent): processed_count += response_times[response_time] if(num_requests - processed_count <= num_of_request): return response_time + # if all response times were None + return 0 def diff_response_time_dicts(latest, old): @@ -246,6 +248,8 @@ def _log_time_of_request(self, t): self.last_request_timestamp = t def _log_response_time(self, response_time): + if response_time is None: + return self.total_response_time += response_time From 1a2cb647b0363802e5b086f3896cd6a5a3d7879f Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Mon, 21 Oct 2019 09:25:33 +0200 Subject: [PATCH 04/33] Add test for events with None response time (failing at the moment) --- locust/test/test_runners.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 14ad50f910..cd6ad33459 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -159,6 +159,30 @@ class MyTestLocust(Locust): s = master.stats.get("/", "GET") self.assertEqual(700, s.median_response_time) + def test_slave_stats_report_with_none_response_times(self): + class MyTestLocust(Locust): + pass + + with mock.patch("locust.rpc.rpc.Server", mocked_rpc_server()) as server: + master = MasterLocustRunner(MyTestLocust, self.options) + server.mocked_send(Message("client_ready", None, "fake_client")) + + master.stats.get("/", "GET").log(100, 23455) + master.stats.get("/", "GET").log(800, 23455) + master.stats.get("/", "GET").log(700, 23455) + master.stats.get("/", "GET").log(None, 23455) + master.stats.get("/other", "GET").log(None, 23455) + + data = {"user_count":1} + events.report_to_master.fire(client_id="fake_client", data=data) + master.stats.clear_all() + + server.mocked_send(Message("stats", data, "fake_client")) + s = master.stats.get("/", "GET") + self.assertEqual(700, s.median_response_time) + s = master.stats.get("/other", "GET") + self.assertEqual(0, s.median_response_time) + def test_master_marks_downed_slaves_as_missing(self): class MyTestLocust(Locust): pass From cc40ce2ec6befafa21a03a48dc3a4b3a57b43166 Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Mon, 21 Oct 2019 11:10:53 +0200 Subject: [PATCH 05/33] Fix propagation of num_none_requests. Add test for median calculations and "only async". Add test for master total stats with none response times. --- locust/stats.py | 16 ++++++++-- locust/test/test_runners.py | 61 +++++++++++++++++++++++++++++++------ 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/locust/stats.py b/locust/stats.py index 7159d75709..e27076c366 100644 --- a/locust/stats.py +++ b/locust/stats.py @@ -83,6 +83,10 @@ def __init__(self): def num_requests(self): return self.total.num_requests + @property + def num_none_requests(self): + return self.total.num_none_requests + @property def num_failures(self): return self.total.num_failures @@ -157,6 +161,9 @@ class StatsEntry(object): num_requests = None """ The number of requests made """ + num_none_requests = None + """ The number of requests made with a None response time (typically async requests) """ + num_failures = None """ Number of failed request """ @@ -216,6 +223,7 @@ def __init__(self, stats, name, method, use_response_times_cache=False): def reset(self): self.start_time = time.time() self.num_requests = 0 + self.num_none_requests = 0 self.num_failures = 0 self.total_response_time = 0 self.response_times = {} @@ -249,6 +257,7 @@ def _log_time_of_request(self, t): def _log_response_time(self, response_time): if response_time is None: + self.num_none_requests += 1 return self.total_response_time += response_time @@ -291,7 +300,7 @@ def fail_ratio(self): @property def avg_response_time(self): try: - return float(self.total_response_time) / self.num_requests + return float(self.total_response_time) / (self.num_requests - self.num_none_requests) except ZeroDivisionError: return 0 @@ -299,7 +308,7 @@ def avg_response_time(self): def median_response_time(self): if not self.response_times: return 0 - median = median_from_dict(self.num_requests, self.response_times) + median = median_from_dict(self.num_requests - self.num_none_requests, self.response_times) or 0 # Since we only use two digits of precision when calculating the median response time # while still using the exact values for min and max response times, the following checks @@ -344,6 +353,7 @@ def extend(self, other): self.start_time = min(self.start_time, other.start_time) self.num_requests = self.num_requests + other.num_requests + self.num_none_requests = self.num_none_requests + other.num_none_requests self.num_failures = self.num_failures + other.num_failures self.total_response_time = self.total_response_time + other.total_response_time self.max_response_time = max(self.max_response_time, other.max_response_time) @@ -366,6 +376,7 @@ def serialize(self): "last_request_timestamp": self.last_request_timestamp, "start_time": self.start_time, "num_requests": self.num_requests, + "num_none_requests": self.num_none_requests, "num_failures": self.num_failures, "total_response_time": self.total_response_time, "max_response_time": self.max_response_time, @@ -382,6 +393,7 @@ def unserialize(cls, data): "last_request_timestamp", "start_time", "num_requests", + "num_none_requests", "num_failures", "total_response_time", "max_response_time", diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index cd6ad33459..ef70f7263d 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -167,21 +167,26 @@ class MyTestLocust(Locust): master = MasterLocustRunner(MyTestLocust, self.options) server.mocked_send(Message("client_ready", None, "fake_client")) - master.stats.get("/", "GET").log(100, 23455) - master.stats.get("/", "GET").log(800, 23455) - master.stats.get("/", "GET").log(700, 23455) - master.stats.get("/", "GET").log(None, 23455) - master.stats.get("/other", "GET").log(None, 23455) + master.stats.get("/mixed", "GET").log(0, 23455) + master.stats.get("/mixed", "GET").log(800, 23455) + master.stats.get("/mixed", "GET").log(700, 23455) + master.stats.get("/mixed", "GET").log(None, 23455) + master.stats.get("/mixed", "GET").log(None, 23455) + master.stats.get("/mixed", "GET").log(None, 23455) + master.stats.get("/mixed", "GET").log(None, 23455) + master.stats.get("/onlyNone", "GET").log(None, 23455) data = {"user_count":1} events.report_to_master.fire(client_id="fake_client", data=data) master.stats.clear_all() server.mocked_send(Message("stats", data, "fake_client")) - s = master.stats.get("/", "GET") - self.assertEqual(700, s.median_response_time) - s = master.stats.get("/other", "GET") - self.assertEqual(0, s.median_response_time) + s1 = master.stats.get("/mixed", "GET") + self.assertEqual(700, s1.median_response_time) + self.assertEqual(500, s1.avg_response_time) + s2 = master.stats.get("/onlyNone", "GET") + self.assertEqual(0, s2.median_response_time) + self.assertEqual(0, s2.avg_response_time) def test_master_marks_downed_slaves_as_missing(self): class MyTestLocust(Locust): @@ -219,7 +224,43 @@ class MyTestLocust(Locust): "user_count": 2, }, "fake_client")) self.assertEqual(700, master.stats.total.median_response_time) - + + def test_master_total_stats_with_none_response_times(self): + class MyTestLocust(Locust): + pass + + with mock.patch("locust.rpc.rpc.Server", mocked_rpc_server()) as server: + master = MasterLocustRunner(MyTestLocust, self.options) + server.mocked_send(Message("client_ready", None, "fake_client")) + stats = RequestStats() + stats.log_request("GET", "/1", 100, 3546) + stats.log_request("GET", "/1", 800, 56743) + stats.log_request("GET", "/1", None, 56743) + stats2 = RequestStats() + stats2.log_request("GET", "/2", 700, 2201) + stats2.log_request("GET", "/2", None, 2201) + stats3 = RequestStats() + stats3.log_request("GET", "/3", None, 2201) + server.mocked_send(Message("stats", { + "stats":stats.serialize_stats(), + "stats_total": stats.total.serialize(), + "errors":stats.serialize_errors(), + "user_count": 1, + }, "fake_client")) + server.mocked_send(Message("stats", { + "stats":stats2.serialize_stats(), + "stats_total": stats2.total.serialize(), + "errors":stats2.serialize_errors(), + "user_count": 2, + }, "fake_client")) + server.mocked_send(Message("stats", { + "stats":stats3.serialize_stats(), + "stats_total": stats3.total.serialize(), + "errors":stats3.serialize_errors(), + "user_count": 2, + }, "fake_client")) + self.assertEqual(700, master.stats.total.median_response_time) + def test_master_current_response_times(self): class MyTestLocust(Locust): pass From a1c59c4e9f6d2fc3e2e65ae5af552958ce1013ce Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Tue, 22 Oct 2019 17:13:01 +0200 Subject: [PATCH 06/33] argparse is a little stricter than optparse about parameters. --- locust/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/main.py b/locust/main.py index 302ad901fa..7abf454c6b 100644 --- a/locust/main.py +++ b/locust/main.py @@ -245,7 +245,7 @@ def parse_options(): parser.add_argument( '-w', '--task-finish-wait-time', action='store', - type="int", + type=int, dest='task_finish_wait_time', default=None, help="number of seconds to wait for a taskset to complete an iteration before exiting. default is to terminate immediately." From d6d87b4b3cb29b9cec1190bed7586f8b87bb492b Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 22 Oct 2019 17:45:22 +0200 Subject: [PATCH 07/33] Added note about low number of simulated users, or low hatch rate, together with high number of slave nodes --- docs/running-locust-distributed.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/running-locust-distributed.rst b/docs/running-locust-distributed.rst index 1299949e7f..c9071056d6 100644 --- a/docs/running-locust-distributed.rst +++ b/docs/running-locust-distributed.rst @@ -20,6 +20,16 @@ processor core, on the slave machines. Both the master and each slave machine, must have a copy of the locust test scripts when running Locust distributed. +.. note:: + It's recommended that you start a number of simulated users that are greater than + ``number of locust classes * number of slaves`` when running Locust distributed. + + Otherwise - due to the current implementation - + you might end up with a distribution of the Locust classes that doesn't correspond to the + Locust classes' ``weight`` attribute. And if the hatch rate is lower than the number of slave + nodes, the hatching would occur in "bursts" where all slave node would hatch a single user and + then sleep for multiple seconds, hatch another user, sleep and repeat. + Example ======= From 7492bb6fb6621c52ff0657bd8789f0aa634453f0 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 22 Oct 2019 23:46:14 +0200 Subject: [PATCH 08/33] Pin version of geventhttpclient-wheels Should hopefully help resolve #1116 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d74cb1ba38..2eb2412312 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ "msgpack-python>=0.4.2", "six>=1.10.0", "pyzmq>=16.0.2", - "geventhttpclient-wheels", + "geventhttpclient-wheels==1.3.1.dev2", ], test_suite="locust.test", tests_require=['mock'], From 7e0d71cdfaed1a36a04259c0a23af31c8fc487b5 Mon Sep 17 00:00:00 2001 From: Petr Date: Wed, 23 Oct 2019 09:55:00 -0400 Subject: [PATCH 09/33] Escape HTML entities in endpoint names #374 URL names (for stats) were not HTML-escaped in the dashboard. This made names with angle brackets disappear. For example: ``` self.client.get(url, name='/some-resource/upload/') ``` would show up as `/some-resource/upload/` instead of `/some-resource/upload/` which is confusing. I added new key to /stats/request - "safe_name" - so that escaping is on the server side and javascript has less logic. --- locust/templates/index.html | 2 +- locust/test/test_web.py | 5 +++-- locust/web.py | 2 ++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/locust/templates/index.html b/locust/templates/index.html index 463bc9c387..56113cbd11 100644 --- a/locust/templates/index.html +++ b/locust/templates/index.html @@ -219,7 +219,7 @@

Version <%=(this.name == "Total" ? "total" : "")%>"> <%= (this.method ? this.method : "") %> - <%= this.name %> + <%= this.safe_name %> <%= this.num_requests %> <%= this.num_failures %> <%= Math.round(this.median_response_time) %> diff --git a/locust/test/test_web.py b/locust/test/test_web.py index 51ab9a8c42..cc649c61fe 100644 --- a/locust/test/test_web.py +++ b/locust/test/test_web.py @@ -43,13 +43,14 @@ def test_stats_no_data(self): self.assertEqual(200, requests.get("http://127.0.0.1:%i/stats/requests" % self.web_port).status_code) def test_stats(self): - stats.global_stats.log_request("GET", "/test", 120, 5612) + stats.global_stats.log_request("GET", "/", 120, 5612) response = requests.get("http://127.0.0.1:%i/stats/requests" % self.web_port) self.assertEqual(200, response.status_code) data = json.loads(response.text) self.assertEqual(2, len(data["stats"])) # one entry plus Total - self.assertEqual("/test", data["stats"][0]["name"]) + self.assertEqual("/", data["stats"][0]["name"]) + self.assertEqual("/<html>", data["stats"][0]["safe_name"]) self.assertEqual("GET", data["stats"][0]["method"]) self.assertEqual(120, data["stats"][0]["avg_response_time"]) diff --git a/locust/web.py b/locust/web.py index 27a1bac512..248ee4759e 100644 --- a/locust/web.py +++ b/locust/web.py @@ -7,6 +7,7 @@ from collections import defaultdict from itertools import chain from time import time +import html import six from flask import Flask, make_response, jsonify, render_template, request @@ -109,6 +110,7 @@ def request_stats(): stats.append({ "method": s.method, "name": s.name, + "safe_name": html.escape(s.name), "num_requests": s.num_requests, "num_failures": s.num_failures, "avg_response_time": s.avg_response_time, From 639ddd2a6e1ec4a3c30ef48694694fd3befd3182 Mon Sep 17 00:00:00 2001 From: Petr Date: Wed, 23 Oct 2019 12:52:46 -0400 Subject: [PATCH 10/33] fix Python 2 compatibility Actually cgi.escape is still available in Python3.7, but is deprecated. So going forward, it's safer to import escape from html. I disabled escaping quotes (quote=False) for consistency and because it's not needed for this purpose. --- locust/web.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/locust/web.py b/locust/web.py index 248ee4759e..955bbec837 100644 --- a/locust/web.py +++ b/locust/web.py @@ -7,7 +7,13 @@ from collections import defaultdict from itertools import chain from time import time -import html + +try: + # >= Py3.2 + from html import escape +except ImportError: + # < Py3.2 + from cgi import escape import six from flask import Flask, make_response, jsonify, render_template, request @@ -105,12 +111,12 @@ def failures_stats_csv(): @memoize(timeout=DEFAULT_CACHE_TIME, dynamic_timeout=True) def request_stats(): stats = [] - + for s in chain(sort_stats(runners.locust_runner.request_stats), [runners.locust_runner.stats.total]): stats.append({ "method": s.method, "name": s.name, - "safe_name": html.escape(s.name), + "safe_name": escape(s.name, quote=False), "num_requests": s.num_requests, "num_failures": s.num_failures, "avg_response_time": s.avg_response_time, From a664d7ad71dacc296d2378adb341237299fa9769 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Thu, 24 Oct 2019 11:19:35 +0200 Subject: [PATCH 11/33] =?UTF-8?q?Rename=20the=20label=20of=20the=20last=20?= =?UTF-8?q?=E2=80=9CTotal=E2=80=9D=20row=20to=20=E2=80=9CAggregated?= =?UTF-8?q?=E2=80=9D=20since=20it=20isn=E2=80=99t=20actually=20a=20sum=20o?= =?UTF-8?q?f=20the=20individual=20rows.=20Fixes=20#629.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog.rst | 7 +++++++ locust/static/locust.js | 1 + locust/stats.py | 6 +++--- locust/templates/index.html | 2 +- locust/test/test_web.py | 8 ++++---- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7501935ce5..bd42f48cc7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,13 @@ Changelog Highlights #################### +Unreleased +========== + +* Renamed the last row in statistics from "Total" to "Aggregated" (since the values aren't a sum of the + individual table rows). + + For full details of the Locust changelog, please see https://github.com/locustio/locust/blob/master/CHANGELOG.md 0.12.1 diff --git a/locust/static/locust.js b/locust/static/locust.js index a78b8cc368..5a51f1f938 100644 --- a/locust/static/locust.js +++ b/locust/static/locust.js @@ -103,6 +103,7 @@ var report; function renderTable(report) { var totalRow = report.stats.pop(); + totalRow.is_aggregated = true; var sortedStats = (report.stats).sort(sortBy(sortAttribute, desc)); sortedStats.push(totalRow); $('#stats tbody').empty(); diff --git a/locust/stats.py b/locust/stats.py index b49beaf3c9..644f3cb8a0 100644 --- a/locust/stats.py +++ b/locust/stats.py @@ -74,7 +74,7 @@ class RequestStats(object): def __init__(self): self.entries = {} self.errors = {} - self.total = StatsEntry(self, "Total", None, use_response_times_cache=True) + self.total = StatsEntry(self, "Aggregated", None, use_response_times_cache=True) self.start_time = None @property @@ -129,7 +129,7 @@ def clear_all(self): """ Remove all stats entries and errors """ - self.total = StatsEntry(self, "Total", None, use_response_times_cache=True) + self.total = StatsEntry(self, "Aggregated", None, use_response_times_cache=True) self.entries = {} self.errors = {} self.start_time = None @@ -633,7 +633,7 @@ def print_stats(stats): except ZeroDivisionError: fail_percent = 0 - console_logger.info((" %-" + str(STATS_NAME_WIDTH) + "s %7d %12s %42.2f") % ('Total', total_reqs, "%d(%.2f%%)" % (total_failures, fail_percent), total_rps)) + console_logger.info((" %-" + str(STATS_NAME_WIDTH) + "s %7d %12s %42.2f") % ('Aggregated', total_reqs, "%d(%.2f%%)" % (total_failures, fail_percent), total_rps)) console_logger.info("") def print_percentile_stats(stats): diff --git a/locust/templates/index.html b/locust/templates/index.html index 56113cbd11..a249169d5f 100644 --- a/locust/templates/index.html +++ b/locust/templates/index.html @@ -217,7 +217,7 @@

Version