The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

#!perl -w
#
# harness-timer-report.pl
#
# - read in the time-annotated outputs of
# "env HARNESS_TIMER=1 make test" or
# "make test_harness"
# - convert the milliseconds to seconds
# - compute a couple of derived values
# - cpu: the sum of 'self' and 'kids'
# - ratio of the wallclock and the cpu
# - optionally show header, the sum, or the max of each colum
# - sort the rows in various ways
# - default ordering by 'cpu' seconds
# - optionally scale the column values by either the sum or the max
# - optionally display only rows that have rows of at least / at most a limit
#
# The --sort option has a few canned sorting rules. If those are
# not to your liking, there is always sort(1).
#
# Example usages:
#
# perl harness-timer-report.pl log
# perl harness-timer-report.pl --sort=wall log
# perl harness-timer-report.pl --scale=sum log
# perl harness-timer-report.pl --scale=sum --min=0.01 log
# perl harness-timer-report.pl --show=header,max,sum log
# perl harness-timer-report.pl --min=wall=10 log
use strict;
use File::Basename qw[basename];
our $ME = basename($0);
sub usage {
die <<__EOF__;
$ME: Usage:
$ME [--scale=[sum|max]]
[--sort=[cpu|wall|ratio|self|kids|test|name]]
[--show=header,sum,max]
[--min=[[cpu|wall|ratio|self|kids]=value,...]]
[--max=[[cpu|wall|ratio|self|kids]=value,...]]
[--order]
[--help|--usage]
[logfile]
The --order includes the original test order as the last column.
The logfile default is STDIN.
__EOF__
}
my %Opt;
usage()
unless
GetOptions(
'scale=s' => \$Opt{scale},
'sort=s' => \$Opt{sort},
'show=s' => \$Opt{show},
'min=s' => \$Opt{min},
'max=s' => \$Opt{max},
'order' => \$Opt{order},
'help|usage' => \$Opt{help},
);
usage() if $Opt{help};
my %SHOW;
if (defined $Opt{show}) {
for my $s (split(/,/, $Opt{show})) {
if ($s =~ /^(header|sum|max)$/) {
$SHOW{$s}++;
} else {
die "$ME: Unexpected --show='$s'\n";
}
}
}
my %MIN;
if (defined $Opt{min}) {
for my $s (split(/,/, $Opt{min})) {
if ($s =~ /^(wall|cpu|kids|self|ratio)=(\d+(?:\.\d+)?)$/) {
$MIN{$1} = $2;
} else {
die "$ME: Unexpected --min='$s'\n";
}
}
}
my %MAX;
if (defined $Opt{max}) {
for my $s (split(/,/, $Opt{max})) {
if ($s =~ /^(wall|cpu|kids|self|ratio)=(\d+(?:\.\d+)?)$/) {
$MAX{$1} = $2;
} else {
die "$ME: Unexpected --max='$s'\n";
}
}
}
use List::Util qw[max];
my ($sa, $sb, $sc, $sd, $se);
my ($ma, $mb, $mc, $md, $me);
my $logfn;
my $logfh;
if (@ARGV == 1) {
$logfn = $ARGV[0];
open($logfh, "<", $logfn) or die "$ME: Failed to open logfn: $logfn\n";
} elsif (@ARGV == 0) {
$logfn = "-";
$logfh = *STDIN;
} else {
die "$ME: Unexpected logfile arguments: @ARGV\n";
}
my $order = 0;
my @t;
while (<$logfh>) {
my ($test, $wall, $self, $kids);
# Output of "env HARNESS_TIMER=1 make test":
# t/re/pat ....................................................... ok 2876 ms 2660 ms 210 ms
if (m{^#\s+(\S+)\s+\.+\s+ok\s+(\d+)\s+ms\s+(\d+)\s+ms\s+(\d+)\s+ms$}) {
($test, $wall, $self, $kids) = ($1, $2, $3, $4);
}
# Output of "env HARNESS_TIMER=1 make test_harness":
# [08:26:11] base/cond.t ........................................................ ok 2 ms ( 0.00 usr 0.00 sys + 0.00 cusr 0.00 csys = 0.00 CPU)
if (m{^\[.+?\]+\s+(\S+)\s+\.+\s+ok\s+(\d+)\s+ms\s+\(\s*(\d+\.\d+)\s+usr\s+\s+(\d+\.\d+)\s+sys\s+\+\s+(\d+\.\d+)\s+cusr\s+(\d+\.\d+)\s+csys\s+=\s+(\d+\.\d+)\s+CPU\)}) {
$test = $1;
$wall = $2;
$self = $3 + $4;
$kids = $5 + $6;
$test =~ s{^\.\./}{}; # "../lib/foo" -> "../lib/foo"
}
next unless defined $test && defined $wall && $wall > 0;
# Milliseconds to seconds.
$wall /= 1000;
$self /= 1000;
$kids /= 1000;
my $cpu = $self + $kids;
my $ratio = $cpu / $wall;
push @t, [ $test, $wall, $self, $kids, $cpu, $ratio, $order++ ];
$sa += $wall;
$sb += $self;
$sc += $kids;
$sd += $cpu;
$ma = max($wall, $ma // $wall);
$mb = max($self, $mb // $self);
$mc = max($kids, $mc // $kids);
$md = max($cpu, $md // $cpu);
$me = max($ratio, $md // $ratio);
}
die "$ME: No input detected in '$logfn'\n" unless @t;
# Compute the sum for the ratio only after the loop.
$se = $sd / $sa;
my %SORTER =
(
'cpu' =>
sub { $b->[4] <=> $a->[4] ||
$b->[1] <=> $a->[1] ||
$a->[0] cmp $b->[0] },
'wall' =>
sub { $b->[1] <=> $a->[1] ||
$b->[4] <=> $a->[4] ||
$a->[0] cmp $b->[0] },
'ratio' =>
sub { $b->[5] <=> $a->[5] ||
$b->[4] <=> $a->[4] ||
$b->[1] <=> $a->[1] ||
$a->[0] cmp $b->[0] },
'self' =>
sub { $b->[2] <=> $a->[2] ||
$b->[3] <=> $a->[3] ||
$a->[0] cmp $b->[0] },
'kids' =>
sub { $b->[3] <=> $a->[3] ||
$b->[2] <=> $a->[2] ||
$a->[0] cmp $b->[0] },
'test' =>
sub { $a->[6] <=> $b->[6] },
'name' =>
sub { $a->[0] cmp $b->[0] },
);
my $sorter;
$Opt{sort} //= 'cpu';
die "$ME: Unexpected --sort='$Opt{sort}'\n"
unless defined $SORTER{$Opt{sort}};
@t = sort { $SORTER{$Opt{sort}}->() } @t;
if (defined $Opt{scale}) {
my ($ta, $tb, $tc, $td, $te) =
$Opt{scale} eq 'sum' ?
($sa, $sb, $sc, $sd, $se) :
$Opt{scale} eq 'max' ?
($ma, $mb, $mc, $md, $me) :
die "$ME: Unexpected --scale='$Opt{scale}'";
my @u;
for my $t (@t) {
push @u, [ $t->[0],
$t->[1] / $ta, $t->[2] / $tb,
$t->[3] / $tc, $t->[4] / $td,
$t->[5] / $te, $t->[6] ];
}
@t = @u;
}
if ($SHOW{header}) {
my @header = qw[TEST WALL SELF KIDS CPU RATIO];
if ($Opt{order}) {
push @header, 'ORDER';
}
print join(" ", @header), "\n";
}
if ($SHOW{sum}) {
print join(" ", "SUM",
map { sprintf("%.6f", $_) } $sa, $sb, $sc, $sd, $se),
"\n";
}
if ($SHOW{max}) {
print join(" ", "MAX",
map { sprintf("%.6f", $_) } $ma, $mb, $mc, $md, $me),
"\n";
}
my %N2I = (wall => 1,
self => 2,
kids => 3,
cpu => 4,
ratio => 5);
sub row_is_skippable {
my ($t) = @_;
if (scalar keys %MIN) {
for my $k (grep { exists $MIN{$_} } keys %N2I) {
if ($t->[$N2I{$k}] < $MIN{$k}) {
return 1;
}
}
}
if (scalar keys %MAX) {
for my $k (grep { exists $MAX{$_} } keys %N2I) {
if ($t->[$N2I{$k}] > $MAX{$k}) {
return 1;
}
}
}
return 0;
}
for my $t (@t) {
next if row_is_skippable($t);
my $out = sprintf("%s %.6f %.6f %.6f %.6f %.6f",
$t->[0], $t->[1], $t->[2], $t->[3], $t->[4], $t->[5]);
if ($Opt{order}) {
$out .= " $t->[6]";
}
print $out, "\n";
}
exit(0);