File Coverage

blib/lib/Test/MemoryGrowth.pm
Criterion Covered Total %
statement 45 46 97.8
branch 5 6 83.3
condition 6 6 100.0
subroutine 6 6 100.0
pod 1 2 50.0
total 63 66 95.4


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2010-2019 -- leonerd@leonerd.org.uk
5              
6             package Test::MemoryGrowth;
7              
8 3     3   139484 use strict;
  3         20  
  3         86  
9 3     3   19 use warnings;
  3         6  
  3         135  
10 3     3   20 use base qw( Test::Builder::Module );
  3         5  
  3         581  
11              
12             our $VERSION = '0.03';
13              
14             our @EXPORT = qw(
15             no_growth
16             );
17              
18 3     3   23 use constant HAVE_DEVEL_MAT_DUMPER => defined eval { require Devel::MAT::Dumper };
  3         6  
  3         6  
  3         1511  
19              
20             =head1 NAME
21              
22             C - assert that code does not cause growth in memory usage
23              
24             =head1 SYNOPSIS
25              
26             use Test::More;
27             use Test::MemoryGrowth;
28              
29             use Some::Class;
30              
31             no_growth {
32             my $obj = Some::Class->new;
33             } 'Constructing Some::Class does not grow memory';
34              
35             my $obj = Some::Class->new;
36             no_growth {
37             $obj->do_thing;
38             } 'Some::Class->do_thing does not grow memory';
39              
40              
41             #### This test will fail ####
42             my @list;
43             no_growth {
44             push @list, "Hello world";
45             } 'pushing to an array does not grow memory';
46              
47             done_testing;
48              
49             =head1 DESCRIPTION
50              
51             This module provides a function to check that a given block of code does not
52             result in the process consuming extra memory once it has finished. Despite the
53             name of this module it does not, in the strictest sense of the word, test for a
54             memory leak: that term is specifically applied to cases where memory has been
55             allocated but all record of it has been lost, so it cannot possibly be
56             reclaimed. While the method employed by this module can detect such bugs, it
57             can also detect cases where memory is still referenced and reachable, but the
58             usage has grown more than would be expected or necessary.
59              
60             The block of code will be run a large number of times (by default 10,000), and
61             the difference in memory usage by the process before and after is compared. If
62             the memory usage has now increased by more than one byte per call, then the
63             test fails.
64              
65             In order to give the code a chance to load initial resources it needs, it will
66             be run a few times first (by default 10); giving it a chance to load files,
67             AUTOLOADs, caches, or any other information that it requires. Any extra memory
68             usage here will not count against it.
69              
70             This simple method is not a guaranteed indicator of the absence of memory
71             resource bugs from a piece of code; it has the possibility to fail in both a
72             false-negative and a false-positive way.
73              
74             =over 4
75              
76             =item False Negative
77              
78             It is possible that a piece of code causes memory usage growth that this
79             module does not detect. Because it only detects memory growth of at least one
80             byte per call, it cannot detect cases of linear memory growth at lower rates
81             than this. Most memory usage growth comes either from Perl-level or C-level
82             bugs where memory objects are created at every call and not reclaimed again.
83             (These are either genuine memory leaks, or needless allocations of objects
84             that are stored somewhere and never reclaimed). It is unlikely such a bug
85             would result in a growth rate smaller than one byte per call.
86              
87             A second failure case comes from the fact that memory usage is taken from the
88             Operating System's measure of the process's Virtual Memory size, so as to be
89             able to detect memory usage growth in C libraries or XS-level wrapping code,
90             as well as Perl functions. Because Perl does not agressively return unused
91             memory to the Operating System, it is possible that a piece of code could use
92             un-allocated but un-reclaimed memory to grow into; resulting in an increase in
93             its requirements despite not requesting extra memory from the Operating
94             System.
95              
96             =item False Positive
97              
98             It is possible that the test will claim that a function grows in memory, when
99             the behaviour is in fact perfectly normal for the code in question. For
100             example, the code could simply be some function whose behaviour is required to
101             store extra state; for example, adding a new item into a list. In this case it
102             is in fact expected that the memory usage of the process will increase.
103              
104             =back
105              
106             By careful use of this test module, false indications can be minimised. By
107             splitting tests across many test scripts, each one can be started in a new
108             process state, where most of the memory assigned from the Operating System is
109             in use by Perl, so anything extra that the code requires will have to request
110             more. This should reduce the false negative indications.
111              
112             By keeping in mind that the module simply measures the change in allocated
113             memory size, false positives can be minimised, by not attempting to assert
114             that certain pieces of code do not grow in memory, when in fact it would be
115             expected that they do.
116              
117             =head2 Devel::MAT Integration
118              
119             If L is installed, this test module will use it to dump the state
120             of the memory after a failure. It will create a F<.pmat> file named the same
121             as the unit test, but with the trailing F<.t> suffix replaced with
122             F<-TEST.pmat> where C is the number of the test that failed (in case
123             there was more than one). It will then run the code under test one more time,
124             before writing another file whose name is suffixed with F<-TEST-after.pmat>.
125             This pair of files may be useful for differential analysis.
126              
127             =cut
128              
129             =head1 FUNCTIONS
130              
131             =cut
132              
133             sub get_memusage
134             {
135             # TODO: This implementation sucks piggie. Write a proper one
136 8 50   8 0 425 open( my $statush, "<", "/proc/self/status" ) or die "Cannot open status - $!";
137              
138 8   100     860 m/^VmSize:\s+([0-9]+) kB/ and return $1 for <$statush>;
139              
140 0         0 die "Unable to determine VmSize\n";
141             }
142              
143             =head2 no_growth
144              
145             no_growth { CODE } %opts, $name
146              
147             Assert that the code block does not consume extra memory.
148              
149             Takes the following named arguments:
150              
151             =over 8
152              
153             =item calls => INT
154              
155             The number of times to call the code during growth testing.
156              
157             =item burn_in => INT
158              
159             The number of times to call the code initially, before watching for memory
160             usage.
161              
162             =back
163              
164             =cut
165              
166             sub no_growth(&@)
167             {
168 4     4 1 5063 my $code = shift;
169 4 100       9 my $name; $name = pop if @_ % 2;
  4         19  
170 4         13 my %args = @_;
171              
172 4         32 my $tb = __PACKAGE__->builder;
173              
174 4   100     69 my $burn_in = $args{burn_in} || 10;
175 4   100     17 my $calls = $args{calls} || 10_000;
176              
177 4         9 my $i = 0;
178 4         16 $code->() while $i++ < $burn_in;
179              
180 4         75 my $before_usage = get_memusage;
181              
182 4         21 $i = 0;
183 4         24 $code->() while $i++ < $calls;
184              
185 4         46704 my $after_usage = get_memusage;
186              
187 4         45 my $increase = $after_usage - $before_usage;
188             # in bytes
189 4         11 $increase *= 1024;
190              
191             # Even if we increased in memory usage, it's OK as long as we didn't gain
192             # more than one byte per call
193 4         27 my $ok = $tb->ok( $increase < $calls, $name );
194              
195 4 100       2455 unless( $ok ) {
196 1         27 $tb->diag( sprintf "Lost %d bytes of memory over %d calls, average of %.2f per call",
197             $increase, $calls, $increase / $calls );
198              
199 1         251 if( HAVE_DEVEL_MAT_DUMPER ) {
200 1         5 my $file = $0;
201 1         4 my $num = $tb->current_test;
202              
203             # Trim the .t off first then append -$num.pmat, in case $0 wasn't a .t file
204 1         123 $file =~ s/\.(?:t|pm|pl)$//;
205              
206 1         6 my $beforefile = "$file-$num.pmat";
207 1         5 my $afterfile = "$file-$num-after.pmat";
208              
209 1         6 $tb->diag( "Writing heap dump to $beforefile" );
210 1         55373 Devel::MAT::Dumper::dump( $beforefile );
211              
212 1         23 $code->();
213              
214 1         27 $tb->diag( "Writing heap dump after one more iteration to $afterfile" );
215 1         56546 Devel::MAT::Dumper::dump( $afterfile );
216             }
217             }
218              
219 4         30 return $ok;
220             }
221              
222             =head1 TODO
223              
224             =over 8
225              
226             =item * Don't be Linux Specific
227              
228             Currently, this module uses a very Linux-specific method of determining
229             process memory usage (namely, by inspecting F). This should
230             really be fixed to some OS-neutral abstraction. Currently I am unaware of a
231             simple portable mechanism to query this. Patches very much welcome. :)
232              
233             =back
234              
235             =head1 AUTHOR
236              
237             Paul Evans
238              
239             =cut
240              
241             0x55AA;