File Coverage

blib/lib/Test/MemoryGrowth.pm
Criterion Covered Total %
statement 41 42 97.6
branch 5 6 83.3
condition 6 6 100.0
subroutine 6 6 100.0
pod 1 2 50.0
total 59 62 95.1


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