@ 11d698f
current
#!/usr/bin/env perl

use strict;
use warnings;
use autodie;
use 5.10.1;

use lib 't';
use Util qw( read_file write_file );

use File::Next;
use File::Spec;
use Getopt::Long;
use JSON;
use List::Util qw( any max );
use Term::ANSIColor qw(colored);
use Time::HiRes qw(gettimeofday tv_interval);

my $show_colors;
my $perform_store;
my $perform_clear;
my $test_others;
my $num_iterations = 1;
my $set = 'searching';

my @use_acks;
my @use_acks_matches;
my $perl = $^X;

my %sets = (
    searching => [
        [ 'xxx-no-match' ],
        [ 'foo' ],
        [ 'foo', '-w' ],
        [ 'foo', '-w', '-i' ],
        [ 'foo\w+', '-w' ],
        [ 'foo\w+', '-w', '-i' ],
        [ '\w+der', '-w' ],
        [ '\w+der', '-w', '-i' ],
        [ 'foo\w+', '-C10' ],
        [ 'foo\w+', '-C10', '-i' ],
        [ '\w+der', '-C10' ],
        [ '\w+der', '-C10', '-i' ],
        [ '(set|get)_\w+' ],
        [ '(set|get)_\w+', '-i' ],
    ],
    case => [
        [ 'foo' ],
        [ '[Ff][Oo][Oo]' ],
        [ 'foo', '-i' ],

        [ 'foo\w+' ],
        [ '[Ff][Oo][Oo]\w+' ],
        [ 'foo\w+', '-i' ],

        [ '(set|get)_\w+' ],
        [ '([Ss][Ee][Tt]|[Gg][Ee][Tt])_\w+' ],
        [ '(set|get)_\w+', '-i' ],
    ],
    range => [
        [ 'foo' ],
        [ 'foo', '--range-start=^sub' ],
        [ 'foo', '--range-end=^}' ],
        [ 'foo', '--range-start=^sub', '--range-end=^}' ],
        [ 'foo', '-l' ],
        [ 'foo', '-l', '--range-start=^sub' ],
        [ 'foo', '-l', '--range-end=^}' ],
        [ 'foo', '-l', '--range-start=^sub', '--range-end=^}' ],
    ],
    context => [
        [ 'foo' ],
        [ 'foo', '-A10' ],
        [ 'foo', '-B10' ],
        [ 'foo', '-C10' ],
    ],
    files => [
        [ '-f' ],
        [ '-g', 'hash' ],
        [ 'foo', '-l' ],
        [ 'foo', '-c' ],
        [ 'foo', '-L' ],
        [ 'foo$', '-l' ],
        [ 'foo$', '-c' ],
        [ 'foo$', '-L' ],
        [ 'foo\w+', '-l' ],
        [ 'foo\w+', '-c' ],
        [ 'foo\w+', '-L' ],
        [ 'foo\w+', '-w', '-l' ],
        [ 'foo\w+', '-w', '-c' ],
        [ 'foo\w+', '-w', '-L' ],
    ],
    slow => [
        [ '\w+date', '-w' ],
        [ '\w+date', '-w', '-i' ],
    ],
    types => [
        [ 'foo' ],
        [ 'foo', '--type=perl' ],
        [ 'foo', '--type=noperl' ],
        [ 'foo', '--type=python' ],
        [ 'foo', '--type=nopython' ],
        [ 'foo', '--type=ocaml' ],
        [ 'foo', '--type=noocaml' ],
    ],
);


GetOptions(
    'clear'        => \$perform_clear,
    'store'        => \$perform_store,
    'color'        => \$show_colors,
    'others'       => \$test_others,
    'times=i'      => \$num_iterations,
    'ack=s@'       => \@use_acks,
    'ack-match=s@' => \@use_acks_matches,
    'perl=s'       => \$perl,
    'set=s'        => \$set,
    'head'         => sub { @use_acks = ('HEAD') },
) or die;

