From ab1bafa742561c0d46029ff5e480b65816043cf3 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Mon, 29 Aug 2011 18:38:53 +0200 Subject: [PATCH 01/19] Started implementation of dynamically spawning locusts. Refactoring code defining weight_locust() --- locust/core.py | 74 ++++++++++++++++++++++++++++++++++---------------- locust/main.py | 2 +- locust/web.py | 7 +++++ 3 files changed, 59 insertions(+), 24 deletions(-) diff --git a/locust/core.py b/locust/core.py index 8c383f53c6..8bece90d85 100644 --- a/locust/core.py +++ b/locust/core.py @@ -329,18 +329,15 @@ def errors(self): @property def user_count(self): return len(self.locusts) - - def hatch(self, stop_timeout=None): - if self.num_requests is not None: - RequestStats.global_max_requests = self.num_requests - + + def weight_locusts(self, new_locusts, stop_timeout): bucket = [] weight_sum = sum((locust.weight for locust in self.locust_classes)) for locust in self.locust_classes: if not locust.tasks: warnings.warn("Notice: Found locust (%s) got no tasks. Skipping..." % locust.__name__) continue - + if self.host is not None: locust.host = self.host if stop_timeout is not None: @@ -348,8 +345,19 @@ def hatch(self, stop_timeout=None): # create locusts depending on weight percent = locust.weight / float(weight_sum) - num_locusts = int(round(self.num_clients * percent)) + num_locusts = int(round(new_locusts * percent)) bucket.extend([locust for x in xrange(0, num_locusts)]) + return bucket + + def hatch(self, new_locusts=None, stop_timeout=None, wait=False): + if new_locusts is None: + new_locusts = self.num_clients + + if self.num_requests is not None: + RequestStats.global_max_requests = self.num_requests + + bucket = self.weight_locusts(new_locusts, stop_timeout) + print "bucket:", bucket, "new_locusts count:", new_locusts print "\nHatching and swarming %i clients at the rate %g clients/s...\n" % (self.num_clients, self.hatch_rate) occurence_count = dict([(l.__name__, 0) for l in self.locust_classes]) @@ -378,23 +386,43 @@ def start_locust(): gevent.sleep(sleep_time) spawn_locusts() - self.locusts.join() - print "All locusts dead\n" - print_stats(self.request_stats) - print_percentile_stats(self.request_stats) #TODO use an event listener, or such, for this? - - def start_hatching(self, locust_count=None, hatch_rate=None): - if locust_count: - self.num_clients = locust_count - if hatch_rate: - self.hatch_rate = hatch_rate - + if wait: + self.locusts.join() + print "All locusts dead\n" + print_stats(self.request_stats) + print_percentile_stats(self.request_stats) #TODO use an event listener, or such, for this? + + def kill_locusts(self, amount): + print "killing locusts:", amount + + 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() + + # When dynamically changing the locust count + if self.state != STATE_INIT: + if self.num_clients > locust_count: + # Kill some locusts + kill_amount = self.num_clients - locust_count + self.num_clients = locust_count + kill_locusts(kill_amount) + elif self.num_clients < locust_count: + # Spawn some locusts + new_locusts = locust_count - self.num_clients + self.num_clients = locust_count + self.hatch(new_locusts=new_locusts) + + if hatch_rate: + self.hatch_rate = hatch_rate + else: + if locust_count: + self.num_clients = locust_count + self.state = STATE_HATCHING + self.hatch(wait=wait) - RequestStats.global_start_time = time() - self.state = STATE_HATCHING - self.hatch() + def stop(self): # if we are currently hatching locusts we need to kill the hatching greenlet first @@ -404,8 +432,8 @@ def stop(self): self.state = STATE_STOPPED class LocalLocustRunner(LocustRunner): - def start_hatching(self, locust_count=None, hatch_rate=None): - self.hatching_greenlet = gevent.spawn(lambda: super(LocalLocustRunner, self).start_hatching(locust_count, hatch_rate)) + 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): diff --git a/locust/main.py b/locust/main.py index 7573de0ed0..cb41dff8f9 100644 --- a/locust/main.py +++ b/locust/main.py @@ -331,7 +331,7 @@ def main(): core.locust_runner = LocalLocustRunner(locust_classes, options.hatch_rate, options.num_clients, options.num_requests, options.host) # spawn client spawning/hatching greenlet if not options.web: - core.locust_runner.start_hatching() + core.locust_runner.start_hatching(wait=True) main_greenlet = core.locust_runner.greenlet elif options.master: core.locust_runner = MasterLocustRunner(locust_classes, options.hatch_rate, options.num_clients, num_requests=options.num_requests, host=options.host, master_host=options.master_host) diff --git a/locust/web.py b/locust/web.py index 644b1722e9..918ff95821 100644 --- a/locust/web.py +++ b/locust/web.py @@ -59,6 +59,13 @@ def stop(): response.headers["Content-type"] = "application/json" return response +#Adding locust +@app.route('/add') +def add(): + from core import locust_runner + locust_runner.start_hatching(20, 1) + return "hejsan!" + @app.route("/stats/requests/csv") def request_stats_csv(): from core import locust_runner From b677356780e8880efb67eb3994b7521166b7881d Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Tue, 30 Aug 2011 17:59:28 +0200 Subject: [PATCH 02/19] added a kill_locusts for dynamically reduce the number of locusts, it's buggy and not done --- locust/core.py | 37 +++++++++++++++++++++++++++++-------- locust/web.py | 2 +- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/locust/core.py b/locust/core.py index 8bece90d85..a8a9fc5bdf 100644 --- a/locust/core.py +++ b/locust/core.py @@ -330,7 +330,7 @@ def errors(self): def user_count(self): return len(self.locusts) - def weight_locusts(self, new_locusts, stop_timeout): + def weight_locusts(self, new_locusts, stop_timeout = None): bucket = [] weight_sum = sum((locust.weight for locust in self.locust_classes)) for locust in self.locust_classes: @@ -373,14 +373,14 @@ def spawn_locusts(): locust = bucket.pop(random.randint(0, len(bucket)-1)) occurence_count[locust.__name__] += 1 - def start_locust(): + def start_locust(_): try: locust()() except RescheduleTaskImmediately: pass except GreenletExit: pass - new_locust = self.locusts.spawn(start_locust) + new_locust = self.locusts.spawn(start_locust, locust) if len(self.locusts) % 10 == 0: print "%i locusts hatched" % len(self.locusts) gevent.sleep(sleep_time) @@ -393,7 +393,28 @@ def start_locust(): print_percentile_stats(self.request_stats) #TODO use an event listener, or such, for this? def kill_locusts(self, amount): + bucket = self.weight_locusts(amount) + print "bucket: ", bucket print "killing locusts:", amount + #print "locusts: ", self.locusts + killbuffer = Group() + while True: + if not bucket: + print "killbuffer: ", killbuffer + killbuffer.kill(block=True) + return + l = bucket.pop() + print "test" + for g in self.locusts: + if l == g.args[0]: + #self.locusts.killone(g, block=True) + killbuffer.add(g) + print "greenlet dying: ", g + break + #print "greenlet %s with locust %s dying" % (g, g.args[0]) + gevent.sleep(0) + + def start_hatching(self, locust_count=None, hatch_rate=None, wait=False): print "start hatching", locust_count, hatch_rate, self.state @@ -402,20 +423,20 @@ def start_hatching(self, locust_count=None, hatch_rate=None, wait=False): RequestStats.global_start_time = time() # When dynamically changing the locust count - if self.state != STATE_INIT: + if self.state != STATE_INIT or self.state != STATE_STOPPED: if self.num_clients > locust_count: # Kill some locusts kill_amount = self.num_clients - locust_count self.num_clients = locust_count - kill_locusts(kill_amount) + self.kill_locusts(kill_amount) elif self.num_clients < locust_count: # Spawn some locusts + if hatch_rate: + self.hatch_rate = hatch_rate new_locusts = locust_count - self.num_clients self.num_clients = locust_count self.hatch(new_locusts=new_locusts) - - if hatch_rate: - self.hatch_rate = hatch_rate + else: if locust_count: self.num_clients = locust_count diff --git a/locust/web.py b/locust/web.py index 918ff95821..1878c8fcbb 100644 --- a/locust/web.py +++ b/locust/web.py @@ -63,7 +63,7 @@ def stop(): @app.route('/add') def add(): from core import locust_runner - locust_runner.start_hatching(20, 1) + locust_runner.start_hatching(5, 1) return "hejsan!" @app.route("/stats/requests/csv") From 3d6eceef1fe4fe658129826285115a6324b3429f Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Wed, 31 Aug 2011 13:56:01 +0200 Subject: [PATCH 03/19] kill_locusts now working properly --- locust/core.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/locust/core.py b/locust/core.py index a8a9fc5bdf..700d785d36 100644 --- a/locust/core.py +++ b/locust/core.py @@ -330,7 +330,11 @@ def errors(self): def user_count(self): return len(self.locusts) - def weight_locusts(self, new_locusts, stop_timeout = None): + 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 + """ bucket = [] weight_sum = sum((locust.weight for locust in self.locust_classes)) for locust in self.locust_classes: @@ -345,7 +349,7 @@ def weight_locusts(self, new_locusts, stop_timeout = None): # create locusts depending on weight percent = locust.weight / float(weight_sum) - num_locusts = int(round(new_locusts * percent)) + num_locusts = int(round(amount * percent)) bucket.extend([locust for x in xrange(0, num_locusts)]) return bucket @@ -393,36 +397,35 @@ def start_locust(_): print_percentile_stats(self.request_stats) #TODO use an event listener, or such, for this? def kill_locusts(self, amount): + """ + Kill an amount of locusts from the Group() object in self.locusts + """ bucket = self.weight_locusts(amount) - print "bucket: ", bucket + #print "group: %s \n" % self.locusts + #print "bucket: ", bucket print "killing locusts:", amount - #print "locusts: ", self.locusts - killbuffer = Group() + temp_locusts = self.locusts while True: if not bucket: - print "killbuffer: ", killbuffer - killbuffer.kill(block=True) + #print "remaining %s \n" % temp_locusts return l = bucket.pop() - print "test" - for g in self.locusts: - if l == g.args[0]: - #self.locusts.killone(g, block=True) - killbuffer.add(g) - print "greenlet dying: ", g + #print "poping bucket" + + for g in temp_locusts: + if g.args[0] == l: + self.locusts.killone(g) + temp_locusts.discard(g) break - #print "greenlet %s with locust %s dying" % (g, g.args[0]) gevent.sleep(0) - - 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() - # When dynamically changing the locust count + # Dynamically changing the locust count if self.state != STATE_INIT or self.state != STATE_STOPPED: if self.num_clients > locust_count: # Kill some locusts From 7f6050d1249004f3df82220770f495527784bc48 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Wed, 31 Aug 2011 15:01:31 +0200 Subject: [PATCH 04/19] fixed a bug that made the initial start_hatching always spawn one locust too few --- locust/core.py | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/locust/core.py b/locust/core.py index 700d785d36..c193c029ee 100644 --- a/locust/core.py +++ b/locust/core.py @@ -353,19 +353,22 @@ def weight_locusts(self, amount, stop_timeout = None): bucket.extend([locust for x in xrange(0, num_locusts)]) return bucket - def hatch(self, new_locusts=None, stop_timeout=None, wait=False): - if new_locusts is None: - new_locusts = self.num_clients + def hatch(self, spawn_amount=None, stop_timeout=None, wait=False): + if spawn_amount is None: + spawn_amount = self.num_clients if self.num_requests is not None: RequestStats.global_max_requests = self.num_requests - bucket = self.weight_locusts(new_locusts, stop_timeout) - print "bucket:", bucket, "new_locusts count:", new_locusts + bucket = self.weight_locusts(spawn_amount, stop_timeout) + spawn_amount = len(bucket) + self.num_clients += spawn_amount + print "bucket:", bucket, "new_locusts count:", spawn_amount print "\nHatching and swarming %i clients at the rate %g clients/s...\n" % (self.num_clients, self.hatch_rate) occurence_count = dict([(l.__name__, 0) for l in self.locust_classes]) total_locust_count = len(bucket) + print "total_locust_count:", total_locust_count def spawn_locusts(): sleep_time = 1.0 / self.hatch_rate @@ -396,14 +399,16 @@ def start_locust(_): print_stats(self.request_stats) print_percentile_stats(self.request_stats) #TODO use an event listener, or such, for this? - def kill_locusts(self, amount): + def kill_locusts(self, kill_amount): """ - Kill an amount of locusts from the Group() object in self.locusts + Kill an kill_amount of locusts from the Group() object in self.locusts """ - bucket = self.weight_locusts(amount) + bucket = self.weight_locusts(kill_amount) + kill_amount = len(bucket) + self.num_clients -= kill_amount #print "group: %s \n" % self.locusts #print "bucket: ", bucket - print "killing locusts:", amount + print "killing locusts:", kill_amount temp_locusts = self.locusts while True: if not bucket: @@ -425,21 +430,21 @@ def start_hatching(self, locust_count=None, hatch_rate=None, wait=False): RequestStats.clear_all() RequestStats.global_start_time = time() + # Dynamically changing the locust count - if self.state != STATE_INIT or self.state != STATE_STOPPED: + if self.state != STATE_INIT and self.state != STATE_STOPPED: if self.num_clients > locust_count: # Kill some locusts - kill_amount = self.num_clients - locust_count - self.num_clients = locust_count + kill_amount = self.num_clients - (self.num_clients - locust_count) + #self.num_clients = locust_count self.kill_locusts(kill_amount) elif self.num_clients < locust_count: # Spawn some locusts if hatch_rate: self.hatch_rate = hatch_rate - new_locusts = locust_count - self.num_clients - self.num_clients = locust_count - self.hatch(new_locusts=new_locusts) - + spawn_amount = locust_count - self.num_clients + #self.num_clients = locust_count + self.hatch(spawn_amount=spawn_amount) else: if locust_count: self.num_clients = locust_count From e6d925675b27ae4568942859dad09c593246ab39 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Wed, 31 Aug 2011 15:11:35 +0200 Subject: [PATCH 05/19] removed some debugging prints --- locust/core.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/locust/core.py b/locust/core.py index c193c029ee..1b40408117 100644 --- a/locust/core.py +++ b/locust/core.py @@ -363,12 +363,10 @@ def hatch(self, spawn_amount=None, stop_timeout=None, wait=False): bucket = self.weight_locusts(spawn_amount, stop_timeout) spawn_amount = len(bucket) self.num_clients += spawn_amount - print "bucket:", bucket, "new_locusts count:", spawn_amount print "\nHatching and swarming %i clients at the rate %g clients/s...\n" % (self.num_clients, self.hatch_rate) occurence_count = dict([(l.__name__, 0) for l in self.locust_classes]) total_locust_count = len(bucket) - print "total_locust_count:", total_locust_count def spawn_locusts(): sleep_time = 1.0 / self.hatch_rate From e3af64757060f7bd807ef781a97a0d36b1561128 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Wed, 31 Aug 2011 17:59:30 +0200 Subject: [PATCH 06/19] rewrote kill_locust with better code, added a new msg in the SlaveLocustRunners listener --- locust/core.py | 42 ++++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/locust/core.py b/locust/core.py index 1b40408117..5a838804c9 100644 --- a/locust/core.py +++ b/locust/core.py @@ -362,9 +362,10 @@ def hatch(self, spawn_amount=None, stop_timeout=None, wait=False): bucket = self.weight_locusts(spawn_amount, stop_timeout) spawn_amount = len(bucket) - self.num_clients += spawn_amount - - print "\nHatching and swarming %i clients at the rate %g clients/s...\n" % (self.num_clients, self.hatch_rate) + if self.state != STATE_INIT: + self.num_clients += spawn_amount + self.state = STATE_HATCHING + print "\nHatching and swarming %i clients at the rate %g clients/s...\n" % (spawn_amount, self.hatch_rate) occurence_count = dict([(l.__name__, 0) for l in self.locust_classes]) total_locust_count = len(bucket) @@ -399,28 +400,21 @@ def start_locust(_): def kill_locusts(self, kill_amount): """ - Kill an kill_amount of locusts from the Group() object in self.locusts + Kill a kill_amount of weighted locusts from the Group() object in self.locusts """ bucket = self.weight_locusts(kill_amount) kill_amount = len(bucket) self.num_clients -= kill_amount - #print "group: %s \n" % self.locusts - #print "bucket: ", bucket print "killing locusts:", kill_amount - temp_locusts = self.locusts - while True: - if not bucket: - #print "remaining %s \n" % temp_locusts - return - l = bucket.pop() - #print "poping bucket" - - for g in temp_locusts: - if g.args[0] == l: - self.locusts.killone(g) - temp_locusts.discard(g) + dying = [] + for g in self.locusts: + for l in bucket: + if l == g.args[0]: + dying.append(g) + bucket.remove(l) break - gevent.sleep(0) + for g in dying: + self.locusts.killone(g) def start_hatching(self, locust_count=None, hatch_rate=None, wait=False): print "start hatching", locust_count, hatch_rate, self.state @@ -428,12 +422,11 @@ def start_hatching(self, locust_count=None, hatch_rate=None, wait=False): RequestStats.clear_all() RequestStats.global_start_time = time() - # Dynamically changing the locust count if self.state != STATE_INIT and self.state != STATE_STOPPED: if self.num_clients > locust_count: # Kill some locusts - kill_amount = self.num_clients - (self.num_clients - locust_count) + kill_amount = self.num_clients - locust_count #self.num_clients = locust_count self.kill_locusts(kill_amount) elif self.num_clients < locust_count: @@ -446,10 +439,7 @@ def start_hatching(self, locust_count=None, hatch_rate=None, wait=False): else: if locust_count: self.num_clients = locust_count - self.state = STATE_HATCHING self.hatch(wait=wait) - - def stop(self): # if we are currently hatching locusts we need to kill the hatching greenlet first @@ -604,6 +594,10 @@ def worker(self): self.stop() self.client.send({"type":"client_stopped", "data":self.client_id}) self.client.send({"type":"client_ready", "data":self.client_id}) + elif msg["type"] == "hatch": + locust_count = msg["data"] + self.hatching_greenlet = gevent.spawn(lambda: self.start_hatching(locust_count=locust_count)) + def stats_reporter(self): while True: From 187e12dd32841eed374757053e437854979ec031 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Thu, 1 Sep 2011 14:01:03 +0200 Subject: [PATCH 07/19] renamed a few variables, the self.status is now STATE_HATCHING when killing locusts as well as spawning, extended the Slave msg listener to include dynamic hatching and the Master start_locust to work with dynamic hatching --- locust/core.py | 78 ++++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/locust/core.py b/locust/core.py index 5a838804c9..d1c57e4cc3 100644 --- a/locust/core.py +++ b/locust/core.py @@ -353,28 +353,30 @@ def weight_locusts(self, amount, stop_timeout = None): bucket.extend([locust for x in xrange(0, num_locusts)]) return bucket - def hatch(self, spawn_amount=None, stop_timeout=None, wait=False): - if spawn_amount is None: - spawn_amount = self.num_clients + def hatch(self, spawn_count=None, stop_timeout=None, wait=False): + if spawn_count is None: + spawn_count = self.num_clients if self.num_requests is not None: RequestStats.global_max_requests = self.num_requests - bucket = self.weight_locusts(spawn_amount, stop_timeout) - spawn_amount = len(bucket) - if self.state != STATE_INIT: - self.num_clients += spawn_amount - self.state = STATE_HATCHING - print "\nHatching and swarming %i clients at the rate %g clients/s...\n" % (spawn_amount, self.hatch_rate) + bucket = self.weight_locusts(spawn_count, stop_timeout) + spawn_count = len(bucket) + if self.state == STATE_INIT or self.state == STATE_STOPPED: + self.state = STATE_HATCHING + self.num_clients = spawn_count + else: + self.num_clients += spawn_count + + 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]) - total_locust_count = len(bucket) def spawn_locusts(): sleep_time = 1.0 / self.hatch_rate while True: if not bucket: print "All locusts hatched: %s" % ", ".join(["%s: %d" % (name, count) for name, count in occurence_count.iteritems()]) - events.hatch_complete.fire(total_locust_count) + events.hatch_complete.fire(self.num_clients) return locust = bucket.pop(random.randint(0, len(bucket)-1)) @@ -398,14 +400,14 @@ def start_locust(_): print_stats(self.request_stats) print_percentile_stats(self.request_stats) #TODO use an event listener, or such, for this? - def kill_locusts(self, kill_amount): + def kill_locusts(self, kill_count): """ - Kill a kill_amount of weighted locusts from the Group() object in self.locusts + Kill a kill_count of weighted locusts from the Group() object in self.locusts """ - bucket = self.weight_locusts(kill_amount) - kill_amount = len(bucket) - self.num_clients -= kill_amount - print "killing locusts:", kill_amount + bucket = self.weight_locusts(kill_count) + kill_count = len(bucket) + self.num_clients -= kill_count + print "killing locusts:", kill_count dying = [] for g in self.locusts: for l in bucket: @@ -415,6 +417,7 @@ def kill_locusts(self, kill_amount): break for g in dying: self.locusts.killone(g) + events.hatch_complete.fire(self.num_clients) def start_hatching(self, locust_count=None, hatch_rate=None, wait=False): print "start hatching", locust_count, hatch_rate, self.state @@ -424,22 +427,23 @@ def start_hatching(self, locust_count=None, hatch_rate=None, wait=False): # Dynamically changing the locust count if self.state != STATE_INIT and self.state != STATE_STOPPED: + self.state = STATE_HATCHING if self.num_clients > locust_count: # Kill some locusts - kill_amount = self.num_clients - locust_count - #self.num_clients = locust_count - self.kill_locusts(kill_amount) + kill_count = self.num_clients - locust_count + self.kill_locusts(kill_count) elif self.num_clients < locust_count: # Spawn some locusts if hatch_rate: self.hatch_rate = hatch_rate - spawn_amount = locust_count - self.num_clients - #self.num_clients = locust_count - self.hatch(spawn_amount=spawn_amount) + spawn_count = locust_count - self.num_clients + self.hatch(spawn_count=spawn_count) else: if locust_count: - self.num_clients = locust_count - self.hatch(wait=wait) + self.hatch(locust_count, wait=wait) + else: + self.hatch(wait=wait) + def stop(self): # if we are currently hatching locusts we need to kill the hatching greenlet first @@ -505,12 +509,13 @@ def user_count(self): def start_hatching(self, locust_count=None, hatch_rate=None): if locust_count: - self.num_clients = locust_count / (len(self.clients.ready) or 1) + self.num_clients = locust_count / ((len(self.clients.ready) + len(self.clients.running)) or 1) if hatch_rate: - self.hatch_rate = float(hatch_rate) / (len(self.clients.ready) or 1) - + self.hatch_rate = float(hatch_rate) / ((len(self.clients.ready) + len(self.clients.running)) or 1) + + print "slave job:", self.num_clients, "clients.ready:", len(self.clients.ready)+len(self.clients.running) print "Sending hatch jobs to %i ready clients" % len(self.clients.ready) - if not len(self.clients.ready): + 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." @@ -519,7 +524,7 @@ def start_hatching(self, locust_count=None, hatch_rate=None): for client in self.clients.itervalues(): msg = {"hatch_rate":self.hatch_rate, "num_clients":self.num_clients, "num_requests": self.num_requests, "host":self.host, "stop_timeout":None} - self.server.send({"type":"start", "data":msg}) + self.server.send({"type":"hatch", "data":msg}) RequestStats.global_start_time = time() self.state = STATE_HATCHING @@ -543,7 +548,7 @@ def client_listener(self): elif msg["type"] == "stats": report = msg["data"] events.slave_report.fire(report["client_id"], report["data"]) - elif msg["type"] == "running": + elif msg["type"] == "hatching": id = msg["data"] self.clients[id].state = STATE_HATCHING elif msg["type"] == "hatch_complete": @@ -582,21 +587,18 @@ def on_report_to_master(client_id, data): def worker(self): while True: msg = self.client.recv() - if msg["type"] == "start": - self.client.send({"type":"running", "data":self.client_id}) + if msg["type"] == "hatch": + self.client.send({"type":"hatching", "data":self.client_id}) job = msg["data"] self.hatch_rate = job["hatch_rate"] - self.num_clients = job["num_clients"] + #self.num_clients = job["num_clients"] self.num_requests = job["num_requests"] self.host = job["host"] - self.hatching_greenlet = gevent.spawn(lambda: self.start_hatching()) + self.hatching_greenlet = gevent.spawn(lambda: self.start_hatching(locust_count=job["num_clients"], hatch_rate=job["hatch_rate"])) elif msg["type"] == "stop": self.stop() self.client.send({"type":"client_stopped", "data":self.client_id}) self.client.send({"type":"client_ready", "data":self.client_id}) - elif msg["type"] == "hatch": - locust_count = msg["data"] - self.hatching_greenlet = gevent.spawn(lambda: self.start_hatching(locust_count=locust_count)) def stats_reporter(self): From f95c1daad8ea6848f746ceee7f907b96b232d85a Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Thu, 1 Sep 2011 18:09:12 +0200 Subject: [PATCH 08/19] removed a function I used for testing --- locust/web.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/locust/web.py b/locust/web.py index 1878c8fcbb..644b1722e9 100644 --- a/locust/web.py +++ b/locust/web.py @@ -59,13 +59,6 @@ def stop(): response.headers["Content-type"] = "application/json" return response -#Adding locust -@app.route('/add') -def add(): - from core import locust_runner - locust_runner.start_hatching(5, 1) - return "hejsan!" - @app.route("/stats/requests/csv") def request_stats_csv(): from core import locust_runner From 9c495c2c2bccebbefa27e4487124fbcc483bbbcd Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Thu, 1 Sep 2011 18:11:25 +0200 Subject: [PATCH 09/19] Have added an edit form to the interface for changing number of locusts, TODO: issue with status not updating after posting swarm_form --- locust/static/style.css | 32 ++++++++++++++++++++++++-------- locust/templates/index.html | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/locust/static/style.css b/locust/static/style.css index 7063275b33..2df06cbb17 100644 --- a/locust/static/style.css +++ b/locust/static/style.css @@ -86,29 +86,29 @@ a { padding-top: 20px; } -.start { +.start, .edit { width: 398px; position: absolute; left: 50%; top: 100px; margin-left: -169px; } -.start .padder { +.start .padder, .edit .padder { padding: 30px; padding-top: 0px; } -.start h2 { +.start h2, .edit h2 { color: #addf82; font-size: 26px; font-weight: bold; } -.start label { +.start label, .edit label { display: block; margin-bottom: 10px; margin-top: 20px; font-size: 16px; } -.start input.val { +.start input.val, .edit input.val { border: none; background: #fff; height: 52px; @@ -116,7 +116,7 @@ a { font-size: 24px; padding-left: 10px; } -.start input.start_button { +.start input.start_button, .edit input.start_button { margin-top: 20px; float: right; } @@ -130,20 +130,33 @@ a { border: 3px solid #eee; background: #111717; } + +.stopped .edit {display: none;} +.running .edit, .hatching .edit { + display: none; + border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radisu: 5px; + border: 3px solid #eee; + background: #111717; +} .running .start, .hatching .start {display: none;} + +.ready .edit {display: none;} .ready .start {display: block;} .running .status, .hatching .status {display: block;} -.stopped .status {display: blocked;} +.stopped .status {display: block;} .ready .status {display: none;} +.stopped .boxes .edit_test, .ready .boxes .edit_test {display: none;} .stopped .boxes .user_count, .ready .boxes .user_count {display: none;} .running a.new_test, .ready a.new_test, .hatching a.new_test {display: none;} .running a.new_test {display: none;} .stopped a.new_test {display: block;} -.start a.close_link { +.start a.close_link, .edit a.close_link{ position: absolute; right: 10px; top: 10px; @@ -151,6 +164,9 @@ a { .stopped .start a.close_link {display: inline;} .running .start a.close_link, .ready .start a.close_link, .hatching .start a.close_link {display: none;} +.stopped .edit a.close_link, .ready .edit a.close_link {display: none;} +.running .edit a.close_link, .hatching .edit a.close_link {display: inline;} + .status table { border-collapse: collapse; width: 100%; diff --git a/locust/templates/index.html b/locust/templates/index.html index f9621abbec..aa71dcf6bc 100644 --- a/locust/templates/index.html +++ b/locust/templates/index.html @@ -18,6 +18,7 @@ {{user_count}} users New test + Edit {% if is_distributed %}
@@ -58,6 +59,23 @@

