Perl6 load testing
I had a need to do some testing of concurrent requests against and service and as is my way I reached for Perl6. After whipping up a quick single line script to do the testing I thought I might want to make something a bit more usable.
I'll run through the code in sections. Firstly we have a bit of boiler plate to use Perl6, allow command lines arguments in any order and load the HTTP::UserAgent module. I picked this one because it's lightweight and simple to use for what I wanted to do.
#!/usr/bin/env perl6
use v6;
use HTTP::UserAgent;
my %*SUB-MAIN-OPTS = :named-anywhere;
With that out of the way I like to use a MAIN
sub for all my command line apps and use the built in self documentation comments with it
#| Makes a number of concurrent requests to a given URL and lists the time it takes to load
#| Times are given in the order requests are started.
sub MAIN (
Str $url, #= Url to request
Int $count = 10, #= Number of requests to make (Default 10)
Int :r(:$ramp-up) = $count, #= Ramp up increment (Note: if it's not a divisor of count the final count will be higher).
Bool :s(:$summarize) = $count > 20 ?? True !! False, #= Summarize the results. Defaults to true if count > 20.
) {
...
}
If you run this code without any arguments you get the automatically generated usage text.
Usage:
./load-tester.pl6 <url> [<count>] [-r|--ramp-up=<Int>] [-s|--summarize] -- Makes a number of concurrent requests to a given URL and lists the time it takes to load Times are given in the order requests are started.
<url> Url to request
[<count>] Number of requests to make (Default 10)
-r|--ramp-up=<Int> Ramp up increment (Note: if it's not a divisor of count the final count will be higher).
-s|--summarize Summarize the results. Defaults to true if count > 20.
A couple of notes on the arguments. The $count
value has a default value, the $ramp-up
value defaults to be the same as $count
so you have no ramp up.
The defaulting of $summarize
is fun, it looks at the value of $count
and bases itself off that. By putting all this logic in the arguments when you enter the subroutine you don't have to waste a bunch of time checking values and setting defaults.
Inside our MAIN
sub we firstly set up our code for loading urls. This is nice and simple.
my $ua = HTTP::UserAgent.new;
my sub time-load() {
my $resp = $ua.get($url);
return now - ENTER now;
}
Note that the time-load
subroutine is lexically scoped using my
to live within the MAIN
subroutine. As such it has access to the newly created $ua
object and the $url
passed in. It's a local named closure we can use to make requests.
The ENTER now
part is a Phaser, the call to now
is triggered when the subroutine is entered and the value got is used later when the calculation is reached. This way our time function returns the amount of time it took
to make the request.
With that we just need to call the function. Of course to load test we want to fire up a bunch of concurrent requests and then wait for them all.
First we want a sequence of number of requests to make which will either start with $count
or start at $ramp-up
and increase up until count.
my @counts = $ramp-up,*+$ramp-up ... $count <= *;
for @counts -> $current {
...
}
Then inside our loop we want to create $current
request concurrently, wait for them all the finish and report on the result.
my @p;
say "$current requests";
for (^$current) {
@p.push( start time-load() );
}
await @p;
@p = @p.map( *.result );
if ! $summarize { .say for @p }
say "Min {@p.min}";
say "Max {@p.max}";
Here we make an array @p
then use start call our time-load
closure as a Promise then we pass this to
await which blocks until all the promises in the array have completed.
Then we get the result and display it. Simple really. The initial commandline version took about a minute to make then I decided to tidy it up into a script as I wanted to use it again. All in about 15 minutes. Things I may look at adding include keeping the load up for a bit maybe using channels and adding some error checking. But for me needs to check a theory I had about a server and it's time properties under load this did the job nicely. Perl6's ability to get a lot done in a little bit of code never ceases to amaze me.
Here's the complete script if you'd like it :
#!/usr/bin/env perl6
use v6;
use HTTP::UserAgent;
my %*SUB-MAIN-OPTS = :named-anywhere;
#| Makes a number of concurrent requests to a given URL and lists the time it takes to load
#| Times are given in the order requests are started.
sub MAIN (
Str $url, #= Url to request
Int $count = 10, #= Number of requests to make (Default 10)
Int :r(:$ramp-up) = $count, #= Ramp up increment (Note: if it's not a divisor of count the final count will be higher).
Bool :s(:$summarize) = $count > 20 ?? True !! False, #= Summarize the results. Defaults to true if count > 20.
) {
my $ua = HTTP::UserAgent.new;
my sub time-load() {
my $resp = $ua.get($url);
return now - ENTER now;
}
my @counts = $ramp-up,*+$ramp-up ... $count <= *;
for @counts -> $current {
my @p;
say "$current requests";
for (^$current) {
@p.push( start time-load() );
}
await @p;
@p = @p.map( *.result );
if ! $summarize { .say for @p }
say "Min {@p.min}";
say "Max {@p.max}";
}
}