my $SOURCE_DIR = shift or die "Must specify a path\n";

my $invocations = $sets{$set} or die "Unknown set $set: Must be one of: ", join( ', ', sort keys %sets ), "\n";
my @invocations = @{$invocations};
push( @{$_}, $SOURCE_DIR ) for @invocations;

if ($perform_clear) {
    unlink('.timings.json');
}

my $json = JSON->new->utf8->pretty;
my $previous_timings;
if ( -e '.timings.json' ) {
    $previous_timings = $json->decode(scalar(read_file('.timings.json')));
}

my @acks = ( 'ack-standalone' );
my $iter = File::Next::files( 'garage' );
while ( my $file = $iter->() ) {
    push( @acks, $file );
}

@acks = grab_versions(@acks);

if ( @use_acks || @use_acks_matches ) {
    foreach my $ack (@acks) {
        my $keep;

        $keep ||= $ack->{'version'} eq 'HEAD';
        $keep ||= $ack->{'version'} eq 'previous';

        $keep ||= any { $ack->{'version'} eq $_ } @use_acks;
        $keep ||= any { $ack->{'version'} =~ /$_/ } @use_acks_matches;

        undef $ack unless $keep;
    }
    @acks = grep { defined } @acks;
}
@acks = sort {
    my ($na,$nb) = map { $_ eq 'HEAD' ? 99 : $_ } ( $a->{'version'}, $b->{'version'} );
    return $na cmp $nb;
} @acks;

# Test grep, ag and ripgrep if we have them.
if ( $test_others ) {
    for my $ackalike ( qw( grep egrep ag rg ) ) {
        for my $dir ( qw( /usr/bin /usr/local/bin ) ) {
            my $path = "$dir/$ackalike";
            if ( -x $path ) {
                my $parms = { path => $path, version => $ackalike };
                if ( $ackalike =~ /grep/ ) {
                    $parms->{extra_args} = [ '-R' ];
                }
                push( @acks, $parms );
            }
        }
    }
}

if ($previous_timings) {
    splice @acks, -1, 0, {
        version => 'previous',
    };
}

say "Testing under Perl $], $^X";
say '';
my $format = create_format(\@invocations, \@acks, $show_colors);
my $header = sprintf $format, '', map { color($_->{'version'})  } @acks;
print $header;
my $dashes = '-' x (length($header) - 1); # -1 for the newline
say $dashes;

my %stored_timings;

