Clone
view release on metacpan or search on metacpan
t/12-memleak.t view on Meta::CPAN
# GH #42: Memory leak when cloning non-existent hash values
# When $hash{nonexistent_key} is passed directly to clone(), Perl creates
# a temporary SVt_PVLV (defelem) SV. The clone code treats PVLV types by
# just incrementing the refcount (clone == ref), but then the magic-cloning
# loop adds duplicate magic entries to the original SV, leaking the cloned
# mg_obj on every call.
# Helper: measure RSS in KB (portable across Linux and macOS)
sub get_rss_kb {
if ($^O eq 'linux') {
# /proc/self/status is available on Linux
open my $fh, '<', '/proc/self/status' or return undef;
while (<$fh>) {
return $1 if /^VmRSS:\s+(\d+)\s+kB/;
}
return undef;
}
elsif ($^O eq 'darwin') {
my $rss = `ps -o rss= -p $$`;
chomp $rss;
return $rss =~ /^\s*(\d+)/ ? $1 : undef;
}
return undef;
}
# Test 1: clone of non-existent hash key should return undef
{
my $data = {};
my $result = clone($data->{no_such_key});
ok(!defined $result, "clone of non-existent hash key returns undef");
}
# Test 2: clone of non-existent hash key with populated hash
{
my %hash = (a => 1, b => 2);
my $result = clone($hash{no_such_key});
ok(!defined $result, "clone of non-existent key in populated hash returns undef");
}
# Test 3: clone of non-existent hashref key
{
my $data = { foo => 'bar' };
my $result = clone($data->{nonexistent});
ok(!defined $result, "clone of non-existent hashref key returns undef");
}
# Test 4: intermediate variable should not leak (baseline)
{
my $data = {};
my $before = get_rss_kb();
SKIP: {
skip "Cannot measure RSS on this platform", 1 unless defined $before;
for (1..100_000) {
my $tmp = $data->{no_such_key};
clone($tmp);
}
my $after = get_rss_kb();
my $delta = $after - $before;
ok($delta < 2000, "clone via intermediate variable does not leak (delta: ${delta} KB)")
or diag("Memory grew by $delta KB over 100K iterations");
}
}
# Test 5: direct hash miss should not leak (the actual bug from GH #42)
{
my $data = {};
my $before = get_rss_kb();
SKIP: {
skip "Cannot measure RSS on this platform", 1 unless defined $before;
for (1..100_000) {
Clone::clone($data->{no_such_key});
}
my $after = get_rss_kb();
my $delta = $after - $before;
ok($delta < 2000, "clone of hash miss does not leak (delta: ${delta} KB)")
or diag("Memory grew by $delta KB over 100K iterations â GH #42 regression");
}
}
# Test 6: populated hash, direct miss should not leak
{
my %hash = (a => 1, b => 2, c => 3);
my $before = get_rss_kb();
SKIP: {
skip "Cannot measure RSS on this platform", 1 unless defined $before;
for (1..100_000) {
Clone::clone($hash{nonexistent});
}
my $after = get_rss_kb();
my $delta = $after - $before;
ok($delta < 2000, "clone of hash miss on populated hash does not leak (delta: ${delta} KB)")
or diag("Memory grew by $delta KB over 100K iterations");
}
}
# Test 7: clone of existing hash key should work fine and not leak
{
my %hash = (key => "value");
my $result = clone($hash{key});
is($result, "value", "clone of existing hash key returns correct value");
}
# Test 8: clone of nested hash with non-existent key
{
my $data = { inner => { a => 1 } };
my $result = clone($data->{inner}{no_such_key});
ok(!defined $result, "clone of non-existent key in nested hash returns undef");
}
done_testing;
( run in 0.817 second using v1.01-cache-2.11-cpan-389fe586d7c )