File: Refcount.pm

package info (click to toggle)
perl 5.42.0-2
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 128,392 kB
  • sloc: perl: 534,963; ansic: 240,563; sh: 72,042; pascal: 6,934; xml: 2,428; yacc: 1,360; makefile: 1,197; cpp: 208; lisp: 1
file content (310 lines) | stat: -rw-r--r-- 8,759 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
#  You may distribute under the terms of either the GNU General Public License
#  or the Artistic License (the same terms as Perl itself)
#
#  (C) Paul Evans, 2008-2023 -- leonerd@leonerd.org.uk

package Test2::Tools::Refcount;

use strict;
use warnings;

use Test2::API qw(context release);

use Scalar::Util qw( weaken refaddr );
use B qw( svref_2object );

our $VERSION = '1.302210';

our @EXPORT = qw(
   is_refcount
   is_oneref
);

our @EXPORT_OK = qw(
   refcount
);

use base qw(Exporter);

use constant HAVE_DEVEL_MAT_DUMPER => defined eval {
    package # No Index
        Devel::MAT::Dumper;
    our $HELPER_PER_PACKAGE;
    our $HELPER_PER_MAGIC;
    require Devel::MAT::Dumper;
};

=encoding UTF-8

=head1 NAME

C<Test2::Tools::Refcount> - assert reference counts on objects

=head1 SYNOPSIS

   use Test2::Tools::Refcount;

   use Some::Class;
   my $object = Some::Class->new();

   is_oneref( $object, '$object has a refcount of 1' );

   my $otherref = $object;

   is_refcount( $object, 2, '$object now has 2 references' );

=head1 DESCRIPTION

The Perl garbage collector uses simple reference counting during the normal
execution of a program. This means that cycles or unweakened references in
other parts of code can keep an object around for longer than intended. To
help avoid this problem, the reference count of a new object from its class
constructor ought to be 1. This way, the caller can know the object will be
properly DESTROYed when it drops all of its references to it.

This module provides two test functions to help ensure this property holds
for an object class, so as to be polite to its callers.

If the assertion fails; that is, if the actual reference count is different to
what was expected, either of the following two modules may be used to assist
the developer in finding where the references are.

=over 4

=item *

If L<Devel::MAT> is installed, this test module will use it to dump the state
of the memory after a failure. It will create a F<.pmat> file named the same
as the unit test, but with the trailing F<.t> suffix replaced with
F<-TEST.pmat> where C<TEST> is the number of the test that failed (in case
there was more than one).

=back

See the examples below for more information.

=cut

=head1 FUNCTIONS

=cut

=head2 is_refcount

   is_refcount( $object, $count, $name )

Test that $object has $count references to it.

=cut

sub is_refcount($$;$)
{
   my ( $object, $count, $name ) = @_;
   @_ = ();

   my $ctx = context();

   if( !ref $object ) {
      my $ok = $ctx->ok( 0, $name );
      $ctx->diag( "  expected a reference, was not given one" );
      $ctx->release;
      return $ok;
   }

   weaken $object; # So this reference itself doesn't show up

   my $REFCNT = refcount( $object );

   my $ok = $ctx->ok( $REFCNT == $count, $name );

   unless( $ok->pass ) {
      $ctx->diag( "  expected $count references, found $REFCNT" );

      if( HAVE_DEVEL_MAT_DUMPER ) {
         my $file = $0;
         my $hub = $ctx->hub;
         my $num = $hub->count;

         # Trim the .t off first then append -$num.pmat, in case $0 wasn't a .t file
         $file =~ s/\.(?:t|pm|pl)$//;
         $file .= "-$num\.pmat";
         $ctx->diag( sprintf "SV address is 0x%x", refaddr $object );
         $ctx->diag( "Writing heap dump to $file" );
         Devel::MAT::Dumper::dump( $file );
      }
   }

   $ctx->release;
   return $ok;
}

=head2 is_oneref

   is_oneref( $object, $name )

Assert that the $object has only 1 reference to it.

=cut

sub is_oneref($;$)
{
   splice( @_, 1, 0, ( 1 ) );
   goto &is_refcount;
}

=head2 refcount

   $count = refcount( $object )

Returns the reference count of the given object as used by the test functions.
This is useful for making tests that don't care what the count is before they
start, but simply assert that the count hasn't changed by the end.

   use Test2::Tools::Refcount import => [qw( is_refcount refcount )];
   {
      my $count = refcount( $object );

      do_something( $object );

      is_refcount( $object, $count, 'do_something() preserves refcount' );
   }

