Perl6 load testing

Tags:

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}";
    }
}