my @total_timings;
foreach my $invocation (@invocations) {
    my @timings;

    my $previous_timing;

    my @line_counts;
    foreach my $ack (@acks) {
        my $elapsed;
        my $nlines;

        if ($ack->{'path'}) {
            ($elapsed,$nlines) = time_ack($ack, $invocation, $perl);
        }
        else {
            $elapsed = $previous_timings->{join(' ', 'ack', @$invocation)};
        }
        if (defined $elapsed) {
            $elapsed = sprintf('%.2f', $elapsed);
        }
        push @timings, color($previous_timing, $elapsed);
        $previous_timing = $elapsed if defined $elapsed;
        push( @line_counts, $nlines );

        if ($perform_store && $ack->{'store_timings'}) {
            $stored_timings{join(' ', 'ack', @$invocation)} = $elapsed;
        }
    }

    printf $format, display_invocation( $invocation ), map { $_ // color('x_x') } @timings;
    if ( 0 && !counts_valid( @line_counts ) ) {
        say 'Line counts not valid: ' . join( ', ', map { $_ // 'undef' } @line_counts );
    }

    my $i = 0;
    $total_timings[$i++] += ($_//0) for @timings;
}
say $dashes;
printf $format, 'Total', map { sprintf( '%.2f', $_ ) } @total_timings;

if ($perform_store) {
    write_file('.timings.json', $json->encode(\%stored_timings));
}

exit 0;

sub grab_versions {
    my @acks = @_;

    my @annotated_acks;

    foreach my $ack (@acks) {
        my $version;

        if($ack =~ /standalone/) {
            $version = 'HEAD';
        }
        else {
            my $output = `$^X $ack --noenv --version 2>&1`;
            if ( $output =~ /ack\s+(?<version>\d+[.]\d+(_\d+)?)/ ) {
                $version = $+{'version'};
            }
            elsif ( $output =~ /ack\s+v(?<version>\d+[.]\d+[.]\d+)/ ) {
                $version = $+{'version'};
            }
            else {
                die "UNABLE TO PARSE $output";
            }
        }

        push @annotated_acks, {
            path    => $ack,
            version => $version,
        };

        if($version eq 'HEAD') {
            $annotated_acks[-1]{'store_timings'} = 1;
        }
    }

    return @annotated_acks;
}

sub create_format {
    my ( $invocations, $acks, $show_colors ) = @_;

    my $max_invocation_length = max map { length display_invocation($_) } @{$invocations};

    my @max_version_lengths = (length(color('000.00'))) x @$acks;

    for(0..$#$acks) {
        if(length($acks->[$_]{'version'}) > $max_version_lengths[$_]) {
            $max_version_lengths[$_] = length($acks->[$_]{'version'});
        }
    }

    return join(' :', "%-${max_invocation_length}.${max_invocation_length}s", map {
        "%${_}s"
    } @max_version_lengths) . "\n";
}

sub time_ack {
    my ( $ack, $invocation, $perl ) = @_;

    my @args = ( $ack->{'path'}, @{$invocation} );
    push( @args, @{$ack->{extra_args}} ) if $ack->{extra_args};

    # Only ack has --noenv and is run under Perl.
    if ( $ack->{'path'} =~ /ack/ ) {
        @args = ( $perl, @args, '--noenv' );
    }

    if ( $ack->{'path'} =~ /ack-1/ ) {
        @args = grep { !/--known/ } @args;
    }

    my $end;
    my $start = [gettimeofday()];

    # We use the last invocations
    my $n_errlines;
    my $n_stdoutlines;

    for ( 1 .. $num_iterations ) {
        my ( $read, $write );
        pipe $read, $write;

        my ( $r_stdout, $w_stdout );
        pipe $r_stdout, $w_stdout;

        $n_stdoutlines = 0;
        $n_errlines = 0;

        my $pid   = fork;

        if($pid) {
            close $write;
            close $w_stdout;
            while (<$r_stdout>) {
                ++$n_stdoutlines;
            }
            while(<$read>) {
                ++$n_errlines;
            }
            waitpid $pid, 0;
            return if $n_errlines;
        }
        else {
            close $read;
            close $r_stdout;
            open STDOUT, '>&', $w_stdout;
            open STDERR, '>&', $write;
            exec @args;
            exit 255;
        }
    }
    $end = [gettimeofday()];

    my $time = tv_interval($start, $end) / $num_iterations;

    return ( $time, $n_stdoutlines, $n_errlines );
}

sub color {
    my ( $previous_value, $value );

    if ( @_ == 2 ) {
        ( $previous_value, $value ) = @_;
    }
    else {
        ( $value ) = @_;
    }

    return $value if !$show_colors;
    return $value if !defined($value);

    return colored(['white'], $value) if !defined($previous_value);

    if ( $previous_value < $value ) {
        return colored(['red'], $value);
    }
    else {
        return colored(['green'], $value);
    }
}

sub counts_valid {
    my @counts;
    my %counts;

    ++$counts{$_//'undef'} for @_;

    return (@counts>0) && ($counts[0]>0) && (keys %counts == 1);
}


sub display_invocation {
    my $invocation = shift;

    my @args = @{$invocation};
    pop @args;  # Don't show the directory.

    return join( ' ', 'ack', @args );
}

__DATA__

TODO:

  * Percentage slowdown per invocation
  * Overall stats dump at the end.
  * Stop passing bad options to 1.x (--known)