File Coverage

blib/lib/Devel/Size/Report.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Devel::Size::Report;
2              
3             require 5.006;
4              
5             $VERSION = '0.13';
6              
7 9     9   323650 use Devel::Size qw(size total_size);
  0            
  0            
8             use Scalar::Util qw/reftype refaddr blessed dualvar isweak readonly isvstring/;
9             use Time::HiRes qw/time/;
10             use Array::RefElem qw/hv_store av_push/;
11             use Devel::Peek qw/SvREFCNT/;
12              
13             require Exporter;
14             @ISA = qw/Exporter/;
15             @EXPORT_OK = qw/
16             report_size track_size element_type type entries_per_element track_sizes
17             hide_tracks
18              
19             S_SCALAR
20             S_HASH
21             S_ARRAY
22             S_GLOB
23             S_UNKNOWN
24             S_CODE
25             S_LVALUE
26             S_REGEXP
27             S_CYCLE
28             S_DOUBLE
29             S_VSTRING
30            
31             SF_WEAK
32             SF_KEY
33             SF_REF
34             SF_WEAK
35             SF_RO
36             SF_DUAL
37             SF_MAGIC
38              
39             /;
40              
41             use strict;
42              
43             #############################################################################
44             # The following should not be global to be thread safe:
45              
46             # If somebody used hv_store, we need also to enter hash key addresses into
47             # SEEN. Default is off, because this wastes memory.
48             my $TRACK_DOUBLES = 0;
49              
50             # _track_size() stores it's result here:
51             my @sizes;
52              
53             # for cycles in memory:
54             my %SEEN;
55              
56             # count calls to track_size for statistics
57             my $CALLS;
58              
59             #############################################################################
60             # The overhead for one ref. Used to correct the results from Devel::Size.
61             my $SIZE_OF_REF;
62              
63             BEGIN
64             {
65             # disable any warnings Devel::Size might spill
66             $Devel::Size::warn = 0;
67              
68             # Devel::Size will dereference arguments, so it misses the size of the
69             # reference. Compute the size for \\0 and \0 and infer the overhead for
70             # one reference from that. Thanx to SADAHIRO Tomoyuki.
71              
72             $SIZE_OF_REF = total_size(\\0) - total_size(\0);
73             }
74              
75             # scalar that can be entered into %SEEN many times:
76             my $UNDEF = undef;
77             # scalar that can be entered into @sizes many times:
78             my $ZERO = 0;
79              
80             # the different types of elements
81             use constant {
82             S_UNKNOWN => 0,
83             S_CYCLE => 1,
84             S_SCALAR => 2,
85             S_ARRAY => 3,
86             S_HASH => 4,
87             S_GLOB => 5,
88             S_CODE => 6,
89             S_REGEXP => 7,
90             S_LVALUE => 8,
91             S_DOUBLE => 9,
92             S_VSTRING => 10 };
93              
94             # some flags (to be added to the types)
95             use constant {
96             SF_KEY => 0x0100,
97             SF_REF => 0x0200,
98             SF_BLESS => 0x0400,
99             SF_WEAK => 0x0800,
100             SF_RO => 0x1000,
101             SF_DUAL => 0x2000,
102             SF_MAGIC => 0x4000 };
103              
104             sub entries_per_element () { 7; }
105              
106             # default mapping for type output names (human readable)
107             my $TYPE = {
108             S_SCALAR() => 'Scalar',
109             S_UNKNOWN() => 'Unknown',
110             S_HASH() => 'Hash ref',
111             S_GLOB() => 'Glob',
112             S_ARRAY() => 'Array ref',
113             S_CODE() => 'Code',
114             S_REGEXP() => 'Regexp',
115             S_LVALUE() => 'Lvalue',
116             S_CYCLE() => 'Circular ref',
117             S_DOUBLE() => 'Double scalar ref',
118             S_VSTRING() => 'VString',
119              
120             SF_REF() => 'Ref',
121             SF_BLESS() => 'Blessed',
122             SF_WEAK() => 'Weak',
123             SF_RO() => 'Read-Only',
124             SF_DUAL() => 'Dual-Var',
125             SF_MAGIC() => 'Magical',
126             SF_KEY() => '',
127             };
128              
129             # default mapping for type name (internal comparisation)
130             my $TYPE_CLASS = {
131             S_SCALAR() => 'SCALAR',
132             S_UNKNOWN() => 'UNKNOWN',
133             S_HASH() => 'HASH',
134             S_GLOB() => 'GLOB',
135             S_ARRAY() => 'ARRAY',
136             S_CODE() => 'CODE',
137             S_REGEXP() => 'REGEXP',
138             S_LVALUE() => 'LVALUE',
139             S_CYCLE() => 'CYCLE',
140             S_DOUBLE() => 'DOUBLE',
141             S_VSTRING() => 'VSTRING',
142             };
143              
144             # map 'SCALAR' => S_SCALAR
145             my $NAME_MAP = {
146             SCALAR => S_SCALAR(),
147             HASH => S_HASH(),
148             GLOB => S_GLOB(),
149             ARRAY => S_ARRAY(),
150             CODE => S_CODE(),
151             REGEXP => S_REGEXP(),
152             LVALUE => S_LVALUE(),
153             CYCLE => S_CYCLE(),
154             DOUBLE => S_DOUBLE(),
155             VSTRING => S_VSTRING(),
156              
157             REF => SF_REF(),
158             KEY => SF_KEY(),
159             WEAK => SF_WEAK(),
160             DUAL => SF_DUAL(),
161             RO => SF_RO(),
162             MAGIC => SF_MAGIC(),
163             };
164              
165             sub _default_options
166             {
167             # set the options to their default values
168             my ($options) = @_;
169              
170             my $o = {};
171             for my $k (keys %$options) { $o->{$k} = $options->{$k}; }
172            
173             $o->{indent} = ' ' if !defined $o->{indent};
174             $o->{names} ||= $TYPE;
175              
176             $o->{bytes} = 'bytes' unless defined $o->{bytes};
177             $o->{bytes} = ' ' . $o->{bytes} if $o->{bytes} ne '';
178              
179             $o->{left} = '' if !defined $o->{left};
180             $o->{inner} = ' ' if !defined $o->{inner};
181            
182             $o->{total} = 1 if !defined $o->{total};
183              
184             $o->{head} = "Size report v$Devel::Size::Report::VERSION for" if !defined $o->{head};
185              
186             $o->{overhead} = " (overhead: %i%s, %0.2f%%)" if !defined $o->{overhead};
187              
188             # binary flags
189             for my $k (qw/addr terse class/)
190             {
191             $o->{$k} ||= 0;
192             }
193              
194             $o;
195             }
196              
197             sub report_size
198             {
199             # walk the given reference recursively and return text describing the size
200             # of each element
201             my ($ref,$opt) = @_;
202            
203             $opt = {} unless defined $opt;
204             if (ref($opt) ne 'HASH')
205             {
206             require Carp;
207             Carp::confess ("report_size() needs a hash ref for options");
208             }
209            
210             my $options = _default_options($opt);
211              
212             $TRACK_DOUBLES = $options->{doubles} || 0;
213            
214             # DONT do "track_size($ref)" because $ref is a copy of $_[0], reusing some
215             # pre-allocated slot and this can have a different total size than $_[0]!!
216              
217             # get the size for all elements so that we can generate a report on it
218             track_sizes($_[0],$opt);
219              
220             my $text = '';
221            
222             my $indent = $options->{indent};
223             my $names = $options->{names};
224             my $bytes = $options->{bytes};
225             my $left = $options->{left};
226             my $inner = $options->{inner};
227             $inner .= $left;
228            
229             my $total = $options->{total};
230             my $head = $options->{head};
231             my $terse = $options->{terse};
232             # show summary?
233             my $show_summary = $options->{summary};
234              
235             my $foverhead = $options->{overhead};
236            
237             # show class?
238             my $class = $options->{class};
239            
240             # show addr?
241             my $addr = $options->{addr};
242              
243             my $count = {}; # per class/element type counter
244             my $sum = {}; # per class/element memory sum
245              
246             # XXX TODO: why not HASH here?
247             my $r = ref($ref); $r = '' if $r =~ /^(ARRAY|SCALAR)$/;
248             $r = " ($r)" if $r ne '';
249             $text = "$left$head '$ref'$r:\n" if $head ne '';
250              
251             my $e = entries_per_element();
252            
253             for (my $i = 0; $i < @sizes; $i += $e)
254             {
255             # inline element_type for speed:
256             # my $type = element_type( ($sizes[$i+1] & 0xFF),$names);
257             my $type = $names->{ ($sizes[$i+1] & 0xFF) } || 'Unknown';
258              
259             if ($show_summary)
260             {
261             my $t = $sizes[$i+1] & 0xFF; $t = $TYPE_CLASS->{$t};
262             $t = $sizes[$i+6] if $sizes[$i+6];
263             print "# $t $sizes[$i+1]\n" if $t eq '_set';
264             if ($t)
265             {
266             $count->{$t} ++;
267             $sum->{$t} += $sizes[$i+2];
268             }
269             # else { should not happen }
270             }
271              
272             if (!$terse)
273             {
274             # include flags
275             for my $flag (SF_WEAK, SF_RO, SF_DUAL)
276             {
277             if ( ($sizes[$i+1] & $flag) != 0)
278             {
279             $type = element_type($flag, $names) . ' ' . $type;
280             }
281             }
282             if ( ($sizes[$i+1] & SF_REF) != 0)
283             {
284             $type .= " " . element_type(SF_REF, $names);
285             }
286              
287             # add addr of element if wanted
288             $type .= "(" . $sizes[$i+5] . ")" if $addr && $sizes[$i+5];
289              
290             # add class of element if wanted
291             $type .= " (" . $sizes[$i+6] . ")" if $class && $sizes[$i+6];
292              
293             my $str = $type;
294             if ( ($sizes[$i+1] & SF_KEY) != 0)
295             {
296             $str = "'$sizes[$i+4]' => " . $type;
297             }
298             $str .= " $sizes[$i+2]$bytes";
299             if ($sizes[$i+3] != 0)
300             {
301             my $overhead =
302             sprintf($foverhead, $sizes[$i+3], $bytes,
303             100 * $sizes[$i+3] / $sizes[$i+2]);
304             $overhead = ' (overhead: unknown)' if $sizes[$i+3] < 0;
305             $str .= $overhead;
306             }
307             $text .= $inner . ($indent x $sizes[$i]) . "$str\n";
308             }
309             }
310              
311             if ($show_summary)
312             {
313             # default sort is by size
314             my $sort = sub { $sum->{$b} <=> $sum->{$a} };
315              
316             $text .= "Total memory by class (inclusive contained elements):\n";
317             foreach my $k (sort $sort keys %$count)
318             {
319             $text .= $indent . _right_align($sum->{$k},10) . " bytes in " . _right_align($count->{$k},6) . " $k\n";
320             }
321             }
322             my $elements = scalar @sizes / $e;
323             $text .= $left . "Total: $sizes[2]$bytes in $elements elements\n" if $total;
324              
325             hide_tracks(); # release memory
326              
327             $text;
328             }
329              
330             sub hide_tracks
331             {
332             @sizes = ();
333             }
334              
335             sub _right_align
336             {
337             my ($txt,$len) = @_;
338              
339             $txt = ' ' . $txt while (length($txt) < $len);
340             $txt;
341             }
342              
343             sub element_type
344             {
345             my ($type,$TYPE) = @_;
346             $TYPE->{$type} || 'Unknown';
347             }
348              
349             sub type
350             {
351             # map a typename to a type number
352             $NAME_MAP->{$_[0]} || S_UNKNOWN;
353             }
354              
355             sub track_sizes
356             {
357             my $opt = $_[1];
358              
359             $TRACK_DOUBLES = $opt->{doubles} || 0;
360            
361             my $time = time(); # record start time
362             undef %SEEN; # reset cycle memory
363             $CALLS = 0;
364             @sizes = (); # reset results array & stores result:
365             _track_size($_[0]); # use $_[0] directly to avoid slot-reusing
366              
367             if ($opt->{debug})
368             {
369             $time = time() - $time;
370             print STDERR "\n DEBUG: Devel::Size::Report v$Devel::Size::Report::VERSION\n";
371             my $size_seen = total_size(\%SEEN);
372             my $size_sizes = total_size(\@sizes);
373              
374             print STDERR " DEBUG: \%SEEN : ", _right_align($size_seen,12), " bytes, ", scalar keys %SEEN, " elements\n";
375             print STDERR " DEBUG: \@sizes: ", _right_align($size_sizes,12), " bytes, ", scalar @sizes, " elements\n";
376             print STDERR " DEBUG: Total : ", _right_align($size_sizes + $size_seen,12), " bytes, ", scalar @sizes + scalar keys %SEEN, " elements\n";
377             print STDERR " DEBUG: Calls to _track_size: $CALLS\n";
378             print STDERR " DEBUG: took ", sprintf("%0.3f",$time), " seconds to gather data for report.\n\n";
379             }
380             undef %SEEN; # save memory, throw away
381              
382             \@sizes;
383             }
384              
385             sub track_size
386             {
387             # fill the results into @sizes
388             track_sizes($_[0], $_[1]);
389              
390             # return a copy (backwards compatibility)
391             @sizes; # return results
392             }
393              
394             sub _addr
395             {
396             # return address of an element as string
397             my $adr;
398             if (ref($_[0]) && $_[1] ne 'REF')
399             {
400             $adr = sprintf("0x%x", refaddr($_[0]));
401             }
402             else
403             {
404             $adr = sprintf("0x%x", refaddr(\($_[0])));
405             }
406              
407             $adr;
408             }
409              
410             sub _type
411             {
412             # find the type of an element and return as string
413             my $type = uc(reftype($_[0]) || '');
414             my $class = blessed($_[0]); $class = '' unless defined $class;
415              
416             # blessed "Regexp" and ref to scalar?
417             $type ='REGEXP' if $class eq 'Regexp';
418              
419             # refs to scalars are tricky
420             $type ='REF'
421             if ref($_[0]) && UNIVERSAL::isa($_[0],'SCALAR') && $type ne 'REGEXP';
422             ($type,$class);
423             }
424              
425             sub _track_size
426             {
427             # Walk the given reference recursively and store the size, type etc of each
428             # element
429             my ($ref, $level) = @_;
430              
431             $level ||= 0;
432              
433             $CALLS++;
434            
435             no warnings 'recursion';
436              
437             # DO NOT use "total_size($ref)" because $ref is a copy of $_[0], reusing some
438             # pre-allocated slot and this can have a different total size than $_[0]!!
439             my $total_size = size($_[0]);
440             my ($type,$blessed) = _type($_[0]);
441            
442             my $adr = _addr($_[0],$type);
443              
444             if (exists $SEEN{$adr})
445             {
446             # already seen this part of the world, so return
447             if (ref($ref))
448             {
449             push @sizes, $level, S_CYCLE, $SIZE_OF_REF, 0, undef, $adr, $blessed;
450             return;
451             }
452             # a scalar seen twice
453             push @sizes, $level, S_DOUBLE, 0, 0;
454             av_push (@sizes, $UNDEF);
455             push @sizes, $adr;
456             av_push (@sizes, $UNDEF);
457             return;
458             }
459              
460             # put in the address of $ref in the %SEEN hash (things with a refcnt of 1
461             # cannot be part of a cycle, since only one thing is pointing at them)
462             hv_store (%SEEN, $adr , $UNDEF) if ref($_[0]) || SvREFCNT($_[0]) > 1;
463              
464             # not a reference, but a plain scalar?
465             if (!ref($ref))
466             {
467             my $type = S_SCALAR;
468             $type = S_VSTRING if isvstring($_[0]);
469              
470             push @sizes, $level, _flags($_[0]) + $type, $total_size;
471             av_push (@sizes, $ZERO);
472             av_push (@sizes, $UNDEF);
473             push @sizes, $adr, $blessed;
474             return;
475             }
476              
477             my $index = scalar @sizes + 2; # idx of "total_size" entry
478              
479             if ($type eq 'ARRAY')
480             {
481             push @sizes, $level, S_ARRAY, $total_size + $SIZE_OF_REF, 0, undef, $adr, $blessed;
482              
483             my $sum = 0;
484             for (my $i = 0; $i < scalar @$ref; $i++)
485             {
486             my $adr = _addr($ref->[$i], _type($ref->[$i]));
487              
488             if (exists $SEEN{$adr} || ref($ref->[$i]))
489             {
490             my $index = scalar @sizes;
491             _track_size($ref->[$i], $level+1);
492             $sum += $sizes[$index+2];
493             }
494             else
495             {
496             # Put in the address of $ref in the %SEEN hash.
497             # If TRACK_DOUBLES is set, we also need to store scalars with
498             # REFCNT == 1 because somebody might have used hv_store() to make all
499             # keys point to the same scalar and these "shared" scalars have
500             # unfortunately a REFCNT of 1.
501             hv_store (%SEEN, $adr , $UNDEF) if $TRACK_DOUBLES || SvREFCNT($_[0]) > 1;
502             my $size = size($ref->[$i]);
503             push @sizes, $level+1, S_SCALAR, $size;
504             av_push (@sizes, $ZERO);
505             av_push (@sizes, $UNDEF);
506             push @sizes, $adr;
507             av_push (@sizes, $UNDEF);
508             $sum += $size;
509             }
510             }
511             $sizes[$index] += $sum;
512             $sizes[$index+1] = $sizes[$index] - $sum;
513             }
514             elsif ($type eq 'HASH')
515             {
516             push @sizes, $level, S_HASH, $total_size + $SIZE_OF_REF, 0, undef, $adr, $blessed;
517              
518             my $sum = 0;
519             foreach my $elem ( keys %$ref )
520             {
521             my $adr = _addr($ref->{$elem}, _type($ref->{$elem}));
522             if (exists $SEEN{$adr} || ref($ref->{$elem}))
523             {
524             my $index = scalar @sizes;
525             _track_size($ref->{$elem},$level+1);
526              
527             $sizes[$index+1] += SF_KEY;
528             $sizes[$index+4] = $elem;
529             $sum += $sizes[$index+2];
530             }
531             else
532             {
533             # Put in the address of $ref in the %SEEN hash.
534             # If TRACK_DOUBLES is set, we also need to store scalars with
535             # REFCNT == 1 because somebody might have used hv_store() to make all
536             # keys point to the same scalar and these "shared" scalars have
537             # unfortunately a REFCNT of 1.
538             hv_store (%SEEN, $adr , $UNDEF) if $TRACK_DOUBLES || SvREFCNT($_[0]) > 1;
539             my $size = size($ref->{$elem});
540             push @sizes, $level+1, SF_KEY + S_SCALAR, $size, 0, $elem, $adr, undef;
541             $sum += $size;
542             }
543             }
544             $sizes[$index] += $sum;
545             $sizes[$index+1] = $sizes[$index] - $sum;
546             }
547             elsif ($type eq 'REGEXP')
548             {
549             push @sizes, $level, type($type), $total_size;
550             av_push (@sizes, $ZERO);
551             av_push (@sizes, $UNDEF);
552             push @sizes, $adr, $blessed;
553             }
554             elsif ($type eq 'REF')
555             {
556             my $type = uc(reftype(${$_[0]}) || '');
557             $type ='REGEXP' if $blessed eq 'Regexp';
558             $type ='SCALAR' if !ref(${$_[0]});
559             my $flags = SF_REF;
560             $flags += SF_WEAK if isweak($_[0]);
561              
562             push @sizes,
563             ($level, $flags + type($type), $total_size, 0, undef, $adr, $blessed);
564             _track_size($$ref,$level+1);
565             $sizes[$index] += $SIZE_OF_REF; # account for wrong \"" sizes
566             $sizes[$index+1] = $sizes[$index] - total_size($$ref);
567             }
568             # SCALAR reference must come after Regexp, because these are also SCALAR !?
569             elsif ($type eq 'SCALAR')
570             {
571             push @sizes, ($level, SF_REF, $total_size, 0, undef, $adr, $blessed);
572             }
573             else
574             {
575             my $overhead = 0;
576             $overhead = -1 if type($type) == S_UNKNOWN;
577             push @sizes, ($level, type($type), $total_size, $overhead, undef, $adr, $blessed);
578             }
579             }
580              
581             sub _flags
582             {
583             my $flags = 0;
584              
585             $flags += SF_RO if readonly($_[0]);
586             $flags += SF_WEAK if isweak($_[0]);
587              
588             # XXX TODO: how to find out if something is:
589             # an LVALUE
590             # a DUALVAR
591             # a STASH
592              
593             $flags;
594             }
595              
596             1;
597             __END__