Replace many closure-based proxy methods with static qsubs
[dbsrgits/DBIx-Class.git] / t / lib / DBICTest / Util / LeakTracer.pm
CommitLineData
218b7c12 1package DBICTest::Util::LeakTracer;
2
3use warnings;
4use strict;
5
6use Carp;
96577657 7use Scalar::Util qw(isweak weaken blessed reftype);
8433421f 8use DBIx::Class::_Util qw(refcount hrefaddr refdesc);
556c4fe6 9use DBIx::Class::Optional::Dependencies;
96577657 10use Data::Dumper::Concise;
218b7c12 11use DBICTest::Util 'stacktrace';
556c4fe6 12use constant {
13 CV_TRACING => DBIx::Class::Optional::Dependencies->req_ok_for ('test_leaks_heavy'),
10635a06 14 SKIP_SCALAR_REFS => ( $] > 5.017 ) ? 1 : 0,
556c4fe6 15};
218b7c12 16
17use base 'Exporter';
bf302897 18our @EXPORT_OK = qw(populate_weakregistry assert_empty_weakregistry visit_refs);
218b7c12 19
20my $refs_traced = 0;
96577657 21my $leaks_found = 0;
218b7c12 22my %reg_of_regs;
23
24sub populate_weakregistry {
96577657 25 my ($weak_registry, $target, $note) = @_;
218b7c12 26
27 croak 'Expecting a registry hashref' unless ref $weak_registry eq 'HASH';
28 croak 'Target is not a reference' unless length ref $target;
29
96577657 30 my $refaddr = hrefaddr $target;
8fa57d17 31
96577657 32 # a registry could be fed to itself or another registry via recursive sweeps
33 return $target if $reg_of_regs{$refaddr};
218b7c12 34
85ad63df 35 weaken( $reg_of_regs{ hrefaddr($weak_registry) } = $weak_registry )
36 unless( $reg_of_regs{ hrefaddr($weak_registry) } );
37
38 # an explicit "garbage collection" pass every time we store a ref
39 # if we do not do this the registry will keep growing appearing
40 # as if the traced program is continuously slowly leaking memory
41 for my $reg (values %reg_of_regs) {
42 (defined $reg->{$_}{weakref}) or delete $reg->{$_}
43 for keys %$reg;
44 }
45
10635a06 46 # FIXME/INVESTIGATE - something fishy is going on with refs to plain
47 # strings, perhaps something to do with the CoW work etc...
48 return $target if SKIP_SCALAR_REFS and reftype($target) eq 'SCALAR';
49
96577657 50 if (! defined $weak_registry->{$refaddr}{weakref}) {
51 $weak_registry->{$refaddr} = {
8fa57d17 52 stacktrace => stacktrace(1),
96577657 53 weakref => $target,
8fa57d17 54 };
96577657 55 weaken( $weak_registry->{$refaddr}{weakref} );
8fa57d17 56 $refs_traced++;
218b7c12 57 }
58
8433421f 59 my $desc = refdesc $target;
96577657 60 $weak_registry->{$refaddr}{slot_names}{$desc} = 1;
61 if ($note) {
62 $note =~ s/\s*\Q$desc\E\s*//g;
63 $weak_registry->{$refaddr}{slot_names}{$note} = 1;
64 }
65
218b7c12 66 $target;
67}
68
96577657 69# Regenerate the slots names on a thread spawn
218b7c12 70sub CLONE {
71 my @individual_regs = grep { scalar keys %{$_||{}} } values %reg_of_regs;
72 %reg_of_regs = ();
73
74 for my $reg (@individual_regs) {
96577657 75 my @live_slots = grep { defined $_->{weakref} } values %$reg
218b7c12 76 or next;
cf8fa286 77
cf8fa286 78 $reg = {}; # get a fresh hashref in the new thread ctx
96577657 79 weaken( $reg_of_regs{hrefaddr($reg)} = $reg );
218b7c12 80
96577657 81 for my $slot_info (@live_slots) {
82 my $new_addr = hrefaddr $slot_info->{weakref};
8fa57d17 83
96577657 84 # replace all slot names
85 $slot_info->{slot_names} = { map {
86 my $name = $_;
87 $name =~ s/\(0x[0-9A-F]+\)/sprintf ('(%s)', $new_addr)/ieg;
88 ($name => 1);
89 } keys %{$slot_info->{slot_names}} };
218b7c12 90
96577657 91 $reg->{$new_addr} = $slot_info;
218b7c12 92 }
93 }
94}
95
556c4fe6 96sub visit_refs {
97 my $args = { (ref $_[0]) ? %{$_[0]} : @_ };
98
99 $args->{seen_refs} ||= {};
100
101 my $visited_cnt = '0E0';
102 for my $i (0 .. $#{$args->{refs}} ) {
556c4fe6 103
a42634cd 104 next unless length ref $args->{refs}[$i]; # not-a-ref
556c4fe6 105
a42634cd 106 my $addr = hrefaddr $args->{refs}[$i];
556c4fe6 107
6ae62c5c 108 # no diving into weakregistries
a42634cd 109 next if $reg_of_regs{$addr};
556c4fe6 110
a42634cd 111 next if $args->{seen_refs}{$addr}++;
556c4fe6 112 $visited_cnt++;
a42634cd 113
114 my $r = $args->{refs}[$i];
115
556c4fe6 116 $args->{action}->($r) or next;
117
6ae62c5c 118 # This may end up being necessarry some day, but do not slow things
119 # down for now
120 #if ( defined( my $t = tied($r) ) ) {
121 # $visited_cnt += visit_refs({ %$args, refs => [ $t ] });
122 #}
123
a42634cd 124 my $type = reftype $r;
125
7664b1a0 126 local $@;
127 eval {
7664b1a0 128 if ($type eq 'HASH') {
129 $visited_cnt += visit_refs({ %$args, refs => [ map {
130 ( !isweak($r->{$_}) ) ? $r->{$_} : ()
131 } keys %$r ] });
132 }
133 elsif ($type eq 'ARRAY') {
134 $visited_cnt += visit_refs({ %$args, refs => [ map {
135 ( !isweak($r->[$_]) ) ? $r->[$_] : ()
136 } 0..$#$r ] });
137 }
138 elsif ($type eq 'REF' and !isweak($$r)) {
139 $visited_cnt += visit_refs({ %$args, refs => [ $$r ] });
140 }
141 elsif (CV_TRACING and $type eq 'CODE') {
142 $visited_cnt += visit_refs({ %$args, refs => [ map {
143 ( !isweak($_) ) ? $_ : ()
144 } scalar PadWalker::closed_over($r) ] }); # scalar due to RT#92269
145 }
146 1;
8433421f 147 } or warn "Could not descend into @{[ refdesc $r ]}: $@\n";
556c4fe6 148 }
149 $visited_cnt;
150}
151
a42634cd 152sub visit_namespaces {
153 my $args = { (ref $_[0]) ? %{$_[0]} : @_ };
556c4fe6 154
a42634cd 155 my $visited = 1;
218b7c12 156
a42634cd 157 $args->{package} ||= '::';
158 $args->{package} = '::' if $args->{package} eq 'main';
de4705b7 159
a42634cd 160 if ( $args->{action}->($args->{package}) ) {
218b7c12 161
a42634cd 162 my $base = $args->{package};
163 $base = '' if $base eq '::';
218b7c12 164
218b7c12 165
a42634cd 166 $visited += visit_namespaces({ %$args, package => $_ }) for map
8d73fcd4 167 { $_ =~ /(.+?)::$/ ? "${base}::$1" : () }
a42634cd 168 grep
169 { $_ =~ /(?<!^main)::$/ }
170 do { no strict 'refs'; keys %{ $base . '::'} }
96577657 171 }
218b7c12 172
a42634cd 173 return $visited;
174}
175
176# compiles a list of addresses stored as globals (possibly even catching
177# class data in the form of method closures), so we can skip them further on
178sub symtable_referenced_addresses {
179
180 my $refs_per_pkg;
181
182 my $dummy_addresslist;
183
184 my $seen_refs = {};
185 visit_namespaces(
186 action => sub {
556c4fe6 187
556c4fe6 188 no strict 'refs';
556c4fe6 189
a42634cd 190 my $pkg = shift;
191 $pkg = '' if $pkg eq '::';
192 $pkg .= '::';
193
556c4fe6 194 # the unless regex at the end skips some dangerous namespaces outright
195 # (but does not prevent descent)
a42634cd 196 $refs_per_pkg->{$pkg} += visit_refs (
556c4fe6 197 seen_refs => $seen_refs,
a42634cd 198
199 # FIXME FIXME FIXME
200 # This is so damn odd - if we feed a constsub {1} (or in fact almost
201 # anything other than the actionsub below, any scalarref will show
202 # up as a leak, trapped by... something...
203 # Ideally we should be able to const this to sub{1} and just return
204 # $seen_refs (in fact it is identical to the dummy list at the end of
205 # a run here). Alas this doesn't seem to work, so punt for now...
206 action => sub { ++$dummy_addresslist->{ hrefaddr $_[0] } },
207
556c4fe6 208 refs => [ map { my $sym = $_;
209 # *{"$pkg$sym"}{CODE} won't simply work - MRO-cached CVs are invisible there
210 ( CV_TRACING ? Class::MethodCache::get_cv("${pkg}$sym") : () ),
211
212 ( defined *{"$pkg$sym"}{SCALAR} and length ref ${"$pkg$sym"} and ! isweak( ${"$pkg$sym"} ) )
213 ? ${"$pkg$sym"} : ()
214 ,
a42634cd 215
556c4fe6 216 ( map {
217 ( defined *{"$pkg$sym"}{$_} and ! isweak(defined *{"$pkg$sym"}{$_}) )
218 ? *{"$pkg$sym"}{$_}
219 : ()
220 } qw(HASH ARRAY IO GLOB) ),
a42634cd 221
556c4fe6 222 } keys %$pkg ],
223 ) unless $pkg =~ /^ :: (?:
224 DB | next | B | .+? ::::ISA (?: ::CACHE ) | Class::C3
225 ) :: $/x;
a42634cd 226 }
227 );
556c4fe6 228
a42634cd 229# use Devel::Dwarn;
230# Ddie [ map
231# { { $_ => $refs_per_pkg->{$_} } }
232# sort
233# {$refs_per_pkg->{$a} <=> $refs_per_pkg->{$b} }
234# keys %$refs_per_pkg
235# ];
236
237 $seen_refs;
238}
239
240sub assert_empty_weakregistry {
241 my ($weak_registry, $quiet) = @_;
242
8d73fcd4 243 Sub::Defer::undefer_all();
244
a42634cd 245 # in case we hooked bless any extra object creation will wreak
246 # havoc during the assert phase
247 local *CORE::GLOBAL::bless;
248 *CORE::GLOBAL::bless = sub { CORE::bless( $_[0], (@_ > 1) ? $_[1] : caller() ) };
249
250 croak 'Expecting a registry hashref' unless ref $weak_registry eq 'HASH';
251
252 defined $weak_registry->{$_}{weakref} or delete $weak_registry->{$_}
253 for keys %$weak_registry;
254
255 return unless keys %$weak_registry;
8fa57d17 256
a42634cd 257 my $tb = eval { Test::Builder->new }
258 or croak "Calling assert_empty_weakregistry in $0 without a loaded Test::Builder makes no sense";
8fa57d17 259
a42634cd 260 for my $addr (keys %$weak_registry) {
261 $weak_registry->{$addr}{display_name} = join ' | ', (
262 sort
263 { length $a <=> length $b or $a cmp $b }
264 keys %{$weak_registry->{$addr}{slot_names}}
265 );
266
267 $tb->BAILOUT("!!!! WEAK REGISTRY SLOT $weak_registry->{$addr}{display_name} IS NOT A WEAKREF !!!!")
268 if defined $weak_registry->{$addr}{weakref} and ! isweak( $weak_registry->{$addr}{weakref} );
269 }
270
8d73fcd4 271 # the symtable walk is very expensive
272 # if we are $quiet (running in an END block) we do not really need to be
273 # that thorough - can get by with only %Sub::Quote::QUOTED
274 delete $weak_registry->{$_} for $quiet
275 ? do {
276 my $refs = {};
277 visit_refs (
278 # only look at the closed over stuffs
279 refs => [ grep { length ref $_ } map { values %{$_->[2]} } grep { ref $_ eq 'ARRAY' } values %Sub::Quote::QUOTED ],
280 seen_refs => $refs,
281 action => sub { 1 },
282 );
283 keys %$refs;
284 }
285 : (
286 # full sumtable walk, starting from ::
287 keys %{ symtable_referenced_addresses() }
288 )
289 ;
556c4fe6 290
96577657 291 for my $addr (sort { $weak_registry->{$a}{display_name} cmp $weak_registry->{$b}{display_name} } keys %$weak_registry) {
292
1a44a267 293 next if ! defined $weak_registry->{$addr}{weakref};
294
5dc4301c 295 $leaks_found++ unless $tb->in_todo;
1a44a267 296 $tb->ok (0, "Leaked $weak_registry->{$addr}{display_name}");
297
298 my $diag = do {
299 local $Data::Dumper::Maxdepth = 1;
300 sprintf "\n%s (refcnt %d) => %s\n",
301 $weak_registry->{$addr}{display_name},
302 refcount($weak_registry->{$addr}{weakref}),
303 (
304 ref($weak_registry->{$addr}{weakref}) eq 'CODE'
305 and
306 B::svref_2object($weak_registry->{$addr}{weakref})->XSUB
307 ) ? '__XSUB__' : Dumper( $weak_registry->{$addr}{weakref} )
308 ;
218b7c12 309 };
1a44a267 310
5dc4301c 311 # FIXME - need to add a circular reference seeker based on the visitor
312 # (will need a bunch of modifications, punting with just a stub for now)
313
556c4fe6 314 $diag .= Devel::FindRef::track ($weak_registry->{$addr}{weakref}, 50) . "\n"
1a44a267 315 if ( $ENV{TEST_VERBOSE} && eval { require Devel::FindRef });
316
317 $diag =~ s/^/ /mg;
318
319 if (my $stack = $weak_registry->{$addr}{stacktrace}) {
320 $diag .= " Reference first seen$stack";
321 }
322
323 $tb->diag($diag);
6ae62c5c 324
325# if ($leaks_found == 1) {
326# # using the fh dumper due to intermittent buffering issues
327# # in case we decide to exit soon after (possibly via _exit)
328# require Devel::MAT::Dumper;
329# local $Devel::MAT::Dumper::MAX_STRING = -1;
330# open( my $fh, '>:raw', "leaked_${addr}_pid$$.pmat" ) or die $!;
331# Devel::MAT::Dumper::dumpfh( $fh );
332# close ($fh) or die $!;
333#
334# use POSIX;
335# POSIX::_exit(1);
336# }
1a44a267 337 }
338
5dc4301c 339 if (! $quiet and !$leaks_found and ! $tb->in_todo) {
1a44a267 340 $tb->ok(1, sprintf "No leaks found at %s line %d", (caller())[1,2] );
218b7c12 341 }
342}
343
344END {
345 if ($INC{'Test/Builder.pm'}) {
346 my $tb = Test::Builder->new;
347
348 # we check for test passage - a leak may be a part of a TODO
349 if ($leaks_found and !$tb->is_passing) {
350
351 $tb->diag(sprintf
352 "\n\n%s\n%s\n\nInstall Devel::FindRef and re-run the test with set "
353 . '$ENV{TEST_VERBOSE} (prove -v) to see a more detailed leak-report'
354 . "\n\n%s\n%s\n\n", ('#' x 16) x 4
355 ) if ( !$ENV{TEST_VERBOSE} or !$INC{'Devel/FindRef.pm'} );
356
357 }
358 else {
359 $tb->note("Auto checked $refs_traced references for leaks - none detected");
360 }
361 }
362}
363
3641;