Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions lib/Template/Grammar.pm
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,67 @@ sub new {
}, $class;
}

# track usages of objects using the factory
# this is an over complex object
# used to track how many objects are currently using the shared factory
my $_factory_usages;

DESTROY {
my ( $self ) = @_;

# on Grammar destruction check if we can safely trigger the destroy for the factory
$self->unregister_factory() if $self;

return;
}

sub unregister_factory {
my ( $self ) = @_;

return unless $self && ref $_factory_usages;
return unless "$factory" eq $_factory_usages->{CURRENT};

if ( $_factory_usages->{HOLD_BY}->{ "$self" } ) {
delete $_factory_usages->{HOLD_BY}->{ "$self" };
}

if ( ! scalar keys %{ $_factory_usages->{HOLD_BY} } ) {
# avoid a memory leak from factory
undef $factory;
undef $_factory_usages;
}

return;
}

sub register_factory {
my ( $self ) = @_;

return unless $factory;

$_factory_usages //= { CURRENT => "", HOLD_BY => {} };

if ( "$factory" ne $_factory_usages->{CURRENT} ) {
# we have updated the factory, should not care about the previous one...
$_factory_usages->{HOLD_BY} = {}; # reset who hold the factory
$_factory_usages->{CURRENT} = "$factory"; # stringify it
}

$_factory_usages->{HOLD_BY}->{ "$self" } = 1; # we are using this factory

return;
}

# update method to set package-scoped $factory lexical
sub install_factory {
my ($self, $new_factory) = @_;

$factory = $new_factory;

# register the current factory in order to clean it on destroy if possible
$self->register_factory();

return $factory;
}


Expand Down
108 changes: 108 additions & 0 deletions t/zz-process-leak.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#============================================================= -*-perl-*-
#
# t/zz-process-leak.t
#
# Check for memory leak when using Template::Plugin::Simple
#
# Written by Nicolas R. <atoomic@cpan.org>
#
# Copyright (C) 2018 cPanel Inc. All Rights Reserved.
#
# This is free software; you can redistribute it and/or modify it
# under the same terms as Perl itself.
#
#========================================================================

use strict;
use warnings;
use lib qw( t/lib ./lib ../lib ../blib/arch );

use Template;
use Template::Plugin::Simple;

use Test::More tests => 6;

plan( skip_all => "Developer test" ) unless ( $ENV{AUTOMATED_TESTING} or $ENV{RELEASE_TESTING} );

eval { require Test::LeakTrace };
if ($@) {
plan( skip_all => 'Test::LeakTrace not installed' );
}

note "Searching for leak using Test::LeakTrace...";

my $vars1 = {
data => [
{
val => 'value1',
}
]
};

my $vars2 = {
data => [
{
val => 'value2',
stuff => [ { name => 'bob' } ]
}
]
};

my @TESTS;

# we are adding it twice to show that this is not really a leak
# as only the first one will leak
# the memory 'leak' comes from the factory singleton in Template::Grammar
push @TESTS, {
vars => $vars1,
expect => qq[value1\n],
} for 1 .. 2;

push @TESTS, {
vars => $vars2,
expect => qq[value2\n... one item\n],
};

my ( $VARS, $OUT );
my $c = 0;
foreach my $t (@TESTS) {

$VARS = $t->{vars};
++$c;

my $no_leaks = Test::LeakTrace::no_leaks_ok( \&check_leak, "no leak when using for var$c" );
is $OUT, $t->{expect}, "output matches what we expect for var$c" or diag $OUT;

if ( !$no_leaks ) {
diag "Memory leak detected when using var$c...";
if ( eval { require Devel::Cycle; 1 } ) {
Devel::Cycle::find_cycle( check_leak() );
}
else {
diag "consider installing Devel::Cycle to detect leak";
}
}

}

exit;

sub check_leak {

my $text = <<'EOT';
[% FOREACH item IN data -%]
[% item.val %]
[% FOREACH data IN item.stuff -%]
... one item
[% END -%]
[% END -%]
EOT

$OUT = ''; # reset it before calling
local $@; # avoid a leak from $@
my $tt = Template->new();
eval { $tt->process( \$text, $VARS, \$OUT ); };

return $tt;
}

2 changes: 1 addition & 1 deletion t/zz-stash-xs-leak.t
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ unless ( $ENV{AUTOMATED_TESTING} or $ENV{RELEASE_TESTING} ) {

# only run the test when compiled with Template::Stash
if ( $Template::Config::STASH ne 'Template::Stash::XS' ) {
skip_all('Template::Config is not using Template::Stash::XS');
plan( skip_all => 'Template::Config is not using Template::Stash::XS' );
}

require Template::Stash::XS;
Expand Down