Skip Menu |

This queue is for tickets about the Hook-LexWrap CPAN distribution.

Report information
The Basics
Id: 50517
Status: new
Priority: 0/
Queue: Hook-LexWrap

People
Owner: Nobody in particular
Requestors: NCLEATON [...] cpan.org
Cc:
AdminCc:

Bug Information
Severity: (no value)
Broken in: 0.22
Fixed in: (no value)



Subject: PATCH: memory leak when temp wrapping a named sub
When Hook::LexWrap is used to temporarily wrap a named sub, the wrapper is disabled rather than removed when the cleanup handle goes out of scope. This is a big problem if code that will be called many times temporarily wraps a sub by name. Each call will leak at least one closure, and maybe other objects. The wrapped sub will also get slower and slower, as each call to it must traverse a long chain of disabled wrappers. The attached patch causes disabled wrappers around named subs to be removed when possible.
Subject: hl.patch
diff -Nurd Hook-LexWrap-0.22.orig/lib/Hook/LexWrap.pm Hook-LexWrap-0.22/lib/Hook/LexWrap.pm --- Hook-LexWrap-0.22.orig/lib/Hook/LexWrap.pm 2008-12-18 23:49:46.000000000 +0200 +++ Hook-LexWrap-0.22/lib/Hook/LexWrap.pm 2009-10-15 09:36:26.000000000 +0200 @@ -4,6 +4,7 @@ use warnings; our $VERSION = '0.22'; use Carp; +use Scalar::Util qw(weaken); { no warnings 'redefine'; @@ -23,6 +24,9 @@ sub import { no strict 'refs'; *{caller()."::wrap"} = \&wrap } +our %_unwrap_map; +our $_unwrap_map_prune_countdown = 0; + sub wrap (*@) { ## no critic Prototypes my ($typeglob, %wrapper) = @_; $typeglob = (ref $typeglob || $typeglob =~ /::/) @@ -82,7 +86,28 @@ *{$typeglob} = $imposter; } return unless defined wantarray; - return bless sub{ $unwrap=1 }, 'Hook::LexWrap::Cleanup'; + weaken(my $woriginal = $original); + weaken(my $wimposter = $imposter); + return bless sub { + $unwrap = 1; + return unless defined $woriginal; + return unless defined $wimposter; + no strict 'refs'; + if (*{$typeglob}{CODE} eq $wimposter) { + while ($_unwrap_map{"$woriginal"}) { + $woriginal = delete $_unwrap_map{"$woriginal"}; + } + *{$typeglob} = $woriginal; + } else { + if (++$_unwrap_map_prune_countdown > 100) { + foreach my $k (keys %_unwrap_map) { + defined $_unwrap_map{$k} or delete $_unwrap_map{$k}; + } + $_unwrap_map_prune_countdown = 0; + } + weaken($_unwrap_map{"$wimposter"} = $woriginal); + } + }, 'Hook::LexWrap::Cleanup'; } package Hook::LexWrap::Cleanup; diff -Nurd Hook-LexWrap-0.22.orig/Makefile.PL Hook-LexWrap-0.22/Makefile.PL --- Hook-LexWrap-0.22.orig/Makefile.PL 2008-11-06 13:05:25.000000000 +0200 +++ Hook-LexWrap-0.22/Makefile.PL 2009-10-15 09:31:14.000000000 +0200 @@ -10,6 +10,7 @@ ABSTRACT_FROM => 'lib/Hook/LexWrap.pm', PL_FILES => {}, PREREQ_PM => { + 'Scalar::Util' => 0, }, ($ExtUtils::MakeMaker::VERSION ge '6.31'? ('LICENSE' => 'perl', ) : ()), diff -Nurd Hook-LexWrap-0.22.orig/t/leak.t Hook-LexWrap-0.22/t/leak.t --- Hook-LexWrap-0.22.orig/t/leak.t 1970-01-01 02:00:00.000000000 +0200 +++ Hook-LexWrap-0.22/t/leak.t 2009-10-15 10:11:26.000000000 +0200 @@ -0,0 +1,77 @@ +use strict; +use warnings; + +use Test::More; +use Hook::LexWrap; + +$Foo::count = 0; +sub Foo::new { + ++$Foo::count; + return bless {}, 'Foo'; +} +sub Foo::DESTROY { + --$Foo::count; +} + +# Install 4 layers of scope limited wrappers around a named sub, then delete +# the cleanup handles (and maybe the wrapped symbol table entry) and check that +# all the installed wrappers are cleaned up. +# Repeat for all possible orderings of deleting the cleanup handles and +# replacing the symbol table entry. + +plan tests => 6*4*3*2 * runtests_testcount(); + +foreach my $replace_when (0, 1, 2, 3, 4, 'never') { + foreach my $blat1 (0, 1, 2, 3) { + foreach my $blat2 (0, 1, 2) { + foreach my $blat3 (0, 1) { + runtests($replace_when, $blat1, $blat2, $blat3); + } + } + } +} + +my $wrapme_callcount = 0; +sub wrapme_real { + ++$wrapme_callcount; +} + +sub runtests { + my ($replace_when, @blat) = @_; + my $name = join ',', @blat, "r=$replace_when"; + + { no warnings 'redefine'; *wrapme = \&wrapme_real } + $Foo::count = $wrapme_callcount = 0; + + my ($a, $b); + my @cleanup_handles; + foreach my $layer (1 .. 4) { + my $foo = new Foo; + push @cleanup_handles, wrap 'wrapme', + pre => sub { ++$a if $foo }, + post => sub { ++$b if $foo }; + } + + my $glob_replaced = 0; + my $expect_wrapme_calls = 0; + foreach my $layers_installed (4, 3, 2, 1, 0) { + if ($replace_when eq $layers_installed) { + no warnings 'redefine'; + *wrapme = sub {}; + $glob_replaced = 1; + } + $a = $b = 0; + wrapme(); + ++$expect_wrapme_calls unless $glob_replaced; + my $expect_prepost_calls = $glob_replaced ? 0 : $layers_installed; + is $a, $expect_prepost_calls, "pre sub callcount $name"; + is $b, $expect_prepost_calls, "post sub callcount $name"; + + splice @cleanup_handles, shift(@blat)||0, 1 if @cleanup_handles; + } + + is $Foo::count, 0, "no closures leaked $name"; + is $wrapme_callcount, $expect_wrapme_calls, "wrapme callcount $name"; +} +sub runtests_testcount { 5*2 + 2 } +