From 6bdc65a5ed64cd18e09746ee13a834e4aea42e37 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Wed, 21 Sep 2011 18:15:04 +0200 Subject: [PATCH 01/39] Added a collection.deque current_response_times and function current_percentile in stats.py to be used in the start_ramp function in core.py. --- locust/core.py | 89 ++++++++++++++++++++++++++++++++----------------- locust/stats.py | 16 ++++++++- 2 files changed, 73 insertions(+), 32 deletions(-) diff --git a/locust/core.py b/locust/core.py index 3c6c24600f..2e698288dd 100644 --- a/locust/core.py +++ b/locust/core.py @@ -543,57 +543,84 @@ def start_ramping(self, hatch_rate=None, max_locusts=1000, hatch_stride=None, pe clients = hatch_stride + # Record low load percentile def calibrate(): self.start_hatching(clients, self.hatch_rate) while True: if self.state != STATE_HATCHING: print "recording low_percentile..." - gevent.sleep(30) - percentile = RequestStats.sum_stats().one_percentile(percent) + gevent.sleep(15) + percentile = RequestStats.sum_stats().current_percentile(percent) + #percentile = RequestStats.sum_stats().one_percentile(percent) + #percentile = RequestStats.sum_stats().median_response_time() print "low_percentile:", percentile self.start_hatching(1, self.hatch_rate) return percentile gevent.sleep(1) - low_percentile = calibrate() + def ramp_up(clients, hatch_stride): + while True: + if self.state != STATE_HATCHING: + if self.num_clients >= max_locusts: + print "ramping stopped due to max_locusts limit reached:", max_locusts + return ramp_down(clients, hatch_stride) + gevent.sleep(10) + if RequestStats.sum_stats().fail_ratio >= acceptable_fail: + print "ramping stopped due to acceptable_fail ratio (%d1.2%%) exceeded with fail ratio %1.2d%%", (acceptable_fail*100, RequestStats.sum_stats().fail_ratio*100) + return ramp_down(clients, hatch_stride) + p = RequestStats.sum_stats().current_percentile(percent) + print "percentile:", p + if p >= low_percentile * 3.0: + print "ramping stopped due to response times getting high:", p + return ramp_down(clients, hatch_rate) + hatch_stride += hatch_stride + clients += hatch_stride + self.start_hatching(clients, self.hatch_rate) + gevent.sleep(1) - while True: - if self.state != STATE_HATCHING: - if self.num_clients >= max_locusts: - print "ramping stopped due to max_locusts limit reached:", max_locusts - return - gevent.sleep(10) - if RequestStats.sum_stats().fail_ratio >= acceptable_fail: - print "ramping stopped due to acceptable_fail ratio (%d1.2%%) exceeded with fail ratio %1.2d%%", (acceptable_fail*100, RequestStats.sum_stats().fail_ratio*100) - return - p = RequestStats.sum_stats().one_percentile(percent) - if p >= low_percentile * 2.0: - print "ramping stopped due to response times getting high:", p - return - self.start_hatching(clients, self.hatch_rate) - clients += hatch_stride - gevent.sleep(1) + def ramp_down(clients, hatch_stride): + while True: + if self.state != STATE_HATCHING: + print "bab!" + gevent.sleep(10) + fails = RequestStats.sum_stats().fail_ratio + if fails >= acceptable_fail: + p = RequestStats.sum_stats().current_percentile(percent) + print "percentile:", p + if p <= low_percentile * 3.0: + if hatch_stride < 200: + print "sweet spot found, ramping stopped!" + return + hatch_stride -= hatch_stride/2 + clients -= hatch_stride + self.start_hatching(clients, self.hatch_rate) + else: + return ramp_up(clients, hatch_stride) + else: + return ramp_up(clients, hatch_stride) + gevent.sleep(1) + + low_percentile = calibrate() + ramp_up(clients, hatch_stride) # while True: # if self.state != STATE_HATCHING: -# print "self.num_clients: %i max_locusts: %i" % (self.num_clients, max_locusts) # if self.num_clients >= max_locusts: # print "ramping stopped due to max_locusts limit reached:", max_locusts # return -# gevent.sleep(5) -# if self.state != STATE_INIT: -# print "num_reqs: %i fail_ratio: %1.2d" % (RequestStats.sum_stats().num_reqs, RequestStats.sum_stats().fail_ratio) -# while RequestStats.sum_stats().num_reqs < 100: -# if RequestStats.sum_stats().fail_ratio >= acceptable_fail: -# print "ramping stopped due to acceptable_fail ratio (%d1.2%%) exceeded with fail ratio %1.2d%%", (acceptable_fail*100, RequestStats.sum_stats().fail_ratio*100) -# return -# gevent.sleep(1) -# if RequestStats.sum_stats().one_percentile(percent) >= response_time: -# print "ramping stopped due to response times over %ims for %1.2f%%" % (response_time, percent*100) +# gevent.sleep(10) +# if RequestStats.sum_stats().fail_ratio >= acceptable_fail: +# print "ramping stopped due to acceptable_fail ratio (%d1.2%%) exceeded with fail ratio %1.2d%%", (acceptable_fail*100, RequestStats.sum_stats().fail_ratio*100) +# return +# p = RequestStats.sum_stats().current_percentile(percent) +# print "percentile:", p +# if p >= low_percentile * 3.0: +# print "ramping stopped due to response times getting high:", p # return # self.start_hatching(clients, self.hatch_rate) -# clients += 10 * hatchrate +# clients += hatch_stride +# hatch_stride += hatch_stride # gevent.sleep(1) def stop(self): diff --git a/locust/stats.py b/locust/stats.py index 7dc3b32003..4272ff227f 100644 --- a/locust/stats.py +++ b/locust/stats.py @@ -2,6 +2,7 @@ import gevent from copy import copy import math +from collections import deque from exception import InterruptLocust import events @@ -54,6 +55,7 @@ def reset(self): self.last_request_timestamp = int(time.time()) self.num_reqs_per_sec = {} self.total_content_length = 0 + self.current_response_times = deque([]) def log(self, response_time, content_length): RequestStats.total_num_requests += 1 @@ -87,6 +89,7 @@ def log(self, response_time, content_length): self.response_times.setdefault(rounded_response_time, 0) self.response_times[rounded_response_time] += 1 + self.current_response_times.append(response_time) # increase total content-length self.total_content_length += content_length @@ -162,7 +165,15 @@ def iadd_stats(self, other, full_request_history=False): self.max_response_time = max(self.max_response_time, other.max_response_time) self._min_response_time = min(self._min_response_time, other._min_response_time) or other._min_response_time self.total_content_length = self.total_content_length + other.total_content_length - + + #print "current_rps: %d last_timestamp: %d" % (self.current_rps, self.last_request_timestamp) + #if self.current_rps and self.current_response_times: + self.current_response_times.extend(other.current_response_times) + if len(self.current_response_times) > 100: + num_pop = len(self.current_response_times)-100 + for i in range(num_pop): + self.current_response_times.popleft() + if full_request_history: for key in other.response_times: self.response_times[key] = self.response_times.get(key, 0) + other.response_times[key] @@ -214,6 +225,9 @@ def create_response_times_list(self): return inflated_list + def current_percentile(self, percent): + return percentile(sorted(self.current_response_times), percent) + def percentile(self, tpl=" %-" + str(STATS_NAME_WIDTH) + "s %8d %6d %6d %6d %6d %6d %6d %6d %6d %6d"): inflated_list = self.create_response_times_list() return tpl % ( From 376f663df73345fd5fe00a0ab3a86e3af0df04a5 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Thu, 22 Sep 2011 00:16:39 +0200 Subject: [PATCH 02/39] fixed ramp_down in start_ramp, not yet tested --- locust/core.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/locust/core.py b/locust/core.py index 2e698288dd..256824d0e7 100644 --- a/locust/core.py +++ b/locust/core.py @@ -585,20 +585,17 @@ def ramp_down(clients, hatch_stride): print "bab!" gevent.sleep(10) fails = RequestStats.sum_stats().fail_ratio - if fails >= acceptable_fail: + if fails <= acceptable_fail: p = RequestStats.sum_stats().current_percentile(percent) print "percentile:", p if p <= low_percentile * 3.0: if hatch_stride < 200: print "sweet spot found, ramping stopped!" return - hatch_stride -= hatch_stride/2 - clients -= hatch_stride - self.start_hatching(clients, self.hatch_rate) - else: return ramp_up(clients, hatch_stride) - else: - return ramp_up(clients, hatch_stride) + hatch_stride -= hatch_stride/2 + clients -= hatch_stride + self.start_hatching(clients, hatch_stride) gevent.sleep(1) low_percentile = calibrate() From d0b69184cf05aaec8e529886f26bdb80fb9a5826 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Thu, 22 Sep 2011 13:46:40 +0200 Subject: [PATCH 03/39] Reworked ramp_down function in start_ramp, also removed old out commented start_ramp code start_ramp is now woking; it's ramping up exceeding a boundery then gradually getting closer, splitting the max distance in half til "hatch_stride" is lower than "precision" --- locust/core.py | 84 +++++++++++++++++++++----------------------------- 1 file changed, 35 insertions(+), 49 deletions(-) diff --git a/locust/core.py b/locust/core.py index 256824d0e7..15c7267bdb 100644 --- a/locust/core.py +++ b/locust/core.py @@ -534,15 +534,13 @@ def start_hatching(self, locust_count, hatch_rate): RequestStats.global_start_time = time() self.state = STATE_HATCHING - def start_ramping(self, hatch_rate=None, max_locusts=1000, hatch_stride=None, percent=0.95, response_time=2000, acceptable_fail=0.05): + def start_ramping(self, hatch_rate=None, max_locusts=1000, hatch_stride=100, + percent=0.95, response_time_limit=2000, acceptable_fail=0.05, + precision=200, calibrate_rt_limit=False): if hatch_rate: self.hatch_rate = hatch_rate - if not hatch_stride: - hatch_stride = 100 - - clients = hatch_stride - + clients = 0 # Record low load percentile def calibrate(): @@ -552,74 +550,62 @@ def calibrate(): print "recording low_percentile..." gevent.sleep(15) percentile = RequestStats.sum_stats().current_percentile(percent) - #percentile = RequestStats.sum_stats().one_percentile(percent) - #percentile = RequestStats.sum_stats().median_response_time() print "low_percentile:", percentile self.start_hatching(1, self.hatch_rate) - return percentile + return percentile*3 gevent.sleep(1) - def ramp_up(clients, hatch_stride): + def ramp_up(clients, hatch_stride, boundery_found=False): while True: if self.state != STATE_HATCHING: + print "ramping up..." if self.num_clients >= max_locusts: - print "ramping stopped due to max_locusts limit reached:", max_locusts + print "ramp up stopped due to max_locusts limit reached:", max_locusts + if not boundery_found: + hatch_stride = hatch_stride/2 return ramp_down(clients, hatch_stride) gevent.sleep(10) if RequestStats.sum_stats().fail_ratio >= acceptable_fail: - print "ramping stopped due to acceptable_fail ratio (%d1.2%%) exceeded with fail ratio %1.2d%%", (acceptable_fail*100, RequestStats.sum_stats().fail_ratio*100) + print "ramp up stopped due to acceptable_fail ratio (%d1.2%%) exceeded with fail ratio %1.2d%%", (acceptable_fail*100, RequestStats.sum_stats().fail_ratio*100) + if not boundery_found: + hatch_stride = hatch_stride/2 return ramp_down(clients, hatch_stride) p = RequestStats.sum_stats().current_percentile(percent) - print "percentile:", p - if p >= low_percentile * 3.0: - print "ramping stopped due to response times getting high:", p - return ramp_down(clients, hatch_rate) - hatch_stride += hatch_stride + if p >= response_time_limit: + print "ramp up stopped due to response times getting high:", p + if not boundery_found: + hatch_stride = hatch_stride/2 + return ramp_down(clients, hatch_stride) clients += hatch_stride + if not boundery_found: + hatch_stride += hatch_stride self.start_hatching(clients, self.hatch_rate) gevent.sleep(1) def ramp_down(clients, hatch_stride): while True: if self.state != STATE_HATCHING: - print "bab!" - gevent.sleep(10) - fails = RequestStats.sum_stats().fail_ratio - if fails <= acceptable_fail: - p = RequestStats.sum_stats().current_percentile(percent) - print "percentile:", p - if p <= low_percentile * 3.0: - if hatch_stride < 200: - print "sweet spot found, ramping stopped!" - return - return ramp_up(clients, hatch_stride) - hatch_stride -= hatch_stride/2 + print "ramping down..." + if self.num_clients < max_locusts: + gevent.sleep(10) + fails = RequestStats.sum_stats().fail_ratio + if fails <= acceptable_fail: + p = RequestStats.sum_stats().current_percentile(percent) + if p <= response_time_limit: + if hatch_stride <= precision: + print "sweet spot found, ramping stopped!" + return + hatch_stride = hatch_stride/2 + return ramp_up(clients, hatch_stride, True) + hatch_stride = hatch_stride/2 clients -= hatch_stride self.start_hatching(clients, hatch_stride) gevent.sleep(1) - low_percentile = calibrate() + if calibrate_rt_limit: + response_time_limit = calibrate() ramp_up(clients, hatch_stride) -# while True: -# if self.state != STATE_HATCHING: -# if self.num_clients >= max_locusts: -# print "ramping stopped due to max_locusts limit reached:", max_locusts -# return -# gevent.sleep(10) -# if RequestStats.sum_stats().fail_ratio >= acceptable_fail: -# print "ramping stopped due to acceptable_fail ratio (%d1.2%%) exceeded with fail ratio %1.2d%%", (acceptable_fail*100, RequestStats.sum_stats().fail_ratio*100) -# return -# p = RequestStats.sum_stats().current_percentile(percent) -# print "percentile:", p -# if p >= low_percentile * 3.0: -# print "ramping stopped due to response times getting high:", p -# return -# self.start_hatching(clients, self.hatch_rate) -# clients += hatch_stride -# hatch_stride += hatch_stride -# gevent.sleep(1) - def stop(self): for client in self.clients.hatching + self.clients.running: self.server.send({"type":"stop", "data":{}}) From 9b2d70f3f51234e5f124a230240dad9eb90e2f0d Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Fri, 23 Sep 2011 15:03:56 +0200 Subject: [PATCH 04/39] Added the ability to sort by column for all columns in the Statistics tab and Failures tab. The sorting is not rendered until the next updateStats call though (so once every 2 seconds) --- locust/templates/index.html | 102 +++++++++++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 14 deletions(-) diff --git a/locust/templates/index.html b/locust/templates/index.html index c02a94fc4d..97cbf82e68 100644 --- a/locust/templates/index.html +++ b/locust/templates/index.html @@ -91,15 +91,15 @@

Change the locust count

- - - - - - - - - + + + + + + + + + @@ -113,8 +113,8 @@

Change the locust count

Name# requests# failsMedianAverageMinMaxContent-Length# reqs/secName# requests# failsMedianAverageMinMaxContent-Length# reqs/sec
- - + + @@ -211,6 +211,61 @@

Version

event.preventDefault(); $(this).parent().parent().hide(); }); + + $("#sort_by_name").click(function(event) { + event.preventDefault(); + set_sort_attribute("name") + }); + $("#sort_by_reqs").click(function(event) { + event.preventDefault(); + set_sort_attribute("num_reqs") + }); + $("#sort_by_fails").click(function(event) { + event.preventDefault(); + set_sort_attribute("num_failures") + }); + $("#sort_by_median").click(function(event) { + event.preventDefault(); + set_sort_attribute("median_response_time") + }); + $("#sort_by_avg").click(function(event) { + event.preventDefault(); + set_sort_attribute("avg_response_time") + }); + $("#sort_by_min").click(function(event) { + event.preventDefault(); + event.preventDefault(); + set_sort_attribute("min_response_time") + }); + $("#sort_by_max").click(function(event) { + event.preventDefault(); + set_sort_attribute("max_response_time") + }); + $("#sort_by_content_length").click(function(event) { + event.preventDefault(); + set_sort_attribute("avg_content_length") + }); + $("#sort_by_rps").click(function(event) { + event.preventDefault(); + set_sort_attribute("current_rps") + }); + + $("#sort_by_fail_count").click(function(event) { + event.preventDefault(); + set_sort_attribute("1") + }); + $("#sort_by_fail_type").click(function(event) { + event.preventDefault(); + set_sort_attribute("0") + }); + + function set_sort_attribute(attribute) { + sort_attribute = attribute + if (desc == true) + desc = false; + else + desc = true; + } var alternate = false; @@ -247,7 +302,25 @@

