1
0
Fork 0
mirror of https://github.com/git/git.git synced 2024-06-10 07:36:10 +02:00
git/t/perf/aggregate.perl
Jeff King b8dcc45387 perf-lib: use a single filename for all measurement types
The perf tests write files recording the results of tests.  These
results are later aggregated by 'aggregate.perl'.  If the tests are run
multiple times, those results are overwritten by the new results.  This
works just fine as long as there are only perf tests measuring the
times, whose results are stored in "$base".times files.

However 22bec79d1a ("t/perf: add infrastructure for measuring sizes",
2018-08-17) introduced a new type of test for measuring the size of
something.  The results of this are written to "$base".size files.

"$base" is essentially made up of the basename of the script plus the
test number.  So if test numbers shift because a new test was
introduced earlier in the script we might end up with both a ".times"
and a ".size" file for the same test.  In the aggregation script the
".times" file is preferred over the ".size" file, so some size tests
might end with performance numbers from a previous run of the test.

This is mainly relevant when writing perf tests that check both
performance and sizes, and can get quite confusing during
developement.

We could fix this by doing a more thorough job of cleaning out old
".times" and ".size" files before running each test. However, an even
easier solution is to just use the same filename for both types of
measurement, meaning we'll always overwrite the previous result. We
don't even need to change the file format to distinguish the two;
aggregate.perl already decides which is which based on a regex of the
content (this may become ambiguous if we add new types in the future,
but we could easily add a header field to the file at that point).

Based on an initial patch from Thomas Gummerer, who discovered the
problem and did all of the analysis (which I stole for the commit
message above):

  https://public-inbox.org/git/20191119185047.8550-1-t.gummerer@gmail.com/

Helped-by: Thomas Gummerer <t.gummerer@gmail.com>
Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2019-11-27 10:48:25 +09:00

352 lines
8.2 KiB
Perl
Executable File

