File Coverage

bench.PL
Criterion Covered Total %
statement 1 1 100.0
branch 1 2 50.0
condition 1 3 33.3
subroutine 1 1 100.0
pod n/a
total 4 7 57.1


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             # Can be invoked as:
4             # ./bench.PL # generates FU/Benchmarks.pod, running new benchmarks as necessary
5             # ./bench.PL id x y # invalidate cache for the (regex-)matching benchmark IDs, x and y and re-run them
6             # ./bench.PL exec id x y # Run just the given benchmark and exit
7             #
8             # This script obviously has more dependencies than the FU distribution itself.
9             # It's supposed to be used by maintainers, not users.
10              
11              
12             # MakeMaker automatically runs this script as a default built step, but that's not very useful.
13 1 50 33 1   7241 BEGIN { exit if @ARGV && @ARGV[0] eq 'bench'; }
14              
15             use v5.36;
16             use builtin 'true', 'false';
17             use Benchmark ':hireswallclock', 'timethis';
18              
19             my %modules = map +($_, eval "require $_; \$${_}::VERSION"), qw/
20             FU
21             Cpanel::JSON::XS
22             JSON::PP
23             JSON::SIMD
24             JSON::Tiny
25             JSON::XS
26             TUWF::XML
27             HTML::Tiny
28             XML::Writer
29             DBD::Pg
30             Pg::PQ
31             /;
32             use FU::Pg;
33              
34             my @exec = $ARGV[0] && $ARGV[0] eq 'exec' ? @ARGV[1..3] : ();
35             my @run = !@exec && @ARGV && (qr/$ARGV[0]/i, $ARGV[1] ? qr/$ARGV[1]/i : qr/.*/, $ARGV[2] ? qr/$ARGV[2]/i : qr/.*/);
36              
37             my %data; # "id x y" => { id x y rate exists }
38             my @bench; # [ id, text, [ x_1, .. ], [ [ y_1, mod_1, sub_1, .. ], .. ] ]
39             my %oldmodules;
40             if (!@exec) {
41             if (open my $F, '<', 'FU/Benchmarks.pod') {
42             my $indata;
43             while (<$F>) {
44             chomp;
45             $oldmodules{$1} = $2 if /^=item L<([a-zA-Z0-9:]+)> ([0-9.]+)/;
46             $indata = 1 if /^# Cached data used by bench\.PL/;
47             next if !$indata || !$_ || /^#/;
48             my %d;
49             @d{qw/id x y rate/} = split /\t/;
50             $data{"$d{id} $d{x} $d{y}"} = \%d;
51             }
52             }
53             }
54              
55              
56             sub fmtbench($id, $text, $xs, $ys) {
57             my $r = "$text\n\n";
58             if (@$xs > 1) {
59             $r .= sprintf '%18s', '';
60             $r .= sprintf '%12s', $_ for @$xs;
61             $r .= "\n";
62             }
63             for my ($n, $yr) (builtin::indexed @$ys) {
64             my $x = $xs->[$n];
65             my ($y, $m, @ys) = @$yr;
66             $m ||= $y;
67             $r .= sprintf '%18s', $y;
68             for my $i (0..$#$xs) {
69             my $d = $data{"$id $xs->[$i] $y"};
70             $r .= $d && $d->{rate} ? sprintf '%10d/s', $d->{rate} : sprintf '%12s', '-';
71             }
72             $r .= "\n";
73             }
74             "$r\n"
75             }
76              
77             $SIG{INT} = $SIG{HUP} = sub { exit };
78             END {
79             exit if @exec;
80              
81             open my $F, '>FU/Benchmarks.pod' or die $!;
82             select $F;
83             while () {
84             s/^%/=/;
85             s#^:modules#join '', map sprintf("=item L<%s> %s\n\n", $_, $modules{$_}), sort keys %modules#e;
86             s#^:benches (.+)#join '', map fmtbench(@$_), grep $_->[0] =~ /$1/, @bench#e;
87             print;
88             }
89             for (sort keys %data) {
90             my $b = $data{$_};
91             print join("\t", map $_//'', @{$b}{qw/ id x y rate /})."\n";
92             }
93             }
94              
95              
96              
97             sub def($id, $text, $xs, @ys) {
98             for my ($ya) (@ys) {
99             my($y, $m, @sub) = @$ya;
100             $m ||= $y;
101             for my($i, $x) (builtin::indexed @$xs) {
102             next if !$sub[$i];
103             my $d = "$id $x $y";
104             $data{$d} ||= { id => $id, x => $x, y => $y };
105             $d = $data{$d};
106             $d->{exists} = 1;
107             }
108             }
109             push @bench, [ $id, $text, $xs, \@ys ];
110             }
111              
112              
113              
114              
115             use FU::Util 'json_format', 'json_parse';
116              
117             sub defjson($name, $canon, $text, $val) {
118             # Use similar options for fair comparisons.
119             my $cp = Cpanel::JSON::XS->new->allow_nonref->unblessed_bool->convert_blessed;
120             my $pp = JSON::PP->new->allow_nonref->core_bools->convert_blessed;
121             my $xs = JSON::XS->new->allow_nonref->boolean_values([false,true])->convert_blessed;
122             my $si = JSON::SIMD->new->allow_nonref->core_bools->convert_blessed;
123             my $c_cp = Cpanel::JSON::XS->new->allow_nonref->unblessed_bool->convert_blessed->canonical;
124             my $c_pp = JSON::PP->new->allow_nonref->core_bools->convert_blessed->canonical;
125             my $c_xs = JSON::XS->new->allow_nonref->boolean_values([false,true])->convert_blessed->canonical;
126             my $c_si = JSON::SIMD->new->allow_nonref->core_bools->convert_blessed->canonical;
127             my $enc = json_format $val;
128             def "json/$name", $text, [ 'Encode', $canon ? 'Canonical' : (), 'Decode' ],
129             [ 'JSON::PP', undef, sub { $pp->encode($val) }, $canon ? sub { $c_pp->encode($val) } : (), sub { $pp->decode($enc) } ],
130             [ 'JSON::Tiny', undef, sub { JSON::Tiny::to_json($val) }, $canon ? undef : (), sub { JSON::Tiny::from_json($enc) } ],
131             [ 'Cpanel::JSON::XS', undef, sub { $cp->encode($val) }, $canon ? sub { $c_cp->encode($val) } : (), sub { $cp->decode($enc) } ],
132             [ 'JSON::SIMD', undef, sub { $si->encode($val) }, $canon ? sub { $c_si->encode($val) } : (), sub { $si->decode($enc) } ],
133             [ 'JSON::XS', undef, sub { $xs->encode($val) }, $canon ? sub { $c_xs->encode($val) } : (), sub { $xs->decode($enc) } ],
134             [ 'FU::Util', 'FU', sub { json_format $val }, $canon ? sub { json_format $val, canonical => 1 } : (), sub { json_parse $enc } ];
135             }
136              
137             # From JSON::XS POD.
138             defjson api => 1, 'API object from L documentation.',
139             [ map +{method => 'handleMessage', params => ['user1','we were just talking'], 'id' => undef, 'array' => [1,11,234,-5,1e5,1e7,1,0]}, 1..10 ];
140              
141             defjson objs => 1, 'Object (small)', [ map +{ map +("string$_", 1), 'a'..'f' }, 0..100 ];
142             defjson objl => 1, 'Object (large)', { map +("string$_-something", 1), 'aa'..'zz' };
143             defjson obju => 1, 'Object (large, mixed unicode)', { map +("str\x{1234}g$_-some\x{85232}hing", 1), 'aa'..'zz' };
144              
145             defjson ints => 0, 'Small integers', [ -5000..5000 ];
146             defjson intl => 0, 'Large integers', [ map { my $n=$_; map +($n+1<<$_), 10..60 } 1..10 ];
147             defjson strs => 0, 'ASCII strings', [ map +('hello, world', 'one more string', 'another string'), 1..100 ];
148             defjson stru => 0, 'Unicode strings', do { use utf8;
149             [ map +('グリザイアの果実 -LE FRUIT DE LA GRISAIA-', '💩', 'Я люблю нічого не робити'), 1..50 ];
150             };
151             defjson stres => 0, 'String escaping (few)', [ map 'This string needs to "be escaped" a little bit', 1..100 ];
152             defjson strel => 0, 'String escaping (many)', [ map "This \" \\ needs \n\x41\x42\x43\x44 more", 1..100 ];
153              
154              
155              
156              
157             package BENCH::TUWFXML {
158             use TUWF::XML ':html5_', 'xml_string';
159             sub f($id) {
160             li_ class => $id % 2 ? 'one' : undef, '+', $id % 5 > 2 ? 'two' : undef, sub {
161             small_ '--'x($id % 50).' ' if $id % 3;
162             a_ href => "/$id",
163             class => $id % 7 > 2 ? 'another-class' : undef,
164             '+' => $id % 9 < 7 ? 'and-another-one' : undef,
165             style => "width: ${id}px",
166             $id;
167             };
168             }
169             sub t { xml_string sub { div_ sub { f $_ for (1..100) } } }
170             }
171              
172             package BENCH::XMLWriter {
173             my $wr;
174             sub f($id) {
175             $wr->startTag(li => class => join(' ', $id % 2 ? 'one' : (), $id % 5 > 2 ? 'two' : ()));
176             $wr->dataElement(small => '--'x($id % 50).' ') if $id % 3;
177             $wr->dataElement(a => $id, href => "/$id", class => join(' ',
178             $id % 7 > 2 ? 'another-class' : (),
179             $id % 9 < 7 ? 'and-another-one' : ()
180             ), style => "width: ${id}px");
181             $wr->endTag();
182             }
183             sub t {
184             $wr = XML::Writer->new(OUTPUT => \my $str, UNSAFE => 1);
185             $wr->startTag('div');
186             f $_ for (1..100);
187             $wr->endTag();
188             }
189             }
190              
191             package BENCH::HTMLTiny {
192             my $h;
193             sub f($id) {
194             $h->li({ class => join(' ', $id % 2 ? 'one' : (), '+', $id % 5 > 2 ? 'two' : ()) }, [
195             $id % 3 ? $h->small('--'x($id % 50).' ') : '',
196             $h->a({
197             href => "/$id",
198             class => join (' ',
199             $id % 7 > 2 ? 'another-class' : (),
200             $id % 9 < 7 ? 'and-another-one' : (),
201             ),
202             style => "width: ${id}px"
203             }, $id),
204             ]);
205             }
206             sub t {
207             $h = HTML::Tiny->new;
208             $h->div(map f($_), 1..100);
209             }
210             }
211              
212             package BENCH::FUXMLWriter {
213             use FU::XMLWriter ':html5_', 'fragment';
214             sub f($id) {
215             li_ class => $id % 2 ? 'one' : undef, '+', $id % 5 > 2 ? 'two' : undef, sub {
216             small_ '--'x($id % 50).' ' if $id % 3;
217             a_ href => "/$id",
218             class => $id % 7 > 2 ? 'another-class' : undef,
219             '+' => $id % 9 < 7 ? 'and-another-one' : undef,
220             style => "width: ${id}px",
221             $id;
222             };
223             }
224             sub t { fragment { div_ sub { f $_ for (1..100) } } }
225             }
226              
227             def 'xml/a', 'HTML fragment', [ 'Rate' ],
228             [ 'TUWF::XML', undef, \&BENCH::TUWFXML::t ],
229             [ 'XML::Writer', undef, \&BENCH::XMLWriter::t ],
230             [ 'HTML::Tiny', undef, \&BENCH::HTMLTiny::t ],
231             [ 'FU::XMLWriter', 'FU', \&BENCH::FUXMLWriter::t ];
232              
233              
234              
235              
236             {
237             die "FU_TEST_DB not set.\n" if @exec && !$ENV{FU_TEST_DB};
238             my $pq = @exec && Pg::PQ::Conn->new($ENV{FU_TEST_DB});
239             my $fu = @exec && FU::Pg->connect($ENV{FU_TEST_DB});
240             # XXX: Doesn't support all connection params this way
241             my $dbi = @exec && DBI->connect("dbi:Pg:dbname=".$pq->db, $pq->user, $pq->pass, {RaiseError => 1, PrintError => 0});
242              
243             my $small = 'SELECT x, x+1, x+2, x+3, x+4, x+5, x+6, x+7, x+8, x+9 FROM generate_series(-10000::smallint, 9999, 10) x(x)';
244             my $big = 'SELECT x<<5, x<<10, x<<15, x<<20, x<<25, x<<30, x<<35, x<<40, x<<45, x<<50 FROM generate_series(1::bigint, 20000, 1) x(x)';
245              
246             my sub dbi { my $sum = 0; my $st = $dbi->prepare_cached($_[0]); for my $row ($dbi->selectall_arrayref($st)->@*) { $sum ^= $_ for @$row; } }
247             my sub pq { my $sum = 0; $pq->prepare('' => $_[0]); for my $row ($pq->execQueryPrepared('')->rows) { $sum ^= $_ for @$row; } }
248             my sub fub { my $sum = 0; for my $row ($fu->sql($_[0])->alla->@*) { $sum ^= $_ for @$row; } }
249             my sub fut { my $sum = 0; for my $row ($fu->sql($_[0])->text->alla->@*) { $sum ^= $_ for @$row; } }
250              
251             def 'pg/ints', 'Fetch and bitwise-or 20k integers', [ 'Smallint', 'Bigint' ],
252             [ 'DBD::Pg', undef, sub { dbi($small) }, sub { dbi($big) } ],
253             [ 'Pg::PQ', undef, sub { pq($small) }, sub { pq($big) } ],
254             [ 'FU::Pg (bin)', 'FU', sub { fub($small) }, sub { fub($big) } ],
255             [ 'FU::Pg (text)', 'FU', sub { fut($small) }, sub { fut($big) } ];
256             }
257              
258              
259              
260              
261              
262             delete @data{ grep !$data{$_}{exists}, keys %data };
263              
264              
265             sub runbench($sub) {
266             my $o = timethis -1, $sub, 0, 'none';
267             printf "%.2f\n", $o->iters/$o->real;
268             exit;
269             }
270              
271             sub execbench($d) {
272             my $sum = 0;
273             my $num = 1;
274             local $| = 1;
275             printf "%-20s%-12s%-20s", $d->{id}, $d->{x}, $d->{y};
276             for (1..$num) {
277             open my $P, '-|', $^X, (map "-I$_", @INC), $0, 'exec', $d->{id}, $d->{x}, $d->{y};
278             chomp(my $rate = <$P>);
279             printf "%10d", $rate;
280             $sum += $rate;
281             }
282             printf " ->%10d\n", $sum/$num;
283             $d->{rate} = sprintf '%.0f', $sum/$num;
284             }
285              
286             for my $b (@bench) {
287             my ($id, $text, $xs, $ys) = @$b;
288             for my ($ya) (@$ys) {
289             my($y, $m, @sub) = @$ya;
290             $m ||= $y;
291             for my($i, $x) (builtin::indexed @$xs) {
292             next if !$sub[$i];
293             if (@exec) {
294             runbench $sub[$i] if $exec[0] eq $id && $exec[1] eq $x && $exec[2] eq $y;
295             } else {
296             my $d = $data{"$id $x $y"};
297             execbench $d if !$oldmodules{$m} || $modules{$m} ne $oldmodules{$m}
298             || (@run && $id =~ /$run[0]/ && $x =~ /$run[1]/ && $y =~ /$run[2]/);
299             }
300             }
301             }
302             }
303              
304             die if @exec;
305              
306              
307             # s/^=/%/ to prevent tools from interpreting the below as POD
308             __DATA__