File Coverage

blib/lib/Test/NoLeaks.pm
Criterion Covered Total %
statement 87 98 88.7
branch 24 44 54.5
condition 21 35 60.0
subroutine 13 13 100.0
pod 2 2 100.0
total 147 192 76.5


line stmt bran cond sub pod time code
1             package Test::NoLeaks;
2              
3 5     5   105824 use strict;
  5         7  
  5         117  
4 5     5   15 use warnings;
  5         5  
  5         112  
5 5     5   1700 use POSIX qw/sysconf _SC_PAGESIZE/;
  5         19612  
  5         22  
6 5     5   3205 use Test::Builder;
  5         5  
  5         84  
7 5     5   18 use Test::More;
  5         5  
  5         27  
8              
9             our $VERSION = '0.06';
10              
11 5     5   980 use base qw(Exporter);
  5         8  
  5         737  
12              
13             our @EXPORT = qw/test_noleaks/; ## no critic (ProhibitAutomaticExportation)
14             our @EXPORT_OK = qw/noleaks/;
15              
16             =head1 NAME
17              
18             Test::NoLeaks - Memory and file descriptor leak detector
19              
20             =head1 SYNOPSYS
21              
22             use Test::NoLeaks;
23              
24             test_noleaks (
25             code => sub{
26             # code that might leak
27             },
28             track_memory => 1,
29             track_fds => 1,
30             passes => 2,
31             warmup_passes => 1,
32             tolerate_hits => 0,
33             );
34              
35             Sample output:
36             # pass 1, leaked: 225280 bytes 0 file descriptors
37             # pass 36, leaked: 135168 bytes 0 file descriptors
38             # pass 52, leaked: 319488 bytes 0 file descriptors
39             # pass 84, leaked: 135168 bytes 0 file descriptors
40             # pass 98, leaked: 155648 bytes 0 file descriptors
41             not ok 1214 - Leaked 970752 bytes (5 hits) 0 file descriptors
42              
43             test_noleaks (
44             code => sub { ... },
45             track_memory => 1,
46             passes => 2,
47             );
48              
49             # old-school way
50             use Test::More;
51             use Test::NoLeaks qw/noleaks/;
52             ok noleaks(
53             code => sub { ... },
54             track_memory => ...,
55             track_fds => ...,
56             passes => ...,
57             warmup_passes => ...,
58             ), "non-leaked code description";
59              
60             =head1 DESCRIPTION
61              
62             It is hard to track memory leaks. There are a lot of perl modules (e.g.
63             L), that try to B and B leaks. Unfortunately,
64             they do not always work, and they are rather limited because they are not
65             able to detect leaks in XS-code or external libraries.
66              
67             Instead of examining perl internals, this module offers a bit naive empirical
68             approach: let the suspicious code to be launched in infinite loop
69             some time and watch (via tools like C)if the memory consumption by
70             perl process increses over time. If it does, while it is expected to
71             be constant (stabilized), then, surely, there are leaks.
72              
73             This approach is able only to B and not able to B them. The
74             module C implements the general idea of the approach, which
75             might be enough in many cases.
76              
77             =head1 INTERFACE
78              
79             =head3 C<< test_noleaks >>
80              
81             =head3 C<< noleaks >>
82              
83             The mandatory hash has the following members
84              
85             =over 2
86              
87             =item * C
88              
89             Suspicious for leaks subroutine, that will be executed multiple times.
90              
91             =item * C
92              
93             =item * C
94              
95             Track memory or file descriptor leaks. At leas one of them should be
96             specified.
97              
98             In B, every socket is file descriptor too, so, C
99             will be able to track unclosed sockets, i.e. network connections.
100              
101             =item * C
102              
103             How many times C should be executed. If memory leak is too small,
104             number of passes should be large enough to trigger additional pages
105             allocation for perl process, and the leak will be detected.
106              
107             Page size is 4kb on linux, so, if C leaks 4 bytes on every
108             pass, then C<1024> passes should be specified.
109              
110             In general, the more passes are specified, the more chance to
111             detect possible leaks.
112              
113             It is good idea to initally define C to some large number,
114             e.g. C<10_000> to be sure, that the suspicious code leaks, but then
115             decrease to some smaller number, enough to produce test fail report,
116             i.e. enough to produces 3-5 memory hits (additional pages allocations).
117             This will speed up tests execution and will save CO2 atmospheric
118             emissions a little bit.
119              
120             Default value is C<100>. Minimal value is C<2>.
121              
122             =item * C
123              
124             How many times the C should be executed before module starts
125             tracking resources consumption on executing the C C
126             times.
127              
128             If you have caches, memoizes etc., then C is your
129             friend.
130              
131             Default value is C<0>.
132              
133             =item * C
134              
135             How many passes, which considered leaked, should be ingnored, i.e.
136             maximal number of possible false leak reports.
137              
138             Even if your code has no leaks, it might cause perl interpreter
139             allocate additional memory pages, e.g. due to memory fragmentation.
140             Those allocations are legal, and should not be treated as leaks.
141              
142             Use this B when memory leaks are already fixed, but there
143             are still false leak reports from C. This value expected
144             to be small enough, i.e. C<1> or C<2>. For additional assurance, please,
145             increase C value, if C is non-zero.
146              
147             Default value is C<0>.
148              
149             =back
150              
151             =head1 MEMORY LEAKS TESTING TECHNIQUES
152              
153             C can be used to test web applications for memory leaks.
154              
155             Let's consider you have the following suspicious code
156              
157             sub might_leak {
158             my $t = Test::Mojo->new('MyApp');
159             $t->post_ok('/search.json' => form => {q => 'Perl'})
160             ->status_is(200);
161             ...;
162             }
163              
164             test_noleaks (
165             code => \&might_leak,
166             track_memory => 1,
167             track_fds => 1,
168             passes => 1000,
169             );
170              
171             The C subroutine isn't optimal for leak detection, because it
172             mixes infrastructure-related code (application) with request code. Let's
173             consider, that there is a leak: every request creates some data and puts
174             it into application, but forgets to do clean up. As soon as the application
175             is re-created on every pass, the leaked data might be destroyed together
176             with the application, and leak might remain undetected.
177              
178             So, the code under test should look much more production like, i.e.
179              
180             my $t = Test::Mojo->new('MyApp');
181             ok($t);
182             sub might_leak {
183             $t->post_ok('/search.json' => form => {q => 'Perl'})
184             ->status_is(200);
185             ...;
186             }
187              
188             That way web-application is created only once, and leaks will be tracked
189             on request-related code.
190              
191             Anyway, C still wrong, because it unintentionally leaks due to
192             use of direct or indirect L functions, like C or
193             C. They should not be used; if you still need to assert, that
194             C works propertly, you can use C subroutine,
195             to cancel any further testing, e.g.
196              
197             sub might_leak {
198             my $got = some_function_might_leak;
199             my $expected = "some_value";
200             BAIL_OUT('some_function_might_leak does not work propertly!')
201             unless $got eq $expected;
202             }
203              
204              
205              
206             Please, B use C more then once per test file. Consider
207             the following example:
208              
209             # (A)
210             test_noleaks(
211             code => &does_not_leak_but_consumes_large_amount_of_memory,
212             ...,
213             )
214              
215             # (B)
216             test_noleaks(
217             code => &leaks_but_consumes_small_amount_of_memory,
218             ...
219             )
220              
221             In A-case OS already allocated large amount of memory for Perl interpreter.
222             In case-B perl might just re-use them, without allocating new ones, and
223             this will be false negative, i.e. memory leak might B be reported.
224              
225              
226             =head1 LIMITATIONS
227              
228             =over 2
229              
230             =item * Currently it works propertly only on B
231              
232             Patches or pull requests to support other OSes are welcome.
233              
234             =item * The module will not work propertly in Bed child
235              
236             It seems a little bit strange to use C or
237             C in forked child, but if you really need that, please,
238             send PR.
239              
240             =back
241              
242             =head1 SEE ALSO
243              
244             L
245              
246             =cut
247              
248             my $PAGE_SIZE;
249              
250             BEGIN {
251 5     5   23 no strict "subs"; ## no critic (ProhibitNoStrict ProhibitProlongedStrictureOverride)
  5         7  
  5         960  
252              
253 5 50   5   43 $PAGE_SIZE = sysconf(_SC_PAGESIZE)
254             or die("page size cannot be determined, Test::NoLeaks cannot be used");
255              
256 5 50       234 open(my $statm, '<', '/proc/self/statm') ## no critic (RequireBriefOpen)
257             or die("couldn't access /proc/self/status : $!");
258             *_platform_mem_size = sub {
259 30081     30081   46842 my $line = <$statm>;
260 30081         27946 seek($statm, 0, 0);
261 30081         45958 my ($pages) = (split / /, $line)[0];
262 30081         36638 return $pages * $PAGE_SIZE;
263 5         23 };
264              
265 5         6 my $fd_dir = '/proc/self/fd';
266 5 50       158 opendir(my $dh, $fd_dir)
267             or die "can't opendir $fd_dir: $!";
268             *_platform_fds = sub {
269 30074     30074   96660 my $open_fd_count = () = readdir($dh);
270 30074         32928 rewinddir($dh);
271 30074         20710 return $open_fd_count;
272 5         2734 };
273             }
274              
275             sub _noleaks {
276 12     12   23 my %args = @_;
277              
278             # check arguments
279 12         19 my $code = $args{code};
280 12 50 33     54 die("code argument (CODEREF) isn't provided")
281             if (!$code || !(ref($code) eq 'CODE'));
282              
283 12         14 my $track_memory = $args{'track_memory'};
284 12         13 my $track_fds = $args{'track_fds'};
285 12 50 66     28 die("don't know what to track (i.e. no 'track_memory' nor 'track_fds' are specified)")
286             if (!$track_memory && !$track_fds);
287              
288 12   50     29 my $passes = $args{passes} || 100;
289 12 50       22 die("passes count too small (should be at least 2)")
290             if $passes < 2;
291              
292 12   100     36 my $warmup_passes = $args{warmup_passes} || 0;
293 12 50       23 die("warmup_passes count too small (should be non-negative)")
294             if $passes < 0;
295              
296             # warm-up phase
297             # a) warm up code
298 12         38 $code->() for (1 .. $warmup_passes);
299              
300             # b) warm-up package itself, as it might cause additional memory (re) allocations
301             # (ignore results)
302 12 100       2185 _platform_mem_size if $track_memory;
303 12 100       35 _platform_fds if $track_fds;
304 12         144 my @leaked_at = map { [0, 0] } (1 .. $passes); # index: pass, value array[$mem_leak, $fds_leak]
  15042         12686  
305              
306             # pre-allocate all variables, including those, which are used in cycle only
307 12         444 my ($total_mem_leak, $total_fds_leak, $memory_hits) = (0, 0, 0);
308 12         17 my ($mem_t0, $fds_t0, $mem_t1, $fds_t1) = (0, 0, 0, 0);
309              
310             # execution phase
311 12         77 for my $pass (0 .. $passes - 1) {
312 15042 100       20000 $mem_t0 = _platform_mem_size if $track_memory;
313 15042 100       20995 $fds_t0 = _platform_fds if $track_fds;
314 15042         15087 $code->();
315 15042 100       510378 $mem_t1 = _platform_mem_size if $track_memory;
316 15042 100       20533 $fds_t1 = _platform_fds if $track_fds;
317              
318 15042         9826 my $leaked_mem = $mem_t1 - $mem_t0;
319 15042 50       17501 $leaked_mem = 0 if ($leaked_mem < 0);
320              
321 15042         8847 my $leaked_fds = $fds_t1 - $fds_t0;
322 15042 50       15132 $leaked_fds = 0 if ($leaked_fds < 0);
323              
324 15042         11747 $leaked_at[$pass]->[0] = $leaked_mem;
325 15042         8695 $leaked_at[$pass]->[1] = $leaked_fds;
326 15042         9655 $total_mem_leak += $leaked_mem;
327 15042         8246 $total_fds_leak += $leaked_fds;
328              
329 15042 100       18119 $memory_hits++ if ($leaked_mem > 0);
330             }
331              
332 12         82 return ($total_mem_leak, $total_fds_leak, $memory_hits, \@leaked_at);
333             }
334              
335             sub noleaks(%) { ## no critic (ProhibitSubroutinePrototypes)
336 11     11 1 79 my %args = @_;
337              
338 11         33 my ($mem, $fds, $mem_hits) = _noleaks(%args);
339              
340 11   100     1308 my $tolerate_hits = $args{tolerate_hits} || 0;
341 11         15 my $track_memory = $args{'track_memory'};
342 11         18 my $track_fds = $args{'track_fds'};
343              
344 11   100     44 my $has_fd_leaks = $track_fds && ($fds > 0);
345 11   100     57 my $has_mem_leaks = $track_memory && ($mem > 0) && ($mem_hits > $tolerate_hits);
346 11   100     95 return !($has_fd_leaks || $has_mem_leaks);
347             }
348              
349             sub test_noleaks(%) { ## no critic (ProhibitSubroutinePrototypes)
350 1     1 1 16 my %args = @_;
351 1         7 my ($mem, $fds, $mem_hits, $details) = _noleaks(%args);
352              
353 1   50     6 my $tolerate_hits = $args{tolerate_hits} || 0;
354 1         3 my $track_memory = $args{'track_memory'};
355 1         1 my $track_fds = $args{'track_fds'};
356              
357 1   33     6 my $has_fd_leaks = $track_fds && ($fds > 0);
358 1   33     5 my $has_mem_leaks = $track_memory && ($mem > 0) && ($mem_hits > $tolerate_hits);
359 1   33     3 my $has_leaks = $has_fd_leaks || $has_mem_leaks;
360              
361 1         2 local $Test::Builder::Level = $Test::Builder::Level + 1;
362 1 50       3 if (!$has_leaks) {
363 1         5 pass("no leaks have been found");
364             } else {
365 0 0       0 my $summary = "Leaked " . ($track_memory ? "$mem bytes ($mem_hits hits) " : "") . ($track_fds ? "$fds file descriptors" : "");
    0          
366              
367 0         0 my @lines;
368 0         0 for my $pass (1 .. @$details) {
369 0         0 my $v = $details->[$pass - 1];
370 0         0 my ($mem, $fds) = @$v;
371 0 0 0     0 if ($mem || $fds) {
372 0 0       0 my $line = "pass $pass, leaked: " . ($track_memory ? $mem . " bytes " : "") . ($track_fds ? $fds . " file descriptors" : "");
    0          
373 0         0 push @lines, $line;
374             }
375             }
376 0         0 my $report = join("\n", @lines);
377              
378 0         0 note($report);
379 0         0 fail("$summary");
380             }
381 1         327 return;
382             }
383              
384             =head1 SOURCE CODE
385              
386             L
387              
388             =head1 AUTHOR
389              
390             binary.com, C<< >>
391              
392             =head1 BUGS
393              
394             Please report any bugs or feature requests to
395             L.
396              
397             =cut
398              
399             1;