=cut

sub refcount
{
   return svref_2object( $_[0] )->REFCNT;
}

=head1 EXAMPLE

Suppose, having written a new class C<MyBall>, you now want to check that its
constructor and methods are well-behaved, and don't leak references. Consider
the following test script:

   use Test::More tests => 2;
   use Test2::Tools::Refcount;

   use MyBall;

   my $ball = MyBall->new();
   is_oneref( $ball, 'One reference after construct' );

   $ball->bounce;

   # Any other code here that might be part of the test script

   is_oneref( $ball, 'One reference just before EOF' );

The first assertion is just after the constructor, to check that the reference
returned by it is the only reference to that object. This fact is important if
we ever want C<DESTROY> to behave properly. The second call is right at the
end of the file, just before the main scope closes. At this stage we expect
the reference count also to be one, so that the object is properly cleaned up.

Suppose, when run, this produces the following output (presuming
L<Devel::MAT::Dumper> is available):

   1..2
   ok 1 - One reference after construct
   not ok 2 - One reference just before EOF
   #   Failed test 'One reference just before EOF'
   #   at ex.pl line 26.
   #   expected 1 references, found 2
   # SV address is 0x55e14c310278
   # Writing heap dump to ex-2.pmat
   # Looks like you failed 1 test of 2.

This has written a F<ex-2.pmat> file we can load using the C<pmat> shell and
use the C<identify> command on the given address to find where it went:

   $ pmat ex-2.pmat 
   Perl memory dumpfile from perl 5.28.1 threaded
   Heap contains 25233 objects
   pmat> identify 0x55e14c310278
   HASH(0)=MyBall at 0x55e14c310278 is:
   ├─(via RV) the lexical $ball at depth 1 of CODE() at 0x55e14c3104a0=main_cv, which is:
   │ └─the main code
   └─(via RV) value {self} of HASH(2) at 0x55e14cacb860, which is (*A):
     └─(via RV) value {cycle} of HASH(2) at 0x55e14cacb860, which is:
       itself

(This document isn't intended to be a full tutorial on L<Devel::MAT> and the
C<pmat> shell; for that see L<Devel::MAT::UserGuide>).

From this output, we can see that the constructor was well-behaved, but that a
reference was leaked by the end of the script - the reference count was 2,
when we expected just 1. Reading the trace output, we can see that there were
2 references that could be found - one stored in the $ball lexical in the main
program, and one stored in a HASH. Since we expected to find the $ball lexical
variable, we know we are now looking for a leak in a hash somewhere in the
code. From reading the test script, we can guess this leak is likely to be in
the bounce() method. Furthermore, we know that the reference to the object
will be stored in a HASH in a member called C<self>.

By reading the code which implements the bounce() method, we can see this is
indeed the case:

   sub bounce
   {
      my $self = shift;
      my $cycle = { self => $self };
      $cycle->{cycle} = $cycle;
   }

From reading the tracing output, we find that the HASH this object is
referenced in also contains a reference to itself, in a member called
C<cycle>. This comes from the last line in this function, a line that
purposely created a cycle, to demonstrate the point. While a real program
probably wouldn't do anything quite this obvious, the trace would still be
useful in finding the likely cause of the leak.

If C<Devel::MAT::Dumper> is not available, then these detailed traces will not
be produced. The basic reference count testing will still take place, but a
smaller message will be produced:

   1..2
   ok 1 - One reference after construct
   not ok 2 - One reference just before EOF
   #   Failed test 'One reference just before EOF'
   #   at demo.pl line 16.
   #   expected 1 references, found 2
   # Looks like you failed 1 test of 2.

=head1 BUGS

=over 4

=item * Temporaries created on the stack

Code which creates temporaries on the stack, to be released again when the
called function returns does not work correctly on perl 5.8 (and probably
before). Examples such as

   is_oneref( [] );

may fail and claim a reference count of 2 instead.

Passing a variable such as

   my $array = [];
   is_oneref( $array );

works fine. Because of the intention of this test module; that is, to assert
reference counts on some object stored in a variable during the lifetime of
the test script, this is unlikely to cause any problems.

=back

=head1 ACKNOWLEDGEMENTS

Peter Rabbitson <ribasushi@cpan.org> - for suggesting using core's C<B>
instead of C<Devel::Refcount> to obtain refcounts

=head1 AUTHOR

Paul Evans <leonerd@leonerd.org.uk>

=cut

0x55AA;