#!/usr/bin/perl
use lib '../../perl/build/lib';
use strict;
use warnings;
use Getopt::Long;
use Git;
use Cwd qw(realpath);
sub get_times {
my $name = shift;
open my $fh, "<", $name or return undef;
my $line = <$fh>;
return undef if not defined $line;
close $fh or die "cannot close $name: $!";
# times
if ($line =~ /^(?:(\d+):)?(\d+):(\d+(?:\.\d+)?) (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)$/) {
my $rt = ((defined $1 ? $1 : 0.0)*60+$2)*60+$3;
return ($rt, $4, $5);
# size
} elsif ($line =~ /^\d+$/) {
return $&;
} else {
die "bad input line: $line";
}
}
sub relative_change {
my ($r, $firstr) = @_;
if ($firstr > 0) {
return sprintf "%+.1f%%", 100.0*($r-$firstr)/$firstr;
} elsif ($r == 0) {
return "=";
} else {
return "+inf";
}
}
sub format_times {
my ($r, $u, $s, $firstr) = @_;
# no value means we did not finish the test
if (!defined $r) {
return "<missing>";
}
# a single value means we have a size, not times
if (!defined $u) {
return format_size($r, $firstr);
}
# otherwise, we have real/user/system times
my $out = sprintf "%.2f(%.2f+%.2f)", $r, $u, $s;
$out .= ' ' . relative_change($r, $firstr) if defined $firstr;
return $out;
}
sub usage {
print <<EOT;
./aggregate.perl [options] [--] [<dir_or_rev>...] [--] [<test_script>...] >
Options:
--codespeed * Format output for Codespeed
--reponame <str> * Send given reponame to codespeed
--sort-by <str> * Sort output (only "regression" criteria is supported)
--subsection <str> * Use results from given subsection
EOT
exit(1);
}
sub human_size {
my $n = shift;
my @units = ('', qw(K M G));
while ($n > 900 && @units > 1) {
$n /= 1000;
shift @units;
}
return $n unless length $units[0];
return sprintf '%.1f%s', $n, $units[0];
}
sub format_size {
my ($size, $first) = @_;
# match the width of a time: 0.00(0.00+0.00)
my $out = sprintf '%15s', human_size($size);
$out .= ' ' . relative_change($size, $first) if defined $first;
return $out;
}
my (@dirs, %dirnames, %dirabbrevs, %prefixes, @tests,
$codespeed, $sortby, $subsection, $reponame);
Getopt::Long::Configure qw/ require_order /;
my $rc = GetOptions("codespeed" => \$codespeed,
"reponame=s" => \$reponame,
"sort-by=s" => \$sortby,
"subsection=s" => \$subsection);
usage() unless $rc;
while (scalar @ARGV) {
my $arg = $ARGV[0];
my $dir;
my $prefix = '';
last if -f $arg or $arg eq "--";
if (! -d $arg) {
my $rev = Git::command_oneline(qw(rev-parse --verify), $arg);
$dir = "build/".$rev;
} elsif ($arg eq '.') {
$dir = '.';
} else {
$dir = realpath($arg);
$dirnames{$dir} = $dir;
$prefix .= 'bindir';
}
push @dirs, $dir;
$dirnames{$dir} ||= $arg;
$prefix .= $dir;
$prefix =~ tr/^a-zA-Z0-9/_/c;
$prefixes{$dir} = $prefix . '.';
shift @ARGV;
}
if (not @dirs) {
@dirs = ('.');
}
$dirnames{'.'} = $dirabbrevs{'.'} = "this tree";
$prefixes{'.'} = '';
shift @ARGV if scalar @ARGV and $ARGV[0] eq "--";
@tests = @ARGV;
if (not @tests) {
@tests = glob "p????-*.sh";
}
my $resultsdir = "test-results";
if (! $subsection and
exists $ENV{GIT_PERF_SUBSECTION} and
$ENV{GIT_PERF_SUBSECTION} ne "") {
$subsection = $ENV{GIT_PERF_SUBSECTION};
}
if ($subsection) {
$resultsdir .= "/" . $subsection;
}
my @subtests;
my %shorttests;
for my $t (@tests) {
$t =~ s{(?:.*/)?(p(\d+)-[^/]+)\.sh$}{$1} or die "bad test name: $t";
my $n = $2;
my $fname = "$resultsdir/$t.subtests";
open my $fp, "<", $fname or die "cannot open $fname: $!";
for (<$fp>) {
chomp;
/^(\d+)$/ or die "malformed subtest line: $_";
push @subtests, "$t.$1";
$shorttests{"$t.$1"} = "$n.$1";
}
close $fp or die "cannot close $fname: $!";
}
sub read_descr {
my $name = shift;
open my $fh, "<", $name or return "<error reading description>";
binmode $fh, ":utf8" or die "PANIC on binmode: $!";
my $line = <$fh>;
close $fh or die "cannot close $name";
chomp $line;
return $line;
}
sub have_duplicate {
my %seen;
for (@_) {
return 1 if exists $seen{$_};
$seen{$_} = 1;
}
return 0;
}
sub have_slash {
for (@_) {
return 1 if m{/};
}
return 0;
}
sub display_dir {
my ($d) = @_;
return exists $dirabbrevs{$d} ? $dirabbrevs{$d} : $dirnames{$d};
}
sub print_default_results {
my %descrs;
my $descrlen = 4; # "Test"
for my $t (@subtests) {
$descrs{$t} = $shorttests{$t}.": ".read_descr("$resultsdir/$t.descr");
$descrlen = length $descrs{$t} if length $descrs{$t}>$descrlen;
}
my %newdirabbrevs = %dirabbrevs;
while (!have_duplicate(values %newdirabbrevs)) {
%dirabbrevs = %newdirabbrevs;
last if !have_slash(values %dirabbrevs);
%newdirabbrevs = %dirabbrevs;
for (values %newdirabbrevs) {
s{^[^/]*/}{};
}
}
my %times;
my @colwidth = ((0)x@dirs);
for my $i (0..$#dirs) {
my $w = length display_dir($dirs[$i]);
$colwidth[$i] = $w if $w > $colwidth[$i];
}
for my $t (@subtests) {
my $firstr;
for my $i (0..$#dirs) {
my $d = $dirs[$i];
my $base = "$resultsdir/$prefixes{$d}$t";
$times{$prefixes{$d}.$t} = [get_times("$base.result")];
my ($r,$u,$s) = @{$times{$prefixes{$d}.$t}};
my $w = length format_times($r,$u,$s,$firstr);
$colwidth[$i] = $w if $w > $colwidth[$i];
$firstr = $r unless defined $firstr;
}
}
my $totalwidth = 3*@dirs+$descrlen;
$totalwidth += $_ for (@colwidth);
printf "%-${descrlen}s", "Test";
for my $i (0..$#dirs) {
printf " %-$colwidth[$i]s", display_dir($dirs[$i]);
}
print "\n";
print "-"x$totalwidth, "\n";
for my $t (@subtests) {
printf "%-${descrlen}s", $descrs{$t};
my $firstr;
for my $i (0..$#dirs) {
my $d = $dirs[$i];
my ($r,$u,$s) = @{$times{$prefixes{$d}.$t}};
printf " %-$colwidth[$i]s", format_times($r,$u,$s,$firstr);
$firstr = $r unless defined $firstr;
}
print "\n";
}
}
sub print_sorted_results {
my ($sortby) = @_;
if ($sortby ne "regression") {
print "Only 'regression' is supported as '--sort-by' argument\n";
usage();
}
my @evolutions;
for my $t (@subtests) {
my ($prevr, $prevu, $prevs, $prevrev);
for my $i (0..$#dirs) {
my $d = $dirs[$i];
my ($r, $u, $s) = get_times("$resultsdir/$prefixes{$d}$t.result");
if ($i > 0 and defined $r and defined $prevr and $prevr > 0) {
my $percent = 100.0 * ($r - $prevr) / $prevr;
push @evolutions, { "percent" => $percent,
"test" => $t,
"prevrev" => $prevrev,
"rev" => $d,
"prevr" => $prevr,
"r" => $r,
"prevu" => $prevu,
"u" => $u,
"prevs" => $prevs,
"s" => $s};
}
($prevr, $prevu, $prevs, $prevrev) = ($r, $u, $s, $d);
}
}
my @sorted_evolutions = sort { $b->{percent} <=> $a->{percent} } @evolutions;
for my $e (@sorted_evolutions) {
printf "%+.1f%%", $e->{percent};
print " " . $e->{test};
print " " . format_times($e->{prevr}, $e->{prevu}, $e->{prevs});
print " " . format_times($e->{r}, $e->{u}, $e->{s});
print " " . display_dir($e->{prevrev});
print " " . display_dir($e->{rev});
print "\n";
}
}
sub print_codespeed_results {
my ($subsection) = @_;
my $project = "Git";
my $executable = `uname -s -m`;
chomp $executable;
if ($subsection) {
$executable .= ", " . $subsection;
}
my $environment;
if ($reponame) {
$environment = $reponame;
} elsif (exists $ENV{GIT_PERF_REPO_NAME} and $ENV{GIT_PERF_REPO_NAME} ne "") {
$environment = $ENV{GIT_PERF_REPO_NAME};
} else {
$environment = `uname -r`;
chomp $environment;
}
my @data;
for my $t (@subtests) {
for my $d (@dirs) {
my $commitid = $prefixes{$d};
$commitid =~ s/^build_//;
$commitid =~ s/\.$//;
my ($result_value, $u, $s) = get_times("$resultsdir/$prefixes{$d}$t.result");
my %vals = (
"commitid" => $commitid,
"project" => $project,
"branch" => $dirnames{$d},
"executable" => $executable,
"benchmark" => $shorttests{$t} . " " . read_descr("$resultsdir/$t.descr"),
"environment" => $environment,
"result_value" => $result_value,
);
push @data, \%vals;
}
}
require JSON;
print JSON::to_json(\@data, {utf8 => 1, pretty => 1, canonical => 1}), "\n";
}
binmode STDOUT, ":utf8" or die "PANIC on binmode: $!";
if ($codespeed) {
print_codespeed_results($subsection);
} elsif (defined $sortby) {
print_sorted_results($sortby);
} else {
print_default_results();
}