Start new Locust swarm

+ +
+
+ Close +
+
+

Change the locust count

+
+ +
+ +
+ +
+
+
+
    @@ -169,12 +187,17 @@

    Version

    $("body").attr("class", "stopped"); $(".box_stop").hide(); $("a.new_test").show(); + $("a.edit_test").hide(); $(".user_count").hide(); }); $(".new_test").click(function(event) { event.preventDefault(); $("#start").show(); }); + $(".edit_test").click(function(event) { + event.preventDefault(); + $("#edit").show(); + }); $(".close_link").click(function(event) { event.preventDefault(); $(this).parent().parent().hide(); @@ -196,11 +219,23 @@

    Version

    $("#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(), + function(response) { + if (response.success) { + $("#edit").fadeOut(); + } + } + ); + }); function updateStats() { $.get('/stats/requests', function (data) { From c31138df1642a7c48bfeb53769fb0c69f94703c0 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Fri, 2 Sep 2011 11:37:24 +0200 Subject: [PATCH 10/19] Fixed so that the class of the body is manually set to "hatching" when the swarm_form is submitted, this so that the edit form can be rendered correctly (the html page is rendered before the state is updated) --- locust/templates/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/locust/templates/index.html b/locust/templates/index.html index aa71dcf6bc..455f3b6058 100644 --- a/locust/templates/index.html +++ b/locust/templates/index.html @@ -215,6 +215,7 @@

    Version

    $.post($(this).attr("action"), $(this).serialize(), function(response) { if (response.success) { + $("body").attr("class", "hatching"); $("#start").fadeOut(); $("#status").fadeIn(); $(".box_running").fadeIn(); From 1ab8c9c8d1e9c556bad377e3d65aae57abfbe626 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Fri, 2 Sep 2011 16:07:28 +0200 Subject: [PATCH 11/19] Added a "Reset Stats" button to the interface --- locust/templates/index.html | 10 ++++++++++ locust/web.py | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/locust/templates/index.html b/locust/templates/index.html index 455f3b6058..c02a94fc4d 100644 --- a/locust/templates/index.html +++ b/locust/templates/index.html @@ -37,6 +37,9 @@
    +
@@ -190,6 +193,12 @@

Version

$("a.edit_test").hide(); $(".user_count").hide(); }); + + $("#box_reset a").click(function(event) { + event.preventDefault(); + $.get($(this).attr("href")); + }); + $(".new_test").click(function(event) { event.preventDefault(); $("#start").show(); @@ -232,6 +241,7 @@

Version

$.post($(this).attr("action"), $(this).serialize(), function(response) { if (response.success) { + $("body").attr("class", "hatching"); $("#edit").fadeOut(); } } diff --git a/locust/web.py b/locust/web.py index 644b1722e9..2080a7ee4d 100644 --- a/locust/web.py +++ b/locust/web.py @@ -43,7 +43,7 @@ def index(): def swarm(): assert request.method == "POST" from core import locust_runner - + locust_count = int(request.form["locust_count"]) hatch_rate = float(request.form["hatch_rate"]) locust_runner.start_hatching(locust_count, hatch_rate) @@ -59,6 +59,10 @@ def stop(): response.headers["Content-type"] = "application/json" return response +@app.route("/stats/reset") +def reset_stats(): + RequestStats.reset_all() + @app.route("/stats/requests/csv") def request_stats_csv(): from core import locust_runner From 509ad44ebdca99321857b2def17748dfe58536a2 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Fri, 2 Sep 2011 17:27:35 +0200 Subject: [PATCH 12/19] Added the function one_percentile that returns the percentile for the given percent --- locust/stats.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/locust/stats.py b/locust/stats.py index 140cd5aebd..bd187da588 100644 --- a/locust/stats.py +++ b/locust/stats.py @@ -216,6 +216,9 @@ def create_response_times_list(self): return inflated_list + def one_percentile(self, percent): + return percentile(self.create_response_times_list(), 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 3b34f9d18ab72d9135109568ce7d9808559f6ccb Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Tue, 6 Sep 2011 17:36:07 +0200 Subject: [PATCH 13/19] Added the function start_ramping that ramp up the clients until a boundary is reached. Changed the start_hatching function is the MasterLocustRunner to work with start_ramping --- locust/core.py | 93 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 80 insertions(+), 13 deletions(-) diff --git a/locust/core.py b/locust/core.py index d1c57e4cc3..3778673fed 100644 --- a/locust/core.py +++ b/locust/core.py @@ -298,7 +298,9 @@ def interrupt(self, reschedule=True): locust_runner = None -STATE_INIT, STATE_HATCHING, STATE_RUNNING, STATE_STOPPED = ["ready", "hatching", "running", "stopped"] +STATE_INIT, STATE_HATCHING, STATE_RUNNING, STATE_STOPPED, STATE_RAMPING = ["ready", "hatching", "running", "stopped", "ramping"] +#STATE_INIT, STATE_HATCHING, STATE_RUNNING, STATE_STOPPED, STATE_RAMPING = (0b1, 0b10, 0b100, 0b1000, 0b10000) + class LocustRunner(object): def __init__(self, locust_classes, hatch_rate, num_clients, num_requests=None, host=None): @@ -422,9 +424,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 @@ -444,7 +445,6 @@ def start_hatching(self, locust_count=None, hatch_rate=None, wait=False): else: self.hatch(wait=wait) - 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(): @@ -508,13 +508,18 @@ def user_count(self): return sum([c.user_count for c in self.clients.itervalues()]) def start_hatching(self, locust_count=None, hatch_rate=None): + + if locust_count: + self.num_clients = locust_count + else: + self.num_clients = 0 + if locust_count: - self.num_clients = locust_count / ((len(self.clients.ready) + len(self.clients.running)) or 1) + slave_num_clients = locust_count / ((len(self.clients.ready) + len(self.clients.running)) or 1) if hatch_rate: - self.hatch_rate = float(hatch_rate) / ((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 "slave job:", self.num_clients, "clients.ready:", len(self.clients.ready)+len(self.clients.running) - print "Sending hatch jobs to %i ready clients" % len(self.clients.ready) + 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." @@ -523,12 +528,74 @@ def start_hatching(self, locust_count=None, hatch_rate=None): RequestStats.clear_all() for client in self.clients.itervalues(): - msg = {"hatch_rate":self.hatch_rate, "num_clients":self.num_clients, "num_requests": self.num_requests, "host":self.host, "stop_timeout":None} + 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=None, percent=0.95, response_time=2000, acceptable_fail=0.05): + if hatch_rate: + self.hatch_rate = hatch_rate + + if not hatch_stride: + hatch_stride = 100 + + 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) + print "low_percentile:", percentile + self.start_hatching(1, self.hatch_rate) + return percentile + gevent.sleep(1) + + low_percentile = calibrate() + + 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 * 1.6: + print "ramping stopped due to response times getting high:", p + return + self.start_hatching(clients, self.hatch_rate) + clients += hatch_stride + gevent.sleep(1) + +# 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) +# return +# self.start_hatching(clients, self.hatch_rate) +# clients += 10 * hatchrate +# gevent.sleep(1) + def stop(self): for client in self.clients.hatching + self.clients.running: self.server.send({"type":"stop", "data":{}}) @@ -558,7 +625,7 @@ def client_listener(self): if len(self.clients.hatching) == 0: count = sum(c.user_count for c in self.clients.itervalues()) events.hatch_complete.fire(count) - + @property def slave_count(self): return len(self.clients.ready) + len(self.clients.hatching) + len(self.clients.running) @@ -600,7 +667,7 @@ def worker(self): self.client.send({"type":"client_stopped", "data":self.client_id}) self.client.send({"type":"client_ready", "data":self.client_id}) - + def stats_reporter(self): while True: data = {} From 1b8c4575fbd6f355ac40fe66a6a814f88ca69b47 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Tue, 6 Sep 2011 17:38:35 +0200 Subject: [PATCH 14/19] Added the property fail_ratio that is the ratio of failure requests --- locust/stats.py | 69 ++++++++++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/locust/stats.py b/locust/stats.py index bd187da588..829288c197 100644 --- a/locust/stats.py +++ b/locust/stats.py @@ -11,7 +11,6 @@ class RequestStatsAdditionError(Exception): pass - class RequestStats(object): requests = {} @@ -35,14 +34,14 @@ def clear_all(cls): cls.global_max_requests = None cls.global_last_request_timestamp = None cls.global_start_time = None - + @classmethod def reset_all(cls): cls.total_num_requests = 0 for name, stats in cls.requests.iteritems(): stats.reset() cls.errors = {} - + def reset(self): self.start_time = time.time() self.num_reqs = 0 @@ -60,7 +59,7 @@ def log(self, response_time, content_length): self.num_reqs += 1 self.total_response_time += response_time - + t = int(time.time()) self.num_reqs_per_sec[t] = self.num_reqs_per_sec.setdefault(t, 0) + 1 self.last_request_timestamp = t @@ -68,10 +67,10 @@ def log(self, response_time, content_length): if self._min_response_time is None: self._min_response_time = response_time - + self._min_response_time = min(self._min_response_time, response_time) self.max_response_time = max(self.max_response_time, response_time) - + # to avoid to much data that has to be transfered to the master node when # running in distributed mode, we save the response time rounded in a dict # so that 147 becomes 150, 3432 becomes 3400 and 58760 becomes 59000 @@ -86,34 +85,44 @@ def log(self, response_time, content_length): # increase request count for the rounded key in response time dict self.response_times.setdefault(rounded_response_time, 0) self.response_times[rounded_response_time] += 1 - + # increase total content-length self.total_content_length += content_length - + def log_error(self, error): self.num_failures += 1 key = "%r: %s" % (error, repr(str(error))) RequestStats.errors.setdefault(key, 0) RequestStats.errors[key] += 1 - + + @property + def fail_ratio(self): + try: + return float(self.num_failures) / self.num_reqs + except ZeroDivisionError: + if self.num_failures > 0: + return 1.0 + else: + return 0.0 + @property def min_response_time(self): if self._min_response_time is None: return 0 return self._min_response_time - + @property def avg_response_time(self): try: return float(self.total_response_time) / self.num_reqs except ZeroDivisionError: return 0 - + @property def median_response_time(self): if not self.response_times: return 0 - + def median(total, count): """ total is the number of requests made @@ -124,15 +133,15 @@ def median(total, count): if pos < count[k]: return k pos -= count[k] - + return median(self.num_reqs, self.response_times) - + @property def current_rps(self): if self.global_last_request_timestamp is None: return 0 slice_start_time = max(self.global_last_request_timestamp - 12, int(self.global_start_time or 0)) - + reqs = [self.num_reqs_per_sec.get(t, 0) for t in range(slice_start_time, self.global_last_request_timestamp-2)] return avg(reqs) @@ -140,9 +149,9 @@ def current_rps(self): def total_rps(self): if not RequestStats.global_last_request_timestamp: return 0.0 - + return self.num_reqs / max(RequestStats.global_last_request_timestamp - RequestStats.global_start_time, 1) - + @property def avg_content_length(self): try: @@ -153,34 +162,34 @@ def avg_content_length(self): def __add__(self, other): #if self.name != other.name: # raise RequestStatsAdditionError("Trying to add two RequestStats objects of different names (%s and %s)" % (self.name, other.name)) - + new = RequestStats(self.name) new.last_request_timestamp = max(self.last_request_timestamp, other.last_request_timestamp) new.start_time = min(self.start_time, other.start_time) - + new.num_reqs = self.num_reqs + other.num_reqs new.num_failures = self.num_failures + other.num_failures new.total_response_time = self.total_response_time + other.total_response_time new.max_response_time = max(self.max_response_time, other.max_response_time) new._min_response_time = min(self._min_response_time, other._min_response_time) or other._min_response_time - new.total_content_length = self.total_content_length + other.total_content_length - + new.total_content_length = self.total_content_length + other.total_content_length + def merge_dict_add(d1, d2): """Merge two dicts by adding the values from each dict""" merged = copy(d1) for key in set(d2.keys()): merged[key] = d1.get(key, 0) + d2.get(key, 0) return merged - + new.num_reqs_per_sec = merge_dict_add(self.num_reqs_per_sec, other.num_reqs_per_sec) new.response_times = merge_dict_add(self.response_times, other.response_times) return new - + def get_stripped_report(self): report = copy(self) self.reset() return report - + def to_dict(self): return { 'num_reqs': self.num_reqs, @@ -196,7 +205,7 @@ def __str__(self): fail_percent = (self.num_failures/float(self.num_reqs))*100 except ZeroDivisionError: fail_percent = 0 - + return (" %-" + str(STATS_NAME_WIDTH) + "s %7d %12s %7d %7d %7d | %7d %7.2f") % ( self.name, self.num_reqs, @@ -207,7 +216,7 @@ def __str__(self): self.median_response_time or 0, self.current_rps or 0 ) - + def create_response_times_list(self): inflated_list = [] for response_time, count in self.response_times.iteritems(): @@ -242,7 +251,7 @@ def get(cls, name): request = RequestStats(name) cls.requests[name] = request return request - + @classmethod def sum_stats(cls, name="Total"): stats = RequestStats(name) @@ -278,7 +287,7 @@ def percentile(N, percent, key=lambda x:x): def on_request_success(name, response_time, response): if RequestStats.global_max_requests is not None and RequestStats.total_num_requests >= RequestStats.global_max_requests: raise InterruptLocust("Maximum number of requests reached") - + content_length = int(response.info.getheader("Content-Length") or 0) RequestStats.get(name).log(response_time, content_length) @@ -296,7 +305,7 @@ def on_slave_report(client_id, data): RequestStats.requests[stats.name] = RequestStats(stats.name) RequestStats.requests[stats.name] += stats RequestStats.global_last_request_timestamp = max(RequestStats.global_last_request_timestamp, stats.last_request_timestamp) - + for err_message, err_count in data["errors"].iteritems(): RequestStats.errors[err_message] = RequestStats.errors.setdefault(err_message, 0) + err_count @@ -329,7 +338,7 @@ def print_stats(stats): print "" def print_percentile_stats(stats): - print "Percentage of the requests completed within given times" + print "Percentage of the requests completed within given times" print (" %-" + str(STATS_NAME_WIDTH) + "s %8s %6s %6s %6s %6s %6s %6s %6s %6s %6s") % ('Name', '# reqs', '50%', '66%', '75%', '80%', '90%', '95%', '98%', '99%', '100%') print "-" * (80 + STATS_NAME_WIDTH) complete_list = [] From 87ab70f2cef447bd1055eb551b5fe1d74d77dff2 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Tue, 6 Sep 2011 17:41:03 +0200 Subject: [PATCH 15/19] Added "/ramp" -request to the routing for testing the start_ramping function --- locust/web.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/locust/web.py b/locust/web.py index 2080a7ee4d..fc913472c7 100644 --- a/locust/web.py +++ b/locust/web.py @@ -59,6 +59,12 @@ def stop(): response.headers["Content-type"] = "application/json" return response +@app.route("/ramp") +def ramp(): + from core import locust_runner + locust_runner.start_ramping(20, 2000, 200) + return "ramp" + @app.route("/stats/reset") def reset_stats(): RequestStats.reset_all() From fd2ea3cd120ca43700fb5e40b724870d22b8963a Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Fri, 9 Sep 2011 15:52:22 +0200 Subject: [PATCH 16/19] changed the percentile value for when ramping should stop --- locust/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/core.py b/locust/core.py index 3778673fed..ba7416c1c5 100644 --- a/locust/core.py +++ b/locust/core.py @@ -568,7 +568,7 @@ def calibrate(): 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 * 1.6: + if p >= low_percentile * 2.0: print "ramping stopped due to response times getting high:", p return self.start_hatching(clients, self.hatch_rate) From d55d514702b81d54850ccc8326ce7a56bcb68e66 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Mon, 19 Sep 2011 15:50:59 +0200 Subject: [PATCH 17/19] Removed default arguments to MasterLocustRunner.start_hatching() since the arguments are not optional --- locust/core.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/locust/core.py b/locust/core.py index 44d3ee591d..3c6c24600f 100644 --- a/locust/core.py +++ b/locust/core.py @@ -514,17 +514,10 @@ def on_slave_report(client_id, data): def user_count(self): return sum([c.user_count for c in self.clients.itervalues()]) - def start_hatching(self, locust_count=None, hatch_rate=None): - - if locust_count: - self.num_clients = locust_count - else: - self.num_clients = 0 - - if locust_count: - slave_num_clients = locust_count / ((len(self.clients.ready) + len(self.clients.running)) or 1) - if hatch_rate: - slave_hatch_rate = float(hatch_rate) / ((len(self.clients.ready) + len(self.clients.running)) or 1) + 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)): From bdac404e821dafa3cc3ddc3fb45bd841642cceba Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Mon, 19 Sep 2011 16:00:49 +0200 Subject: [PATCH 18/19] Removed unused function: one_percentile() --- locust/stats.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/locust/stats.py b/locust/stats.py index 4f673f9248..7dc3b32003 100644 --- a/locust/stats.py +++ b/locust/stats.py @@ -214,9 +214,6 @@ def create_response_times_list(self): return inflated_list - def one_percentile(self, percent): - return percentile(self.create_response_times_list(), 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 2f938ce81a11bc2db07ec257b78a5895a3e9dd67 Mon Sep 17 00:00:00 2001 From: Hugo Heyman Date: Mon, 19 Sep 2011 16:28:01 +0200 Subject: [PATCH 19/19] Fixed so that the reset stats web endpoint returns ok instead of nothing which resulted in stacktrace --- locust/web.py | 1 + 1 file changed, 1 insertion(+) diff --git a/locust/web.py b/locust/web.py index dc68dd4654..8a6e4b9988 100644 --- a/locust/web.py +++ b/locust/web.py @@ -71,6 +71,7 @@ def ramp(): @app.route("/stats/reset") def reset_stats(): RequestStats.reset_all() + return "ok" @app.route("/stats/requests/csv") def request_stats_csv():