Version

} ); }); - + + var sort_by = function(field, reverse, primer){ + reverse = (reverse) ? -1 : 1; + return function(a,b){ + a = a[field]; + b = b[field]; + if (typeof(primer) != 'undefined'){ + a = primer(a); + b = primer(b); + } + if (ab) return reverse * 1; + return 0; + } + } + + var sort_attribute = "name"; + var desc = false; + function updateStats() { $.get('/stats/requests', function (data) { var report = JSON.parse(data); @@ -263,9 +336,10 @@

Version

$('#errors tbody').empty(); alternate = false; - $('#stats tbody').jqoteapp(stats_tpl, report.stats); + //$('#stats tbody').jqoteapp(stats_tpl, (report.stats).sort(sort_by(sort_attribute, desc))); + $('#stats tbody').jqoteapp(stats_tpl, (report.stats).sort(sort_by(sort_attribute, desc))); alternate = false; - $('#errors tbody').jqoteapp(errors_tpl, report.errors); + $('#errors tbody').jqoteapp(errors_tpl, (report.errors).sort(sort_by(sort_attribute, desc))); setTimeout(updateStats, 2000); }); } From 20a1c9b6079a16fdfa52a7344e9b95ca62d5fc0e Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Mon, 3 Oct 2011 17:25:28 +0200 Subject: [PATCH 05/39] Created autotune.py "current_percentile" calculates the response time percentile for the requests in a time window. It is implemented to work in both distributed and local mode. autotune.py listens to the events on_slave_report, on_report_to_master and on_request_success and handles a deque of the latest response times TIME_WINDOW seconds back in time. --- locust/autotune.py | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 locust/autotune.py diff --git a/locust/autotune.py b/locust/autotune.py new file mode 100644 index 0000000000..907fac3f40 --- /dev/null +++ b/locust/autotune.py @@ -0,0 +1,49 @@ +from stats import percentile, RequestStats +from collections import deque +import events + +master_response_times = deque([]) +slave_response_times = [] + +# The time window in seconds that current_percentile use data from +TIME_WINDOW = 15.0 + +def current_percentile(percent): + from core import locust_runner, MasterLocustRunner + if isinstance(locust_runner, MasterLocustRunner): + return percentile(sorted([item for sublist in master_response_times for item in sublist]), percent) + else: + return percentile(sorted(master_response_times), percent) + +def on_request_success(_, response_time, _2): + from core import locust_runner, MasterLocustRunner + if isinstance(locust_runner, MasterLocustRunner): + slave_response_times.append(response_time) + else: + # The locust_runner is not running in distributed mode + master_response_times.append(response_time) + + # remove from the queue + rps = RequestStats.sum_stats().current_rps + if len(master_response_times) > rps*TIME_WINDOW: + for i in xrange(len(master_response_times) - rps*TIME_WINDOW): + master_response_times.popleft() + +def on_report_to_master(_, data): + data["current_responses"] = slave_response_times + global slave_response_times + slave_response_times = [] + +def on_slave_report(_, data): + from core import locust_runner, SLAVE_REPORT_INTERVAL + master_response_times.append(data["current_responses"]) + + # remove from the queue + slaves = locust_runner.slave_count + response_times_per_slave_count = TIME_WINDOW/SLAVE_REPORT_INTERVAL + if len(master_response_times) > slaves * response_times_per_slave_count: + master_response_times.popleft() + +events.request_success += on_request_success +events.report_to_master += on_report_to_master +events.slave_report += on_slave_report \ No newline at end of file From db10e4d99eb4ebe5cecb1b843706e0d85ff8c633 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Mon, 3 Oct 2011 17:41:21 +0200 Subject: [PATCH 06/39] start_ramping now uses the current_percentile-function from autotune.py Added variable reset to start_ramping, if reset is set to true all currently spawned clients will be killed Added variable start_count; start_count is the number of clients that will be spawned before starting the ramping --- locust/core.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/locust/core.py b/locust/core.py index 15c7267bdb..2cdf0fb044 100644 --- a/locust/core.py +++ b/locust/core.py @@ -12,6 +12,7 @@ import traceback from hashlib import md5 +from autotune import current_percentile from locust.stats import print_percentile_stats from clients import HttpBrowser from stats import RequestStats, print_stats @@ -536,11 +537,15 @@ def start_hatching(self, locust_count, hatch_rate): def start_ramping(self, hatch_rate=None, max_locusts=1000, hatch_stride=100, percent=0.95, response_time_limit=2000, acceptable_fail=0.05, - precision=200, calibrate_rt_limit=False): + precision=200, calibrate_rt_limit=False, reset=False, start_count=0): if hatch_rate: self.hatch_rate = hatch_rate - clients = 0 + if reset: + clients = 0 + else: + clients = self.num_clients + # Record low load percentile def calibrate(): @@ -549,9 +554,9 @@ def calibrate(): if self.state != STATE_HATCHING: print "recording low_percentile..." gevent.sleep(15) - percentile = RequestStats.sum_stats().current_percentile(percent) + percentile = current_percentile(percent) print "low_percentile:", percentile - self.start_hatching(1, self.hatch_rate) + self.start_hatching(0, self.hatch_rate) return percentile*3 gevent.sleep(1) @@ -570,7 +575,7 @@ def ramp_up(clients, hatch_stride, boundery_found=False): if not boundery_found: hatch_stride = hatch_stride/2 return ramp_down(clients, hatch_stride) - p = RequestStats.sum_stats().current_percentile(percent) + p = current_percentile(percent) if p >= response_time_limit: print "ramp up stopped due to response times getting high:", p if not boundery_found: @@ -590,7 +595,7 @@ def ramp_down(clients, hatch_stride): gevent.sleep(10) fails = RequestStats.sum_stats().fail_ratio if fails <= acceptable_fail: - p = RequestStats.sum_stats().current_percentile(percent) + p = current_percentile(percent) if p <= response_time_limit: if hatch_stride <= precision: print "sweet spot found, ramping stopped!" @@ -599,11 +604,16 @@ def ramp_down(clients, hatch_stride): return ramp_up(clients, hatch_stride, True) hatch_stride = hatch_stride/2 clients -= hatch_stride - self.start_hatching(clients, hatch_stride) + self.start_hatching(clients, self.hatch_rate) gevent.sleep(1) + if reset: + self.start_hatching(0,self.hatch_rate) if calibrate_rt_limit: response_time_limit = calibrate() + if start_count: + if start_coun > self.num_clients: + self.start_hatching(start_count, hatch_stride) ramp_up(clients, hatch_stride) def stop(self): From c17a67fd95091a20f51823b85b3a688a93433e87 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Mon, 3 Oct 2011 17:44:05 +0200 Subject: [PATCH 07/39] Removed the use of self.current_response_times and removed the function current_percentile since all this functionality now is in autotune.py --- locust/stats.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/locust/stats.py b/locust/stats.py index 4272ff227f..a5ddc949b7 100644 --- a/locust/stats.py +++ b/locust/stats.py @@ -2,7 +2,6 @@ import gevent from copy import copy import math -from collections import deque from exception import InterruptLocust import events @@ -55,7 +54,6 @@ def reset(self): self.last_request_timestamp = int(time.time()) self.num_reqs_per_sec = {} self.total_content_length = 0 - self.current_response_times = deque([]) def log(self, response_time, content_length): RequestStats.total_num_requests += 1 @@ -89,7 +87,6 @@ def log(self, response_time, content_length): self.response_times.setdefault(rounded_response_time, 0) self.response_times[rounded_response_time] += 1 - self.current_response_times.append(response_time) # increase total content-length self.total_content_length += content_length @@ -166,14 +163,6 @@ def iadd_stats(self, other, full_request_history=False): self._min_response_time = min(self._min_response_time, other._min_response_time) or other._min_response_time self.total_content_length = self.total_content_length + other.total_content_length - #print "current_rps: %d last_timestamp: %d" % (self.current_rps, self.last_request_timestamp) - #if self.current_rps and self.current_response_times: - self.current_response_times.extend(other.current_response_times) - if len(self.current_response_times) > 100: - num_pop = len(self.current_response_times)-100 - for i in range(num_pop): - self.current_response_times.popleft() - if full_request_history: for key in other.response_times: self.response_times[key] = self.response_times.get(key, 0) + other.response_times[key] @@ -225,9 +214,6 @@ def create_response_times_list(self): return inflated_list - def current_percentile(self, percent): - return percentile(sorted(self.current_response_times), percent) - def percentile(self, tpl=" %-" + str(STATS_NAME_WIDTH) + "s %8d %6d %6d %6d %6d %6d %6d %6d %6d %6d"): inflated_list = self.create_response_times_list() return tpl % ( From 84b4e9fd5de88c2de9702c25cf11a8652e7da68e Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Mon, 3 Oct 2011 18:32:05 +0200 Subject: [PATCH 08/39] removed redundant code for sorting the statistics and failure columns removed tag element on the labels and use the
# failsType# failsType
instead --- locust/templates/index.html | 93 +++++++++---------------------------- 1 file changed, 22 insertions(+), 71 deletions(-) diff --git a/locust/templates/index.html b/locust/templates/index.html index 97cbf82e68..4626b71dc7 100644 --- a/locust/templates/index.html +++ b/locust/templates/index.html @@ -91,15 +91,15 @@

Change the locust count

- - - - - - - - - + + + + + + + + + @@ -113,8 +113,8 @@

Change the locust count

Name# requests# failsMedianAverageMinMaxContent-Length# reqs/secName# requests# failsMedianAverageMinMaxContent-Length# reqs/sec
- - + + @@ -211,61 +211,6 @@

Version

event.preventDefault(); $(this).parent().parent().hide(); }); - - $("#sort_by_name").click(function(event) { - event.preventDefault(); - set_sort_attribute("name") - }); - $("#sort_by_reqs").click(function(event) { - event.preventDefault(); - set_sort_attribute("num_reqs") - }); - $("#sort_by_fails").click(function(event) { - event.preventDefault(); - set_sort_attribute("num_failures") - }); - $("#sort_by_median").click(function(event) { - event.preventDefault(); - set_sort_attribute("median_response_time") - }); - $("#sort_by_avg").click(function(event) { - event.preventDefault(); - set_sort_attribute("avg_response_time") - }); - $("#sort_by_min").click(function(event) { - event.preventDefault(); - event.preventDefault(); - set_sort_attribute("min_response_time") - }); - $("#sort_by_max").click(function(event) { - event.preventDefault(); - set_sort_attribute("max_response_time") - }); - $("#sort_by_content_length").click(function(event) { - event.preventDefault(); - set_sort_attribute("avg_content_length") - }); - $("#sort_by_rps").click(function(event) { - event.preventDefault(); - set_sort_attribute("current_rps") - }); - - $("#sort_by_fail_count").click(function(event) { - event.preventDefault(); - set_sort_attribute("1") - }); - $("#sort_by_fail_type").click(function(event) { - event.preventDefault(); - set_sort_attribute("0") - }); - - function set_sort_attribute(attribute) { - sort_attribute = attribute - if (desc == true) - desc = false; - else - desc = true; - } var alternate = false; @@ -303,7 +248,7 @@

Version

); }); - var sort_by = function(field, reverse, primer){ + var sortBy = function(field, reverse, primer){ reverse = (reverse) ? -1 : 1; return function(a,b){ a = a[field]; @@ -318,9 +263,16 @@

Version

} } - var sort_attribute = "name"; + var sortAttribute = "name"; var desc = false; + $(".stats_label").click(function(event) { + event.preventDefault(); + sortAttribute = $(this).attr("data-sortkey"); + desc = !desc; + console.log("sortAttribute:", sortAttribute) + }); + function updateStats() { $.get('/stats/requests', function (data) { var report = JSON.parse(data); @@ -336,10 +288,9 @@

Version

$('#errors tbody').empty(); alternate = false; - //$('#stats tbody').jqoteapp(stats_tpl, (report.stats).sort(sort_by(sort_attribute, desc))); - $('#stats tbody').jqoteapp(stats_tpl, (report.stats).sort(sort_by(sort_attribute, desc))); + $('#stats tbody').jqoteapp(stats_tpl, (report.stats).sort(sortBy(sortAttribute, desc))); alternate = false; - $('#errors tbody').jqoteapp(errors_tpl, (report.errors).sort(sort_by(sort_attribute, desc))); + $('#errors tbody').jqoteapp(errors_tpl, (report.errors).sort(sortBy(sortAttribute, desc))); setTimeout(updateStats, 2000); }); } From 2f935c379a73020712f2d008ed02f77f5147873c Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Tue, 4 Oct 2011 14:20:32 +0200 Subject: [PATCH 09/39] Fixed so that the row with Total in the stats is always in the bottom when sorting by columns --- locust/templates/index.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/locust/templates/index.html b/locust/templates/index.html index 4626b71dc7..df41d4aa29 100644 --- a/locust/templates/index.html +++ b/locust/templates/index.html @@ -288,7 +288,11 @@

Version

$('#errors tbody').empty(); alternate = false; - $('#stats tbody').jqoteapp(stats_tpl, (report.stats).sort(sortBy(sortAttribute, desc))); + + totalRow = report.stats.pop() + sortedStats = (report.stats).sort(sortBy(sortAttribute, desc)) + sortedStats.push(totalRow) + $('#stats tbody').jqoteapp(stats_tpl, sortedStats); alternate = false; $('#errors tbody').jqoteapp(errors_tpl, (report.errors).sort(sortBy(sortAttribute, desc))); setTimeout(updateStats, 2000); From 8398e0eda43127c0732562455ba8d22bf76efafb Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Tue, 4 Oct 2011 14:44:18 +0200 Subject: [PATCH 10/39] changed name of the TIME_WINDOW parameter to PERCENTILE_TIME_WINDOW --- locust/autotune.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/autotune.py b/locust/autotune.py index 907fac3f40..b28b248b67 100644 --- a/locust/autotune.py +++ b/locust/autotune.py @@ -6,7 +6,7 @@ slave_response_times = [] # The time window in seconds that current_percentile use data from -TIME_WINDOW = 15.0 +PERCENTILE_TIME_WINDOW = 15.0 def current_percentile(percent): from core import locust_runner, MasterLocustRunner From e8646a2a9c309f926bed714db5ebf4b8354fa95d Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Tue, 4 Oct 2011 17:07:16 +0200 Subject: [PATCH 11/39] forgot to change the name of TIME_WINDOW ni all places in last commit fixed a check of the key "current_reponses" in data for the event on_slave_report so that the list of responsetimes is only appended if the key exists --- locust/autotune.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/locust/autotune.py b/locust/autotune.py index b28b248b67..3078f06f4c 100644 --- a/locust/autotune.py +++ b/locust/autotune.py @@ -26,7 +26,7 @@ def on_request_success(_, response_time, _2): # remove from the queue rps = RequestStats.sum_stats().current_rps if len(master_response_times) > rps*TIME_WINDOW: - for i in xrange(len(master_response_times) - rps*TIME_WINDOW): + for i in xrange(len(master_response_times) - rps*PERCENTILE_TIME_WINDOW): master_response_times.popleft() def on_report_to_master(_, data): @@ -36,11 +36,13 @@ def on_report_to_master(_, data): def on_slave_report(_, data): from core import locust_runner, SLAVE_REPORT_INTERVAL - master_response_times.append(data["current_responses"]) + + if "current_responses" in data: + master_response_times.append(data["current_responses"]) # remove from the queue slaves = locust_runner.slave_count - response_times_per_slave_count = TIME_WINDOW/SLAVE_REPORT_INTERVAL + response_times_per_slave_count = PERCENTILE_TIME_WINDOW/SLAVE_REPORT_INTERVAL if len(master_response_times) > slaves * response_times_per_slave_count: master_response_times.popleft() From fad9e70b3d8e55ac5d4f9ceef3a815885a66c3f8 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Tue, 4 Oct 2011 17:11:41 +0200 Subject: [PATCH 12/39] Added a commandline option "--ramp" that starts saving the latest response times for being used in the start_ramping function --- locust/core.py | 4 +++- locust/main.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/locust/core.py b/locust/core.py index 2cdf0fb044..1a44d89753 100644 --- a/locust/core.py +++ b/locust/core.py @@ -12,7 +12,6 @@ import traceback from hashlib import md5 -from autotune import current_percentile from locust.stats import print_percentile_stats from clients import HttpBrowser from stats import RequestStats, print_stats @@ -538,6 +537,9 @@ def start_hatching(self, locust_count, hatch_rate): def start_ramping(self, hatch_rate=None, max_locusts=1000, hatch_stride=100, percent=0.95, response_time_limit=2000, acceptable_fail=0.05, precision=200, calibrate_rt_limit=False, reset=False, start_count=0): + + from autotune import current_percentile + if hatch_rate: self.hatch_rate = hatch_rate diff --git a/locust/main.py b/locust/main.py index 2ac9814a7b..e9f8240897 100644 --- a/locust/main.py +++ b/locust/main.py @@ -160,6 +160,15 @@ def parse_options(): help="Host or IP adress of locust master for distributed load testing. Only used when running with --slave. Defaults to 127.0.0.1." ) + # ramp feature enabled option + parser.add_option( + '--ramp', + action='store_true', + dest='ramp', + default=False, + help="Enables the auto tuning ramping feature for finding highest stable client count. NOTE having ramp enabled will add some more overhead for additional stats gathering" + ) + # Finalize # Return three-tuple of parser + the output from parse_args (opt obj, args) opts, args = parser.parse_args() @@ -331,6 +340,9 @@ def main(): # enable/disable gzip in WebLocust's HTTP client WebLocust.gzip = options.gzip + if options.ramp: + import autotune + if not options.master and not options.slave: core.locust_runner = LocalLocustRunner(locust_classes, options.hatch_rate, options.num_clients, options.num_requests, options.host) # spawn client spawning/hatching greenlet From 84caf26178414853267f60d15902e6c729da1217 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Tue, 4 Oct 2011 17:12:58 +0200 Subject: [PATCH 13/39] added pointer cursor for the table headers of the stats (for sorting) --- locust/static/style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/locust/static/style.css b/locust/static/style.css index 2df06cbb17..e16d933252 100644 --- a/locust/static/style.css +++ b/locust/static/style.css @@ -167,6 +167,10 @@ a { .stopped .edit a.close_link, .ready .edit a.close_link {display: none;} .running .edit a.close_link, .hatching .edit a.close_link {display: inline;} +.stats_label { + cursor: pointer; +} + .status table { border-collapse: collapse; width: 100%; From 30d6dda41278135e55bcf6030eaa490e9a447c97 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Tue, 11 Oct 2011 14:01:03 +0200 Subject: [PATCH 14/39] Fixed so that the stats-table is updated instantly when sorting by a column --- locust/templates/index.html | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/locust/templates/index.html b/locust/templates/index.html index df41d4aa29..4dd1646b6e 100644 --- a/locust/templates/index.html +++ b/locust/templates/index.html @@ -263,19 +263,29 @@

Version

} } + // Sorting by column var sortAttribute = "name"; var desc = false; - + var report; $(".stats_label").click(function(event) { event.preventDefault(); sortAttribute = $(this).attr("data-sortkey"); desc = !desc; - console.log("sortAttribute:", sortAttribute) + + $('#stats tbody').empty(); + $('#errors tbody').empty(); + alternate = false; + totalRow = report.stats.pop() + sortedStats = (report.stats).sort(sortBy(sortAttribute, desc)) + sortedStats.push(totalRow) + $('#stats tbody').jqoteapp(stats_tpl, sortedStats); + alternate = false; + $('#errors tbody').jqoteapp(errors_tpl, (report.errors).sort(sortBy(sortAttribute, desc))); }); function updateStats() { $.get('/stats/requests', function (data) { - var report = JSON.parse(data); + report = JSON.parse(data); $("#total_rps").html(Math.round(report.total_rps*100)/100); $("#fail_ratio").html(Math.round(report.fail_ratio*10000)/100); $("#status_text").html(report.state); @@ -286,7 +296,7 @@

Version

$('#stats tbody').empty(); $('#errors tbody').empty(); - + alternate = false; totalRow = report.stats.pop() From a20579e7527d3ed1b6e35fbb45db37d407acf577 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Tue, 11 Oct 2011 14:05:08 +0200 Subject: [PATCH 15/39] Changed names, switching names of hatch function and spawn_locusts this because of that the kill_locusts name fits better if the function adding clients is named spawn_locusts --- locust/core.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/locust/core.py b/locust/core.py index 1a44d89753..2e7c2f8d9c 100644 --- a/locust/core.py +++ b/locust/core.py @@ -362,7 +362,7 @@ def weight_locusts(self, amount, stop_timeout = None): bucket.extend([locust for x in xrange(0, num_locusts)]) return bucket - def hatch(self, spawn_count=None, stop_timeout=None, wait=False): + def spawn_locusts(self, spawn_count=None, stop_timeout=None, wait=False): if spawn_count is None: spawn_count = self.num_clients @@ -380,7 +380,7 @@ def hatch(self, spawn_count=None, stop_timeout=None, wait=False): print "\nHatching and swarming %i clients at the rate %g clients/s...\n" % (spawn_count, self.hatch_rate) occurence_count = dict([(l.__name__, 0) for l in self.locust_classes]) - def spawn_locusts(): + def hatch(): sleep_time = 1.0 / self.hatch_rate while True: if not bucket: @@ -402,7 +402,7 @@ def start_locust(_): print "%i locusts hatched" % len(self.locusts) gevent.sleep(sleep_time) - spawn_locusts() + hatch() if wait: self.locusts.join() print "All locusts dead\n" @@ -445,12 +445,12 @@ def start_hatching(self, locust_count=None, hatch_rate=None, wait=False): if hatch_rate: self.hatch_rate = hatch_rate spawn_count = locust_count - self.num_clients - self.hatch(spawn_count=spawn_count) + self.spawn_locusts(spawn_count=spawn_count) else: if locust_count: - self.hatch(locust_count, wait=wait) + self.spawn_locusts(locust_count, wait=wait) else: - self.hatch(wait=wait) + self.spawn_locusts(wait=wait) def stop(self): # if we are currently hatching locusts we need to kill the hatching greenlet first From e8e868ccd0702366ffe93b939ea2d46d12af565f Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Tue, 11 Oct 2011 15:23:52 +0200 Subject: [PATCH 16/39] Moved start_ramping to LocustRunner class instead of MasterLocustRunner Fixed so that the ramp down will never kill less than "precision" amount of clients --- locust/core.py | 160 +++++++++++++++++++++++++------------------------ 1 file changed, 81 insertions(+), 79 deletions(-) diff --git a/locust/core.py b/locust/core.py index 2e7c2f8d9c..a32c82d4f7 100644 --- a/locust/core.py +++ b/locust/core.py @@ -459,81 +459,6 @@ def stop(self): self.locusts.kill(block=True) self.state = STATE_STOPPED -class LocalLocustRunner(LocustRunner): - def start_hatching(self, locust_count=None, hatch_rate=None, wait=False): - self.hatching_greenlet = gevent.spawn(lambda: super(LocalLocustRunner, self).start_hatching(locust_count, hatch_rate, wait=wait)) - self.greenlet = self.hatching_greenlet - -class DistributedLocustRunner(LocustRunner): - def __init__(self, locust_classes, hatch_rate, num_clients, num_requests, host=None, master_host="localhost"): - super(DistributedLocustRunner, self).__init__(locust_classes, hatch_rate, num_clients, num_requests, host) - self.master_host = master_host - -class SlaveNode(object): - def __init__(self, id, state=STATE_INIT): - self.id = id - self.state = state - self.user_count = 0 - -class MasterLocustRunner(DistributedLocustRunner): - def __init__(self, *args, **kwargs): - super(MasterLocustRunner, self).__init__(*args, **kwargs) - - class SlaveNodesDict(dict): - def get_by_state(self, state): - return [c for c in self.itervalues() if c.state == state] - - @property - def ready(self): - return self.get_by_state(STATE_INIT) - - @property - def hatching(self): - return self.get_by_state(STATE_HATCHING) - - @property - def running(self): - return self.get_by_state(STATE_RUNNING) - - self.clients = SlaveNodesDict() - - self.client_stats = {} - self.client_errors = {} - self._request_stats = {} - - self.server = zmqrpc.Server() - self.greenlet = Group() - self.greenlet.spawn(self.client_listener).link_exception() - - # listener that gathers info on how many locust users the slaves has spawned - def on_slave_report(client_id, data): - self.clients[client_id].user_count = data["user_count"] - events.slave_report += on_slave_report - - @property - def user_count(self): - return sum([c.user_count for c in self.clients.itervalues()]) - - def start_hatching(self, locust_count, hatch_rate): - self.num_clients = locust_count - slave_num_clients = locust_count / ((len(self.clients.ready) + len(self.clients.running)) or 1) - slave_hatch_rate = float(hatch_rate) / ((len(self.clients.ready) + len(self.clients.running)) or 1) - - print "Sending hatch jobs to %i ready clients" % (len(self.clients.ready) + len(self.clients.running)) - if not (len(self.clients.ready)+len(self.clients.running)): - print "WARNING: You are running in distributed mode but have no slave servers connected." - print "Please connect slaves prior to swarming." - - if self.state != STATE_RUNNING and self.state != STATE_HATCHING: - RequestStats.clear_all() - - for client in self.clients.itervalues(): - msg = {"hatch_rate":slave_hatch_rate, "num_clients":slave_num_clients, "num_requests": self.num_requests, "host":self.host, "stop_timeout":None} - self.server.send({"type":"hatch", "data":msg}) - - RequestStats.global_start_time = time() - self.state = STATE_HATCHING - def start_ramping(self, hatch_rate=None, max_locusts=1000, hatch_stride=100, percent=0.95, response_time_limit=2000, acceptable_fail=0.05, precision=200, calibrate_rt_limit=False, reset=False, start_count=0): @@ -548,7 +473,6 @@ def start_ramping(self, hatch_rate=None, max_locusts=1000, hatch_stride=100, else: clients = self.num_clients - # Record low load percentile def calibrate(): self.start_hatching(clients, self.hatch_rate) @@ -565,7 +489,6 @@ def calibrate(): def ramp_up(clients, hatch_stride, boundery_found=False): while True: if self.state != STATE_HATCHING: - print "ramping up..." if self.num_clients >= max_locusts: print "ramp up stopped due to max_locusts limit reached:", max_locusts if not boundery_found: @@ -583,6 +506,7 @@ def ramp_up(clients, hatch_stride, boundery_found=False): if not boundery_found: hatch_stride = hatch_stride/2 return ramp_down(clients, hatch_stride) + print "ramping up..." clients += hatch_stride if not boundery_found: hatch_stride += hatch_stride @@ -592,7 +516,6 @@ def ramp_up(clients, hatch_stride, boundery_found=False): def ramp_down(clients, hatch_stride): while True: if self.state != STATE_HATCHING: - print "ramping down..." if self.num_clients < max_locusts: gevent.sleep(10) fails = RequestStats.sum_stats().fail_ratio @@ -604,7 +527,11 @@ def ramp_down(clients, hatch_stride): return hatch_stride = hatch_stride/2 return ramp_up(clients, hatch_stride, True) - hatch_stride = hatch_stride/2 + print "ramping down..." + if hatch_stride > precision: + hatch_stride = hatch_stride/2 + else: + hatch_stride = precision clients -= hatch_stride self.start_hatching(clients, self.hatch_rate) gevent.sleep(1) @@ -618,6 +545,81 @@ def ramp_down(clients, hatch_stride): self.start_hatching(start_count, hatch_stride) ramp_up(clients, hatch_stride) +class LocalLocustRunner(LocustRunner): + def start_hatching(self, locust_count=None, hatch_rate=None, wait=False): + self.hatching_greenlet = gevent.spawn(lambda: super(LocalLocustRunner, self).start_hatching(locust_count, hatch_rate, wait=wait)) + self.greenlet = self.hatching_greenlet + +class DistributedLocustRunner(LocustRunner): + def __init__(self, locust_classes, hatch_rate, num_clients, num_requests, host=None, master_host="localhost"): + super(DistributedLocustRunner, self).__init__(locust_classes, hatch_rate, num_clients, num_requests, host) + self.master_host = master_host + +class SlaveNode(object): + def __init__(self, id, state=STATE_INIT): + self.id = id + self.state = state + self.user_count = 0 + +class MasterLocustRunner(DistributedLocustRunner): + def __init__(self, *args, **kwargs): + super(MasterLocustRunner, self).__init__(*args, **kwargs) + + class SlaveNodesDict(dict): + def get_by_state(self, state): + return [c for c in self.itervalues() if c.state == state] + + @property + def ready(self): + return self.get_by_state(STATE_INIT) + + @property + def hatching(self): + return self.get_by_state(STATE_HATCHING) + + @property + def running(self): + return self.get_by_state(STATE_RUNNING) + + self.clients = SlaveNodesDict() + + self.client_stats = {} + self.client_errors = {} + self._request_stats = {} + + self.server = zmqrpc.Server() + self.greenlet = Group() + self.greenlet.spawn(self.client_listener).link_exception() + + # listener that gathers info on how many locust users the slaves has spawned + def on_slave_report(client_id, data): + self.clients[client_id].user_count = data["user_count"] + events.slave_report += on_slave_report + + @property + def user_count(self): + return sum([c.user_count for c in self.clients.itervalues()]) + + def start_hatching(self, locust_count, hatch_rate): + self.num_clients = locust_count + slave_num_clients = locust_count / ((len(self.clients.ready) + len(self.clients.running)) or 1) + slave_hatch_rate = float(hatch_rate) / ((len(self.clients.ready) + len(self.clients.running)) or 1) + + print "Sending hatch jobs to %i ready clients" % (len(self.clients.ready) + len(self.clients.running)) + if not (len(self.clients.ready)+len(self.clients.running)): + print "WARNING: You are running in distributed mode but have no slave servers connected." + print "Please connect slaves prior to swarming." + + if self.state != STATE_RUNNING and self.state != STATE_HATCHING: + RequestStats.clear_all() + + for client in self.clients.itervalues(): + msg = {"hatch_rate":slave_hatch_rate, "num_clients":slave_num_clients, "num_requests": self.num_requests, "host":self.host, "stop_timeout":None} + self.server.send({"type":"hatch", "data":msg}) + + RequestStats.global_start_time = time() + self.state = STATE_HATCHING + def stop(self): for client in self.clients.hatching + self.clients.running: self.server.send({"type":"stop", "data":{}}) From f725cbc7f38f0c861c0678f0e1964d33bf7c6fc2 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Tue, 11 Oct 2011 15:27:22 +0200 Subject: [PATCH 17/39] Fixed a "got float expected int" warning and fixed a variable name I forgot to refactor --- locust/autotune.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/locust/autotune.py b/locust/autotune.py index 3078f06f4c..72729aa147 100644 --- a/locust/autotune.py +++ b/locust/autotune.py @@ -1,6 +1,7 @@ from stats import percentile, RequestStats from collections import deque import events +import math master_response_times = deque([]) slave_response_times = [] @@ -25,8 +26,8 @@ def on_request_success(_, response_time, _2): # remove from the queue rps = RequestStats.sum_stats().current_rps - if len(master_response_times) > rps*TIME_WINDOW: - for i in xrange(len(master_response_times) - rps*PERCENTILE_TIME_WINDOW): + if len(master_response_times) > rps*PERCENTILE_TIME_WINDOW: + for i in xrange(len(master_response_times) - int(math.ceil(rps*PERCENTILE_TIME_WINDOW))): master_response_times.popleft() def on_report_to_master(_, data): From 6b62099de719bfe062226095e8c96dba42a60208 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Wed, 12 Oct 2011 18:45:14 +0200 Subject: [PATCH 18/39] Fixed an isse that made hatch_stide able to go below precision once --- locust/core.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/locust/core.py b/locust/core.py index 2ac15f185d..f85a628fc0 100644 --- a/locust/core.py +++ b/locust/core.py @@ -463,18 +463,13 @@ def stop(self): def start_ramping(self, hatch_rate=None, max_locusts=1000, hatch_stride=100, percent=0.95, response_time_limit=2000, acceptable_fail=0.05, - precision=200, calibrate_rt_limit=False, reset=False, start_count=0): + precision=200, start_count=0): from autotune import current_percentile - + calibrate_rt_limit=False if hatch_rate: self.hatch_rate = hatch_rate - if reset: - clients = 0 - else: - clients = self.num_clients - # Record low load percentile def calibrate(): self.start_hatching(clients, self.hatch_rate) @@ -531,21 +526,18 @@ def ramp_down(clients, hatch_stride): return ramp_up(clients, hatch_stride, True) print "ramping down..." if hatch_stride > precision: - hatch_stride = hatch_stride/2 + hatch_stride = max((hatch_stride/2),precision) else: hatch_stride = precision clients -= hatch_stride self.start_hatching(clients, self.hatch_rate) gevent.sleep(1) - if reset: - self.start_hatching(0,self.hatch_rate) if calibrate_rt_limit: response_time_limit = calibrate() - if start_count: - if start_coun > self.num_clients: - self.start_hatching(start_count, hatch_stride) - ramp_up(clients, hatch_stride) + if start_count > self.num_clients: + self.start_hatching(start_count, hatch_stride) + ramp_up(start_count, hatch_stride) class LocalLocustRunner(LocustRunner): def start_hatching(self, locust_count=None, hatch_rate=None, wait=False): From 56772b27494fc69563623d7acced925fd316abd2 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Wed, 12 Oct 2011 18:46:55 +0200 Subject: [PATCH 19/39] Added UI for the optional ramp functionallity (the UI is also optional) --- locust/templates/index.html | 74 ++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/locust/templates/index.html b/locust/templates/index.html index 4dd1646b6e..a1d42a777a 100644 --- a/locust/templates/index.html +++ b/locust/templates/index.html @@ -17,8 +17,11 @@
{{user_count}} users
- New test + New test Edit + {% if ramp %} + Ramp + {% endif %} {% if is_distributed %}
@@ -57,10 +60,14 @@

Start new Locust swarm



- +
+ {% if ramp and state == "ready" %} +
+ ...or let locust ramp find the highest stable locust count for you + {% endif %}
@@ -74,11 +81,46 @@

Change the locust count



- +
+ + {% if ramp %} +
+
+ Close +
+
+

Ramping

+
+
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+ + +
+
+
+ {% endif %}
    @@ -198,9 +240,14 @@

    Version

    event.preventDefault(); $.get($(this).attr("href")); }); - - $(".new_test").click(function(event) { + $(".ramp_test").click(function(event) { event.preventDefault(); + $("#start").hide(); + $("#ramp").show(); + }); + $("#new_test").click(function(event) { + event.preventDefault(); + $("#ramp").hide(); $("#start").show(); }); $(".edit_test").click(function(event) { @@ -236,6 +283,23 @@

    Version

    ); }); + $('#ramp_form').submit(function(event) { + event.preventDefault(); + $.post($(this).attr("action"), $(this).serialize(), + function(response) { + if (response.success) { + $("body").attr("class", "hatching"); + $("#ramp").fadeOut(); + $("#status").fadeIn(); + $(".box_running").fadeIn(); + $("a.new_test").fadeOut(); + $("a.edit_test").fadeIn(); + $(".user_count").fadeIn(); + } + } + ); + }); + $('#edit_form').submit(function(event) { event.preventDefault(); $.post($(this).attr("action"), $(this).serialize(), From 8a5b695600838bd4659ec177d5f7c3089bcfebcc Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Wed, 12 Oct 2011 18:49:19 +0200 Subject: [PATCH 20/39] Made the stats gathering as well as the UI rendering optional for the ramp functionality --- locust/main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/locust/main.py b/locust/main.py index e9f8240897..5200378cdb 100644 --- a/locust/main.py +++ b/locust/main.py @@ -331,18 +331,18 @@ def main(): # if --master is set, implicitly set --web if options.master: options.web = True - + + if options.ramp: + import autotune + if options.web and not options.slave: # spawn web greenlet print "Starting web monitor on port 8089" - main_greenlet = gevent.spawn(web.start, locust_classes, options.hatch_rate, options.num_clients, options.num_requests) + main_greenlet = gevent.spawn(web.start, locust_classes, options.hatch_rate, options.num_clients, options.num_requests, options.ramp) # enable/disable gzip in WebLocust's HTTP client WebLocust.gzip = options.gzip - if options.ramp: - import autotune - if not options.master and not options.slave: core.locust_runner = LocalLocustRunner(locust_classes, options.hatch_rate, options.num_clients, options.num_requests, options.host) # spawn client spawning/hatching greenlet From 2fcb4b49a1ade8b1899fbe41c9b9e9d10f0b6e46 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Wed, 12 Oct 2011 18:50:45 +0200 Subject: [PATCH 21/39] Added ramp UI css --- locust/static/style.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/locust/static/style.css b/locust/static/style.css index e16d933252..194a303fa4 100644 --- a/locust/static/style.css +++ b/locust/static/style.css @@ -93,6 +93,12 @@ a { top: 100px; margin-left: -169px; } + +.ramp { + width:800px; + margin-left: -370px; +} + .start .padder, .edit .padder { padding: 30px; padding-top: 0px; @@ -167,6 +173,8 @@ a { .stopped .edit a.close_link, .ready .edit a.close_link {display: none;} .running .edit a.close_link, .hatching .edit a.close_link {display: inline;} +.ready .ramp {display: none;} + .stats_label { cursor: pointer; } From 92ee44d87238cadd4cd0d2407a750b29b403f65f Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Wed, 12 Oct 2011 18:53:49 +0200 Subject: [PATCH 22/39] Added the global variable _ramp which is True if the --ramp option is used; this to make the UI for ramp optional as well Changed "/ramp" route to fit with the ramp form in the web UI --- locust/web.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/locust/web.py b/locust/web.py index 8a6e4b9988..0c05444b03 100644 --- a/locust/web.py +++ b/locust/web.py @@ -23,6 +23,7 @@ _num_requests = None _hatch_rate = None _request_stats_context_cache = {} +_ramp = False @app.route('/') @@ -39,6 +40,7 @@ def index(): is_distributed=is_distributed, slave_count=slave_count, user_count=locust_runner.user_count, + ramp = _ramp, version=version ) @@ -62,11 +64,22 @@ def stop(): response.headers["Content-type"] = "application/json" return response -@app.route("/ramp") +@app.route("/ramp", methods=["POST"]) def ramp(): from core import locust_runner - locust_runner.start_ramping(20, 2000, 200) - return "ramp" + init_clients = int(request.form["init_count"]) + hatch_rate = int(request.form["hatch_rate"]) + hatch_stride = int(request.form["hatch_stride"]) + precision = int(request.form["precision"]) + max_clients = int(request.form["max_count"]) + response_time = int(request.form["response_time"]) + percentile = float(int(request.form["percentile"]) / 100.0) + fail_rate = float(int(request.form["fail_rate"]) / 100.0) + + locust_runner.start_ramping(hatch_rate, max_clients, hatch_stride, percentile, response_time, fail_rate, precision, init_clients) + response = make_response(json.dumps({'success':True, 'message': 'Ramping started'})) + response.headers["Content-type"] = "application/json" + return response @app.route("/stats/reset") def reset_stats(): @@ -191,12 +204,13 @@ def request_stats(): report = _request_stats_context_cache["report"] return json.dumps(report) -def start(locust, hatch_rate, num_clients, num_requests): - global _locust, _hatch_rate, _num_clients, _num_requests +def start(locust, hatch_rate, num_clients, num_requests, ramp): + global _locust, _hatch_rate, _num_clients, _num_requests, _ramp _locust = locust _hatch_rate = hatch_rate _num_clients = num_clients _num_requests = num_requests + _ramp = ramp wsgi.WSGIServer(('', 8089), app, log=None).serve_forever() def _sort_stats(stats): From a970533d438466eec59c4c9f6175d62992dd34be Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Thu, 20 Oct 2011 15:39:48 +0200 Subject: [PATCH 23/39] added "calibration_time" argument to start_ramping to be able to change calibration times during the ramping --- locust/core.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/locust/core.py b/locust/core.py index f85a628fc0..5b7476d132 100644 --- a/locust/core.py +++ b/locust/core.py @@ -461,9 +461,10 @@ def stop(self): self.locusts.kill(block=True) self.state = STATE_STOPPED + def start_ramping(self, hatch_rate=None, max_locusts=1000, hatch_stride=100, percent=0.95, response_time_limit=2000, acceptable_fail=0.05, - precision=200, start_count=0): + precision=200, start_count=0, calibration_time=15): from autotune import current_percentile calibrate_rt_limit=False @@ -491,7 +492,7 @@ def ramp_up(clients, hatch_stride, boundery_found=False): if not boundery_found: hatch_stride = hatch_stride/2 return ramp_down(clients, hatch_stride) - gevent.sleep(10) + gevent.sleep(calibration_time) if RequestStats.sum_stats().fail_ratio >= acceptable_fail: print "ramp up stopped due to acceptable_fail ratio (%d1.2%%) exceeded with fail ratio %1.2d%%", (acceptable_fail*100, RequestStats.sum_stats().fail_ratio*100) if not boundery_found: @@ -514,7 +515,7 @@ def ramp_down(clients, hatch_stride): while True: if self.state != STATE_HATCHING: if self.num_clients < max_locusts: - gevent.sleep(10) + gevent.sleep(calibration_time) fails = RequestStats.sum_stats().fail_ratio if fails <= acceptable_fail: p = current_percentile(percent) From 72898e3c53e79496431b72d25c33186dd4ce0670 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Thu, 20 Oct 2011 15:43:15 +0200 Subject: [PATCH 24/39] fixed so that the ramp form doesn't "freeze" when submitting; to do this I made the start_ramping spawn in a separate greenlet added "calibration_time" to the ramp form in the interface --- locust/templates/index.html | 10 +++++++--- locust/web.py | 5 +++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/locust/templates/index.html b/locust/templates/index.html index a1d42a777a..3c7058551b 100644 --- a/locust/templates/index.html +++ b/locust/templates/index.html @@ -104,18 +104,22 @@

    Ramping



    + +

- -

+ +

-
+

+ +
diff --git a/locust/web.py b/locust/web.py index 0c05444b03..a22388505d 100644 --- a/locust/web.py +++ b/locust/web.py @@ -11,6 +11,7 @@ from locust.stats import RequestStats, median_from_dict from locust import version +import gevent DEFAULT_CACHE_TIME = 2.0 @@ -75,8 +76,8 @@ def ramp(): response_time = int(request.form["response_time"]) percentile = float(int(request.form["percentile"]) / 100.0) fail_rate = float(int(request.form["fail_rate"]) / 100.0) - - locust_runner.start_ramping(hatch_rate, max_clients, hatch_stride, percentile, response_time, fail_rate, precision, init_clients) + calibration_time = int(request.form["wait_time"]) + gevent.spawn(locust_runner.start_ramping, hatch_rate, max_clients, hatch_stride, percentile, response_time, fail_rate, precision, init_clients, calibration_time) response = make_response(json.dumps({'success':True, 'message': 'Ramping started'})) response.headers["Content-type"] = "application/json" return response From e3f03b4522b2676c0dffccd44a3e064bdc9f6b17 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Thu, 20 Oct 2011 15:57:01 +0200 Subject: [PATCH 25/39] fixed a bug where the ramp_text would be shown in the new test form after a test has already been run (the ramp_text should only be shown on the index page when no test has yet been run) --- locust/static/style.css | 3 +++ locust/templates/index.html | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/locust/static/style.css b/locust/static/style.css index 194a303fa4..8affab52f4 100644 --- a/locust/static/style.css +++ b/locust/static/style.css @@ -175,6 +175,9 @@ a { .ready .ramp {display: none;} +.ready .ramp_text {display: inline;} +.hatching .ramp_text, .running .ramp_text, .stopped .ramp_text {display: none;} + .stats_label { cursor: pointer; } diff --git a/locust/templates/index.html b/locust/templates/index.html index 3c7058551b..ffc5b071ad 100644 --- a/locust/templates/index.html +++ b/locust/templates/index.html @@ -64,10 +64,13 @@

Start new Locust swarm

- {% if ramp and state == "ready" %} -
+
+
+ {% if ramp %} + ...or let locust ramp find the highest stable locust count for you {% endif %} +
From eb43c8c9e5e25f9bd51e9eb4064efaa1110ab567 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Thu, 20 Oct 2011 16:09:19 +0200 Subject: [PATCH 26/39] renamed autotune.py to rampstats.py; it makes more sense --- locust/{autotune.py => rampstats.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename locust/{autotune.py => rampstats.py} (100%) diff --git a/locust/autotune.py b/locust/rampstats.py similarity index 100% rename from locust/autotune.py rename to locust/rampstats.py From 975c78ac850961e033db31e966dada37169df9ed Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Thu, 20 Oct 2011 17:29:07 +0200 Subject: [PATCH 27/39] start_hatching was called with the wrong argument (hatch_stride instead of hatch_rate) in start_ramping when spawning the initial number of clients --- locust/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/core.py b/locust/core.py index 5b7476d132..7aef783cec 100644 --- a/locust/core.py +++ b/locust/core.py @@ -537,7 +537,7 @@ def ramp_down(clients, hatch_stride): if calibrate_rt_limit: response_time_limit = calibrate() if start_count > self.num_clients: - self.start_hatching(start_count, hatch_stride) + self.start_hatching(start_count, hatch_rate) ramp_up(start_count, hatch_stride) class LocalLocustRunner(LocustRunner): From 4ff4b8876a7d2acbd7e1f23fb5205f1e58d7565c Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Fri, 21 Oct 2011 12:52:13 +0200 Subject: [PATCH 28/39] When I renamed autotune.py the refactoring did not affect the imports so now I fixed htem --- locust/core.py | 2 +- locust/main.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/locust/core.py b/locust/core.py index 7aef783cec..6e434ebe25 100644 --- a/locust/core.py +++ b/locust/core.py @@ -466,7 +466,7 @@ def start_ramping(self, hatch_rate=None, max_locusts=1000, hatch_stride=100, percent=0.95, response_time_limit=2000, acceptable_fail=0.05, precision=200, start_count=0, calibration_time=15): - from autotune import current_percentile + from rampstats import current_percentile calibrate_rt_limit=False if hatch_rate: self.hatch_rate = hatch_rate diff --git a/locust/main.py b/locust/main.py index 5200378cdb..30fc9cf2e6 100644 --- a/locust/main.py +++ b/locust/main.py @@ -333,7 +333,7 @@ def main(): options.web = True if options.ramp: - import autotune + import rampstats if options.web and not options.slave: # spawn web greenlet From c54ad5ef0b585597cca6ed1c545b95c5ae7a68da Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Fri, 21 Oct 2011 14:05:06 +0200 Subject: [PATCH 29/39] Moved the eventlisteners in rampstats.py to main.py The eventlistensers are only created if --ramp is used --- locust/main.py | 5 +++++ locust/rampstats.py | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/locust/main.py b/locust/main.py index 30fc9cf2e6..501873a00a 100644 --- a/locust/main.py +++ b/locust/main.py @@ -334,6 +334,11 @@ def main(): if options.ramp: import rampstats + from rampstats import on_request_success, on_report_to_master, on_slave_report + import events + events.request_success += on_request_success + events.report_to_master += on_report_to_master + events.slave_report += on_slave_report if options.web and not options.slave: # spawn web greenlet diff --git a/locust/rampstats.py b/locust/rampstats.py index 72729aa147..827ce8a784 100644 --- a/locust/rampstats.py +++ b/locust/rampstats.py @@ -46,7 +46,3 @@ def on_slave_report(_, data): response_times_per_slave_count = PERCENTILE_TIME_WINDOW/SLAVE_REPORT_INTERVAL if len(master_response_times) > slaves * response_times_per_slave_count: master_response_times.popleft() - -events.request_success += on_request_success -events.report_to_master += on_report_to_master -events.slave_report += on_slave_report \ No newline at end of file From db74842ef89d1a7ea5da29d2e4a99720c1f030a6 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Fri, 21 Oct 2011 14:40:18 +0200 Subject: [PATCH 30/39] fixed and added some print outs in start_ramping removed unused calibration code --- locust/core.py | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/locust/core.py b/locust/core.py index 954f0d11c7..69ca381e54 100644 --- a/locust/core.py +++ b/locust/core.py @@ -467,23 +467,9 @@ def start_ramping(self, hatch_rate=None, max_locusts=1000, hatch_stride=100, precision=200, start_count=0, calibration_time=15): from rampstats import current_percentile - calibrate_rt_limit=False if hatch_rate: self.hatch_rate = hatch_rate - # Record low load percentile - def calibrate(): - self.start_hatching(clients, self.hatch_rate) - while True: - if self.state != STATE_HATCHING: - print "recording low_percentile..." - gevent.sleep(15) - percentile = current_percentile(percent) - print "low_percentile:", percentile - self.start_hatching(0, self.hatch_rate) - return percentile*3 - gevent.sleep(1) - def ramp_up(clients, hatch_stride, boundery_found=False): while True: if self.state != STATE_HATCHING: @@ -493,8 +479,9 @@ def ramp_up(clients, hatch_stride, boundery_found=False): hatch_stride = hatch_stride/2 return ramp_down(clients, hatch_stride) gevent.sleep(calibration_time) - if RequestStats.sum_stats().fail_ratio >= acceptable_fail: - print "ramp up stopped due to acceptable_fail ratio (%d1.2%%) exceeded with fail ratio %1.2d%%", (acceptable_fail*100, RequestStats.sum_stats().fail_ratio*100) + fail_ratio = RequestStats.sum_stats().fail_ratio + if fail_ratio >= acceptable_fail: + print "ramp up stopped due to acceptable_fail ratio %d%% exceeded with fail ratio %1.2d%%" % (acceptable_fail*100, fail_ratio*100) if not boundery_found: hatch_stride = hatch_stride/2 return ramp_down(clients, hatch_stride) @@ -531,11 +518,14 @@ def ramp_down(clients, hatch_stride): else: hatch_stride = precision clients -= hatch_stride - self.start_hatching(clients, self.hatch_rate) + if clients > 0: + self.start_hatching(clients, self.hatch_rate) + else: + print "WARNING: no responses met the ramping thresholds, check your ramp configuration and \"--host\" address" + print "ramping stopped!" + return gevent.sleep(1) - if calibrate_rt_limit: - response_time_limit = calibrate() if start_count > self.num_clients: self.start_hatching(start_count, hatch_rate) ramp_up(start_count, hatch_stride) From 6540207c817186b6781ade7c5d1c61bd44d5c170 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Fri, 21 Oct 2011 14:52:18 +0200 Subject: [PATCH 31/39] changed some printouts i start_ramping --- locust/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locust/core.py b/locust/core.py index 69ca381e54..aaa30236c2 100644 --- a/locust/core.py +++ b/locust/core.py @@ -474,14 +474,14 @@ def ramp_up(clients, hatch_stride, boundery_found=False): while True: if self.state != STATE_HATCHING: if self.num_clients >= max_locusts: - print "ramp up stopped due to max_locusts limit reached:", max_locusts + print "ramp up stopped due to max locusts limit reached:", max_locusts if not boundery_found: hatch_stride = hatch_stride/2 return ramp_down(clients, hatch_stride) gevent.sleep(calibration_time) fail_ratio = RequestStats.sum_stats().fail_ratio if fail_ratio >= acceptable_fail: - print "ramp up stopped due to acceptable_fail ratio %d%% exceeded with fail ratio %1.2d%%" % (acceptable_fail*100, fail_ratio*100) + print "ramp up stopped due to acceptable fail ratio %d%% exceeded with fail ratio %d%%" % (acceptable_fail*100, fail_ratio*100) if not boundery_found: hatch_stride = hatch_stride/2 return ramp_down(clients, hatch_stride) From 652ea564ad60eefb17fdcc942a5ed62e6f34b494 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Fri, 21 Oct 2011 15:04:59 +0200 Subject: [PATCH 32/39] Fixed "variable used prior to global declaration" warning; Added some comments --- locust/rampstats.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/locust/rampstats.py b/locust/rampstats.py index 827ce8a784..55e263dc3a 100644 --- a/locust/rampstats.py +++ b/locust/rampstats.py @@ -11,17 +11,21 @@ def current_percentile(percent): from core import locust_runner, MasterLocustRunner + + # Are we running in distributed mode or not? if isinstance(locust_runner, MasterLocustRunner): + # Flatten out the deque of lists and calculate the percentile to be returned return percentile(sorted([item for sublist in master_response_times for item in sublist]), percent) else: return percentile(sorted(master_response_times), percent) def on_request_success(_, response_time, _2): from core import locust_runner, MasterLocustRunner + + # Are we running in distributed mode or not? if isinstance(locust_runner, MasterLocustRunner): slave_response_times.append(response_time) else: - # The locust_runner is not running in distributed mode master_response_times.append(response_time) # remove from the queue @@ -31,8 +35,8 @@ def on_request_success(_, response_time, _2): master_response_times.popleft() def on_report_to_master(_, data): - data["current_responses"] = slave_response_times global slave_response_times + data["current_responses"] = slave_response_times slave_response_times = [] def on_slave_report(_, data): From d9a9f6646a90f45561a703de85f1417cad2d9b8c Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Fri, 21 Oct 2011 15:53:15 +0200 Subject: [PATCH 33/39] RequestStats property fail_ratio did not calculate the fail ratio correctly, fixed it --- locust/stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/stats.py b/locust/stats.py index a5ddc949b7..880d50e284 100644 --- a/locust/stats.py +++ b/locust/stats.py @@ -99,7 +99,7 @@ def log_error(self, error): @property def fail_ratio(self): try: - return float(self.num_failures) / self.num_reqs + return float(self.num_failures) / (self.num_reqs + self.num_failures) except ZeroDivisionError: if self.num_failures > 0: return 1.0 From 4a6e039649d65b7f6683ac1da8bd806030440120 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Fri, 21 Oct 2011 15:57:31 +0200 Subject: [PATCH 34/39] Fixed wrong failure percentage bug in UI Changed so that RequestStats property fail_ratio is used in web.py instead of caluculating the fail ratio there --- locust/templates/index.html | 3 ++- locust/web.py | 8 +------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/locust/templates/index.html b/locust/templates/index.html index ffc5b071ad..303d275763 100644 --- a/locust/templates/index.html +++ b/locust/templates/index.html @@ -358,7 +358,8 @@

Version

$.get('/stats/requests', function (data) { report = JSON.parse(data); $("#total_rps").html(Math.round(report.total_rps*100)/100); - $("#fail_ratio").html(Math.round(report.fail_ratio*10000)/100); + //$("#fail_ratio").html(Math.round(report.fail_ratio*10000)/100); + $("#fail_ratio").html(Math.round(report.fail_ratio*100)); $("#status_text").html(report.state); $("#userCount").html(report.user_count); diff --git a/locust/web.py b/locust/web.py index f1e83d802d..6646881552 100644 --- a/locust/web.py +++ b/locust/web.py @@ -172,13 +172,7 @@ def request_stats(): report = {"stats":stats, "errors":list(locust_runner.errors.iteritems())} if stats: report["total_rps"] = stats[len(stats)-1]["current_rps"] - try: - report["fail_ratio"] = float(stats[len(stats)-1]["num_failures"]) / stats[len(stats)-1]["num_reqs"] - except ZeroDivisionError: - if stats[len(stats)-1]["num_failures"] > 0: - report["fail_ratio"] = 100 - else: - report["fail_ratio"] = 0 + report["fail_ratio"] = RequestStats.sum_stats("Total").fail_ratio # since generating a total response times dict with all response times from all # urls is slow, we make a new total response time dict which will consist of one From 8efe8a658f99e8749c529c33c40dc5d235bc1b8e Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Tue, 25 Oct 2011 12:38:18 +0200 Subject: [PATCH 35/39] Fixed so that only the event listeners needed are created for the master/slaves when usering the --ramp option --- locust/main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/locust/main.py b/locust/main.py index 501873a00a..8cd0b90f9a 100644 --- a/locust/main.py +++ b/locust/main.py @@ -337,8 +337,10 @@ def main(): from rampstats import on_request_success, on_report_to_master, on_slave_report import events events.request_success += on_request_success - events.report_to_master += on_report_to_master - events.slave_report += on_slave_report + if options.slave: + events.report_to_master += on_report_to_master + if options.master: + events.slave_report += on_slave_report if options.web and not options.slave: # spawn web greenlet From 5c7cfcfb9548bb2c2204b503a93541e1b2b0d779 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Tue, 25 Oct 2011 13:36:26 +0200 Subject: [PATCH 36/39] The check to see whether we'r running in ditributed mode or not is now only done once and the result put in a variable in rampstats.py --- locust/rampstats.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/locust/rampstats.py b/locust/rampstats.py index 55e263dc3a..d5038bf020 100644 --- a/locust/rampstats.py +++ b/locust/rampstats.py @@ -1,4 +1,5 @@ from stats import percentile, RequestStats +from core import locust_runner, MasterLocustRunner from collections import deque import events import math @@ -6,24 +7,21 @@ master_response_times = deque([]) slave_response_times = [] +# Are we running in distributed mode or not? +is_distributed = isinstance(locust_runner, MasterLocustRunner) + # The time window in seconds that current_percentile use data from PERCENTILE_TIME_WINDOW = 15.0 def current_percentile(percent): - from core import locust_runner, MasterLocustRunner - - # Are we running in distributed mode or not? - if isinstance(locust_runner, MasterLocustRunner): + if is_distributed: # Flatten out the deque of lists and calculate the percentile to be returned return percentile(sorted([item for sublist in master_response_times for item in sublist]), percent) else: return percentile(sorted(master_response_times), percent) def on_request_success(_, response_time, _2): - from core import locust_runner, MasterLocustRunner - - # Are we running in distributed mode or not? - if isinstance(locust_runner, MasterLocustRunner): + if is_distributed: slave_response_times.append(response_time) else: master_response_times.append(response_time) From 2ba5b6069c9d1d59e31cd91a01684e3cd5a87ff2 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Tue, 25 Oct 2011 16:02:33 +0200 Subject: [PATCH 37/39] Fixed so that the fail ratio has to be strictly greater than the acceptable fail ratio Before there was a problem if the acceptable fail ratio was 0 --- locust/core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locust/core.py b/locust/core.py index aaa30236c2..60926b91cb 100644 --- a/locust/core.py +++ b/locust/core.py @@ -480,7 +480,7 @@ def ramp_up(clients, hatch_stride, boundery_found=False): return ramp_down(clients, hatch_stride) gevent.sleep(calibration_time) fail_ratio = RequestStats.sum_stats().fail_ratio - if fail_ratio >= acceptable_fail: + if fail_ratio > acceptable_fail: print "ramp up stopped due to acceptable fail ratio %d%% exceeded with fail ratio %d%%" % (acceptable_fail*100, fail_ratio*100) if not boundery_found: hatch_stride = hatch_stride/2 @@ -503,8 +503,8 @@ def ramp_down(clients, hatch_stride): if self.state != STATE_HATCHING: if self.num_clients < max_locusts: gevent.sleep(calibration_time) - fails = RequestStats.sum_stats().fail_ratio - if fails <= acceptable_fail: + fail_ratio = RequestStats.sum_stats().fail_ratio + if fail_ratio <= acceptable_fail: p = current_percentile(percent) if p <= response_time_limit: if hatch_stride <= precision: From 9b034843270d6a06433ea3ce3b534452cf9d0737 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Thu, 3 Nov 2011 19:15:45 +0100 Subject: [PATCH 38/39] Fixed a bug that made variable "is_distributed" every time --- locust/main.py | 21 +++++++++++---------- locust/rampstats.py | 5 ++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/locust/main.py b/locust/main.py index 8cd0b90f9a..abd53765ac 100644 --- a/locust/main.py +++ b/locust/main.py @@ -332,16 +332,6 @@ def main(): if options.master: options.web = True - if options.ramp: - import rampstats - from rampstats import on_request_success, on_report_to_master, on_slave_report - import events - events.request_success += on_request_success - if options.slave: - events.report_to_master += on_report_to_master - if options.master: - events.slave_report += on_slave_report - if options.web and not options.slave: # spawn web greenlet print "Starting web monitor on port 8089" @@ -362,6 +352,17 @@ def main(): core.locust_runner = SlaveLocustRunner(locust_classes, options.hatch_rate, options.num_clients, num_requests=options.num_requests, host=options.host, master_host=options.master_host) main_greenlet = core.locust_runner.greenlet + if options.ramp: + import rampstats + from rampstats import on_request_success, on_report_to_master, on_slave_report + import events + if options.slave: + events.report_to_master += on_report_to_master + if options.master: + events.slave_report += on_slave_report + else: + events.request_success += on_request_success + if options.print_stats or (not options.web and not options.slave): # spawn stats printing greenlet gevent.spawn(stats_printer) diff --git a/locust/rampstats.py b/locust/rampstats.py index d5038bf020..e69733c30a 100644 --- a/locust/rampstats.py +++ b/locust/rampstats.py @@ -1,5 +1,5 @@ from stats import percentile, RequestStats -from core import locust_runner, MasterLocustRunner +from core import locust_runner, DistributedLocustRunner from collections import deque import events import math @@ -8,7 +8,7 @@ slave_response_times = [] # Are we running in distributed mode or not? -is_distributed = isinstance(locust_runner, MasterLocustRunner) +is_distributed = isinstance(locust_runner, DistributedLocustRunner) # The time window in seconds that current_percentile use data from PERCENTILE_TIME_WINDOW = 15.0 @@ -39,7 +39,6 @@ def on_report_to_master(_, data): def on_slave_report(_, data): from core import locust_runner, SLAVE_REPORT_INTERVAL - if "current_responses" in data: master_response_times.append(data["current_responses"]) From 6b3068a32ba51f49c8d8bc3b307c7b8f0d889312 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Thu, 3 Nov 2011 19:28:44 +0100 Subject: [PATCH 39/39] Reworked start_ramping a bit, hatch_stride does not grow when ramping up now --- locust/core.py | 47 +++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/locust/core.py b/locust/core.py index 60926b91cb..a3029904cc 100644 --- a/locust/core.py +++ b/locust/core.py @@ -342,7 +342,7 @@ def user_count(self): def weight_locusts(self, amount, stop_timeout = None): """ Distributes the amount of locusts for each WebLocust-class according to it's weight - and a list: bucket with the weighted locusts is returned + returns a list "bucket" with the weighted locusts """ bucket = [] weight_sum = sum((locust.weight for locust in self.locust_classes)) @@ -419,7 +419,7 @@ def kill_locusts(self, kill_count): print "killing locusts:", kill_count dying = [] for g in self.locusts: - for l in bucket: + for l in bucket: if l == g.args[0]: dying.append(g) bucket.remove(l) @@ -431,8 +431,8 @@ def kill_locusts(self, kill_count): def start_hatching(self, locust_count=None, hatch_rate=None, wait=False): print "start hatching", locust_count, hatch_rate, self.state if self.state != STATE_RUNNING and self.state != STATE_HATCHING: - RequestStats.clear_all() - RequestStats.global_start_time = time() + RequestStats.clear_all() + RequestStats.global_start_time = time() # Dynamically changing the locust count if self.state != STATE_INIT and self.state != STATE_STOPPED: self.state = STATE_HATCHING @@ -469,32 +469,39 @@ def start_ramping(self, hatch_rate=None, max_locusts=1000, hatch_stride=100, from rampstats import current_percentile if hatch_rate: self.hatch_rate = hatch_rate - + + def ramp_down_help(clients, hatch_stride): + print "ramping down..." + hatch_stride = max(hatch_stride/2, precision) + clients -= hatch_stride + self.start_hatching(clients, self.hatch_rate) + return clients, hatch_stride + def ramp_up(clients, hatch_stride, boundery_found=False): while True: if self.state != STATE_HATCHING: if self.num_clients >= max_locusts: print "ramp up stopped due to max locusts limit reached:", max_locusts - if not boundery_found: - hatch_stride = hatch_stride/2 + client, hatch_stride = ramp_down_help(clients, hatch_stride) return ramp_down(clients, hatch_stride) gevent.sleep(calibration_time) fail_ratio = RequestStats.sum_stats().fail_ratio if fail_ratio > acceptable_fail: print "ramp up stopped due to acceptable fail ratio %d%% exceeded with fail ratio %d%%" % (acceptable_fail*100, fail_ratio*100) - if not boundery_found: - hatch_stride = hatch_stride/2 + client, hatch_stride = ramp_down_help(clients, hatch_stride) return ramp_down(clients, hatch_stride) p = current_percentile(percent) if p >= response_time_limit: - print "ramp up stopped due to response times getting high:", p - if not boundery_found: - hatch_stride = hatch_stride/2 + print "ramp up stopped due to percentile response times getting high:", p + client, hatch_stride = ramp_down_help(clients, hatch_stride) return ramp_down(clients, hatch_stride) + if boundery_found and hatch_stride <= precision: + print "sweet spot found, ramping stopped!" + return print "ramping up..." + if boundery_found: + hatch_stride = max((hatch_stride/2),precision) clients += hatch_stride - if not boundery_found: - hatch_stride += hatch_stride self.start_hatching(clients, self.hatch_rate) gevent.sleep(1) @@ -510,18 +517,18 @@ def ramp_down(clients, hatch_stride): if hatch_stride <= precision: print "sweet spot found, ramping stopped!" return - hatch_stride = hatch_stride/2 + print "ramping up..." + hatch_stride = max((hatch_stride/2),precision) + clients += hatch_stride + self.start_hatching(clients, self.hatch_rate) return ramp_up(clients, hatch_stride, True) print "ramping down..." - if hatch_stride > precision: - hatch_stride = max((hatch_stride/2),precision) - else: - hatch_stride = precision + hatch_stride = max((hatch_stride/2),precision) clients -= hatch_stride if clients > 0: self.start_hatching(clients, self.hatch_rate) else: - print "WARNING: no responses met the ramping thresholds, check your ramp configuration and \"--host\" address" + print "WARNING: no responses met the ramping thresholds, check your ramp configuration, locustfile and \"--host\" address" print "ramping stopped!" return gevent.sleep(1)
# failsType# failsType