#!/usr/bin/perl -w
=head1 NAME
bench_template.pl - Test relative performance of Template::Alloy to Template::Toolkit
=cut
use strict;
use Benchmark qw(cmpthese timethese);
use POSIX qw(tmpnam);
use File::Path qw(rmtree);
use Template::Alloy;
use CGI::Ex::Dump qw(debug);
use Template;
use constant test_taint => 0 && eval { require Taint::Runtime }; # s/0/1/ to check tainting
Taint::Runtime::taint_start() if test_taint;
my $tt_cache_dir = tmpnam;
END { rmtree $tt_cache_dir };
mkdir $tt_cache_dir, 0755;
my $swap = {
one => "ONE",
a_var => "a",
foo => '[% bar %]',
bar => "baz",
hash => {a => 1, b => 2, c => { d => [{hee => ["hmm"]}] }},
array => [qw(A B C D E a A)],
code => sub {"(@_)"},
filt => sub {sub {$_[0]x2}},
};
use Template::Stash;;
my $s = Template::Stash->new($swap);
#use Template::Stash::XS;
#$s = Template::Stash::XS->new($swap);
###----------------------------------------------------------------###
### get objects ready
my @config1 = (STASH => $s, ABSOLUTE => 1, CONSTANTS => {simple => 'var'}, EVAL_PERL => 1, INCLUDE_PATH => $tt_cache_dir);
#push @config1, (INTERPOLATE => 1);
my @config2 = (@config1, COMPILE_EXT => '.ttc');
my $tt1 = Template->new(@config1);
my $tap = Template::Alloy->new(@config1, COMPILE_PERL => 1);
#use Template::Alloy::XS;
#my $tt1 = Template::Alloy::XS->new(@config1);
#my $tap = Template::Alloy::XS->new(@config1, COMPILE_PERL => 1);
#$swap->{$_} = $_ for (1 .. 1000); # swap size affects benchmark speed
###----------------------------------------------------------------###
### write out some file to be used later
my $fh;
my $bar_template = "$tt_cache_dir/bar.tt";
END { unlink $bar_template };
open($fh, ">$bar_template") || die "Couldn't open $bar_template: $!";
print $fh "BAR";
close $fh;
my $baz_template = "$tt_cache_dir/baz.tt";
END { unlink $baz_template };
open($fh, ">$baz_template") || die "Couldn't open $baz_template: $!";
print $fh "[% SET baz = 42 %][% baz %][% bing %]";
close $fh;
my $longer_template = "[% INCLUDE bar.tt %]"
."[% array.join('|') %]"
.("123"x200)
."[% FOREACH a IN array %]foobar[% IF a == 'A' %][% INCLUDE baz.tt %][% END %]bazbing[% END %]"
.("456"x200)
."[% IF foo ; bar ; ELSIF baz ; bing ; ELSE ; bong ; END %]"
.("789"x200)
."[% IF foo ; bar ; ELSIF baz ; bing ; ELSE ; bong ; END %]"
.("012"x200)
."[% IF foo ; bar ; ELSIF baz ; bing ; ELSE ; bong ; END %]"
."[% array.join('|') %]"
."[% PROCESS bar.tt %]";
my $hello2000 = "
[% title %]
[% array = [ \"Hello\", \"World\", \"2000\", \"Hello\", \"World\", \"2000\" ] %]
[% sorted = array.sort %]
[% multi = [ sorted, sorted, sorted, sorted, sorted ] %]
[% FOREACH row = multi %]
[% FOREACH col = row %]
[% col %] |
[% END %]
[% END %]
[% param = integer %]
[% FOREACH i = [ 1 .. 10 ] %]
[% var = i + param %]"
.("\n [%var%] Hello World Hello World Hello World Hello World Hello World Hello World Hello World Hello World Hello World Hello World
"x20)."
[% END %]
";
###----------------------------------------------------------------###
### set a few globals that will be available in our subs
my $show_list = grep {$_ eq '--list'} @ARGV;
my $run_all = grep {$_ eq '--all'} @ARGV;
my @run = $run_all ? () : @ARGV;
my $str_ref;
my $filename;
### uncomment to run a specific test - otherwise all tests run
#@run = qw(07);
# ### All percents are Template::Alloy vs TT2
# ### (The percent that Alloy is faster than TT)
# Existing object by string ref #
# New object with CACHE_EXT set # #
# New object each time (undef CACHE_SIZE) # # #
# This percent is compiled in memory (repeated calls) # # # #
my $tests = { # # # # #
'01_empty' => "", # 157% # 357% # 318% # 386% # 17173.6/s #
'02_var_sma' => "[% one %]", # 131% # 361% # 359% # 709% # 14022.8/s #
'03_var_lar' => "[% one %]"x100, # 33% # 238% # 61% # 1694% # 1046.3/s #
'04_set_sma' => "[% SET one = 2 %]", # 142% # 311% # 331% # 889% # 14614.1/s #
'05_set_lar' => "[% SET one = 2 %]"x100, # 66% # 172% # 22% # 3621% # 1436.4/s #
'06_set_range' => "[% SET one = [0..30] %]", # 72% # 223% # 246% # 648% # 9774.1/s #
'07_chain_sm' => "[% hash.a %]", # 138% # 376% # 336% # 786% # 13001.4/s #
'08_mixed_sma' => "".((" "x100)."[% one %]\n")x10, # 81% # 333% # 216% # 1617% # 6483.7/s #
'09_mixed_med' => "".((" "x10)."[% one %]\n")x100, # 35% # 306% # 97% # 2156% # 1017.8/s #
'10_str_sma' => "".("[% \"".(" "x100)."\$one\" %]\n")x10, # -19% # 1345% # 95% # 4956% # 2873.3/s #
'11_str_lar' => "".("[% \"".(" "x10)."\$one\" %]\n")x100, # -50% # 325% # 2% # 1218% # 370.4/s #
'12_num_lterl' => "[% 2 %]", # 150% # 346% # 362% # 755% # 16365.4/s #
'13_plus' => "[% 1 + 2 %]", # 100% # 304% # 318% # 691% # 13031.8/s #
'14_chained' => "[% c.d.0.hee.0 %]", # 117% # 399% # 314% # 891% # 12271.8/s #
'15_chain_set' => "[% SET c.d.0.hee.0 = 2 %]", # 120% # 327% # 292% # 924% # 10048.1/s #
'16_chain_lar' => "[% c.d.0.hee.0 %]"x100, # -2% # 379% # 54% # 1880% # 520.7/s #
'17_chain_sl' => "[% SET c.d.0.hee.0 = 2 %]"x100, # 99% # 257% # 72% # 1663% # 354.2/s #
'18_cplx_comp' => "[% t = 1 || 0 ? 0 : 1 || 2 ? 2 : 3 %][% t %]", # 66% # 199% # 233% # 1090% # 9359.2/s #
'19_if_sim_t' => "[% a=1 %][% IF a %]Two[% END %]", # 126% # 306% # 299% # 1145% # 12739.3/s #
'20_if_sim_f' => " [% IF a %]Two[% END %]", # 137% # 377% # 314% # 935% # 13652.8/s #
'21_if_else' => "[% IF a %]A[% ELSE %]B[% END %]", # 134% # 383% # 314% # 1061% # 13651.9/s #
'22_if_elsif' => "[% IF a %]A[% ELSIF b %]B[% ELSE %]C[% END %]", # 109% # 346% # 276% # 1179% # 11572.2/s #
'23_for_i_sml' => "[% FOREACH i = [0..10] ; i ; END %]", # 43% # 156% # 138% # 348% # 3258.5/s #
'24_for_i_med' => "[% FOREACH i = [0..100] ; i ; END %]", # 7% # 27% # 12% # 56% # 506.4/s #
'25_for_sml' => "[% FOREACH [0..10] ; i ; END %]", # 41% # 161% # 142% # 341% # 3163.9/s #
'26_for_med' => "[% FOREACH [0..100] ; i ; END %]", # 19% # 35% # 18% # 71% # 509.1/s #
'27_while' => "[% f = 10 %][%WHILE f%][%f=f- 1%][%f%][% END %]", # 14% # 143% # 73% # 262% # 1848.0/s #
'28_whl_set_l' => "[% f = 10; WHILE (g=f) ; f = f - 1 ; f ; END %]", # 4% # 105% # 58% # 190% # 1420.1/s #
'29_whl_set_s' => "[% f = 1; WHILE (g=f) ; f = f - 1 ; f ; END %]", # 55% # 201% # 190% # 891% # 6351.0/s #
'30_file_proc' => "[% PROCESS bar.tt %]", # 171% # 345% # 331% # 638% # 9388.6/s #
'31_file_incl' => "[% INCLUDE baz.tt %]", # 122% # 274% # 238% # 432% # 6426.3/s #
'32_process' => "[% BLOCK foo %]Hi[% END %][% PROCESS foo %]", # 118% # 339% # 295% # 928% # 9431.1/s #
'33_include' => "[% BLOCK foo %]Hi[% END %][% INCLUDE foo %]", # 106% # 322% # 286% # 802% # 8298.5/s #
'34_macro' => "[% MACRO foo BLOCK %]Hi[% END %][% foo %]", # 91% # 194% # 241% # 793% # 8881.4/s #
'35_macro_arg' => "[% MACRO foo(n) BLOCK %]Hi[%n%][%END%][%foo(2)%]", # 70% # 189% # 223% # 899% # 7111.7/s #
'36_macro_pro' => "[% MACRO foo PROCESS bar;BLOCK bar%]7[%END;foo%]", # 79% # 211% # 255% # 793% # 6189.0/s #
'37_filter2' => "[% n = 1 %][% n | repeat(2) %]", # 126% # 317% # 297% # 1173% # 11058.5/s #
'38_filter' => "[% n = 1 %][% n FILTER repeat(2) %]", # 99% # 252% # 257% # 995% # 9657.4/s #
'39_fltr_name' => "[% n=1; n FILTER echo=repeat(2); n FILTER echo%]", # 47% # 239% # 194% # 912% # 6633.8/s #
'40_constant' => "[% constants.simple %]", # 167% # 359% # 360% # 1060% # 16677.6/s #
'41_perl' => "[%one='ONE'%][% PERL %]print \"[%one%]\"[%END%]", # 63% # 297% # 239% # 827% # 7242.2/s #
'42_filtervar' => "[% 'hi' | \$filt %]", # 55% # 352% # 272% # 607% # 8618.4/s #
'43_filteruri' => "[% ' ' | uri %]", # 76% # 360% # 277% # 689% # 10118.0/s #
'44_filterevl' => "[% foo | eval %]", # 367% # 319% # 334% # 752% # 6482.5/s #
'45_capture' => "[% foo = BLOCK %]Hi[% END %][% foo %]", # 120% # 270% # 260% # 1002% # 12199.6/s #
'46_refs' => "[% b = \\code(1); b(2) %]", # 33% # 201% # 175% # 545% # 6132.4/s #
'47_complex' => "$longer_template", # 62% # 194% # 136% # 790% # 1302.9/s #
'48_hello2000' => "$hello2000", # 29% # 145% # 40% # 303% # 251.9/s #
# overall # 86% # 291% # 214% # 999% #
# With Stash::XS
#'47_complex' => "$longer_template", # 15% # 184% # 100% # 12% # 1952.5/s #
#'48_hello2000' => "$hello2000", # 28% # 104% # -12% # 29% # 574.0/s #
## overall # 15% # 277% # 186% # 15% #
};
### load the code representation
my $text = {};
seek DATA, 0, 0;
my $data = do { local $/ = undef; };
foreach my $key (keys %$tests) {
$data =~ m/(.*\Q$key\E.*)/ || next;
$text->{$key} = $1;
}
if ($show_list) {
foreach my $text (sort values %$text) {
print "$text\n";
}
exit;
}
my $run = join("|", @run);
@run = grep {/$run/} sort keys %$tests;
###----------------------------------------------------------------###
sub file_TT_new {
my $out = '';
my $t = Template->new(@config1);
$t->process($filename, $swap, \$out);
return $out;
}
sub str_TT_new {
my $out = '';
my $t = Template->new(@config1);
$t->process($str_ref, $swap, \$out);
return $out;
}
sub file_TT {
my $out = '';
$tt1->process($filename, $swap, \$out);
return $out;
}
sub str_TT {
my $out = '';
$tt1->process($str_ref, $swap, \$out) || debug $tt1->error;
return $out;
}
sub file_TT_cache_new {
my $out = '';
my $t = Template->new(@config2);
$t->process($filename, $swap, \$out);
return $out;
}
###----------------------------------------------------------------###
sub file_Alloy_new {
my $out = '';
my $t = Template::Alloy->new(@config1);
$t->process($filename, $swap, \$out);
return $out;
}
sub str_Alloy_new {
my $out = '';
my $t = Template::Alloy->new(@config1);
$t->process($str_ref, $swap, \$out);
return $out;
}
sub file_Alloy {
my $out = '';
$tap->process($filename, $swap, \$out);
return $out;
}
sub str_Alloy {
my $out = '';
$tap->process($str_ref, $swap, \$out);
return $out;
}
sub file_Alloy_cache_new {
my $out = '';
my $t = Template::Alloy->new(@config2);
$t->process($filename, $swap, \$out);
return $out;
}
###----------------------------------------------------------------###
@run = sort(keys %$tests) if $#run == -1;
my $output = '';
my %cumulative;
foreach my $test_name (@run) {
die "Invalid test $test_name" if ! exists $tests->{$test_name};
my $txt = $tests->{$test_name};
my $sample = $text->{$test_name};
$sample =~ s/^.+=>//;
$sample =~ s/\#.+$//;
print "-------------------------------------------------------------\n";
print "Running test $test_name\n";
print "Test text: $sample\n";
### set the global file types
$str_ref = \$txt;
$filename = $tt_cache_dir ."/$test_name.tt";
open(my $fh, ">$filename") || die "Couldn't open $filename: $!";
print $fh $txt;
close $fh;
#debug file_Alloy(), str_TT();
#debug $tap->parse_tree($file);
### check output - and also allow for caching
for (1..2) {
if (file_Alloy() ne str_TT()) {
debug $tap->parse_tree($str_ref);
debug file_Alloy(), str_TT();
die "file_Alloy didn't match";
}
die "file_TT didn't match " if file_TT() ne str_TT();
die "str_Alloy didn't match " if str_Alloy() ne str_TT();
die "file_Alloy_cache_new didn't match " if file_Alloy_cache_new() ne str_TT();
die "file_TT_cache_new didn't match " if file_TT_cache_new() ne str_TT();
}
next if test_taint;
###----------------------------------------------------------------###
my $r = eval { timethese (-2, {
file_TT_n => \&file_TT_new,
# str_TT_n => \&str_TT_new,
file_TT => \&file_TT,
str_TT => \&str_TT,
file_TT_c_n => \&file_TT_cache_new,
file_Alloy_n => \&file_Alloy_new,
# str_Alloy_n => \&str_Alloy_new,
file_Alloy => \&file_Alloy,
str_Alloy => \&str_Alloy,
file_Alloy_c_n => \&file_Alloy_cache_new,
}) };
if (! $r) {
debug "$@";
next;
}
eval { cmpthese $r };
my $copy = $text->{$test_name};
$copy =~ s/\#.+//;
$output .= $copy;
eval {
my $hash = {
'1 cached_in_memory ' => ['file_Alloy', 'file_TT'],
'2 new_object ' => ['file_Alloy_n', 'file_TT_n'],
'3 cached_on_file (new_object)' => ['file_Alloy_c_n', 'file_TT_c_n'],
'4 string reference ' => ['str_Alloy', 'str_TT'],
'5 Alloy new vs TT in mem ' => ['file_Alloy_n', 'file_TT'],
'6 Alloy in mem vs TT new ' => ['file_Alloy', 'file_TT_n'],
'7 Alloy in mem vs Alloy new ' => ['file_Alloy', 'file_Alloy_n'],
'8 TT in mem vs TT new ' => ['file_TT', 'file_TT_n'],
};
foreach my $type (sort keys %$hash) {
my ($key1, $key2) = @{ $hash->{$type} };
my $ct = $r->{$key1};
my $tt = $r->{$key2};
my $ct_s = $ct->iters / ($ct->cpu_a || 1);
my $tt_s = $tt->iters / ($tt->cpu_a || 1);
my $p = int(100 * ($ct_s - $tt_s) / ($tt_s || 1));
print "$type - Alloy is $p% faster than TT\n";
$output .= sprintf('# %3s%% ', $p) if $type =~ /^[1234]/;
### store cumulatives
if (abs($p) < 10000) {
$cumulative{$type} ||= [0, 0];
$cumulative{$type}->[0] += $p;
$cumulative{$type}->[1] ++;
}
}
};
debug "$@"
if $@;
$output .= "# ".sprintf("%.1f", $r->{'file_Alloy'}->iters / ($r->{'file_Alloy'}->cpu_a || 1))."/s #\n";
# $output .= "#\n";
foreach my $row (values %cumulative) {
$row->[2] = sprintf('%.1f', $row->[0] / ($row->[1]||1));
}
if ($#run > 0) {
foreach (sort keys %cumulative) {
printf "Cumulative $_: %6.1f\n", $cumulative{$_}->[2];
}
}
}
### add the final total row
if ($#run > 0) {
$output .= " # overall" . (" "x61);
foreach my $type (sort keys %cumulative) {
$output .= sprintf('# %3s%% ', int $cumulative{$type}->[2]) if $type =~ /^[1234]/;
}
$output .= "#\n";
}
print $output;
#print `ls -lR $tt_cache_dir`;
__DATA__