File Coverage

blib/lib/Pg/Explain/Buffers.pm
Criterion Covered Total %
statement 177 182 97.2
branch 61 84 72.6
condition 8 8 100.0
subroutine 24 24 100.0
pod 5 5 100.0
total 275 303 90.7


line stmt bran cond sub pod time code
1             package Pg::Explain::Buffers;
2              
3             # UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
4 75     75   80333 use v5.18;
  75         292  
5 75     75   445 use strict;
  75         193  
  75         2367  
6 75     75   396 use warnings;
  75         199  
  75         2174  
7 75     75   402 use warnings qw( FATAL utf8 );
  75         166  
  75         2533  
8 75     75   1026 use utf8;
  75         183  
  75         443  
9 75     75   2485 use open qw( :std :utf8 );
  75         1429  
  75         448  
10 75     75   11012 use Unicode::Normalize qw( NFC );
  75         2315  
  75         4141  
11 75     75   1189 use Unicode::Collate;
  75         9019  
  75         2238  
12 75     75   971 use Encode qw( decode );
  75         10615  
  75         4619  
13              
14 1     1   7 if ( grep /\P{ASCII}/ => @ARGV ) {
  1         2  
  1         11  
15             @ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
16             }
17              
18             # UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
19              
20 75     75   38710 use Carp;
  75         194  
  75         6586  
21 75     75   941 use Clone qw( clone );
  75         3978  
  75         4486  
22 75     75   950 use autodie;
  75         14179  
  75         637  
23              
24             use overload
25 75         1059 '+' => \&_buffers_add,
26             '-' => \&_buffers_subtract,
27 75     75   416477 'bool' => \&_buffers_bool;
  75         15562  
28              
29             =head1 NAME
30              
31             Pg::Explain::Buffers - Object to store buffers information about node in PostgreSQL's explain analyze
32              
33             =head1 VERSION
34              
35             Version 2.4
36              
37             =cut
38              
39             our $VERSION = '2.4';
40              
41             =head1 SYNOPSIS
42              
43             Quick summary of what the module does.
44              
45             Perhaps a little code snippet.
46              
47             use Pg::Explain;
48              
49             my $explain = Pg::Explain->new('source_file' => 'some_file.out');
50             ...
51              
52             if ( $explain->top_node->buffers ) {
53             print $explain->top_node->buffers->as_text();
54             }
55             ...
56              
57             Alternatively you can build the object itself from either a string (conforming
58             to text version of EXPLAIN ANALYZE output) or a structure, containing keys like
59             in JSON/YAML/XML formats of the explain:
60              
61             use Pg::Explain::Buffers;
62              
63             my $from_string = Pg::Explain::Buffers->new( 'Buffers: shared hit=12101 read=73' );
64             my $from_struct = Pg::Explain::Buffers->new( {
65             'Shared Hit Blocks' => 12101,
66             'Shared Read Blocks' => 73,
67             } );
68              
69             To such object you can later on add Timing information, though only with
70             string - if you had it in struct, make it available on creation.
71              
72             $buffers->add_timing( 'I/O Timings: read=58.316 write=1.672' );
73              
74             =head1 FUNCTIONS
75              
76             =head2 new
77              
78             Object constructor.
79              
80             Takes one argument, either a string or hashref to build data from.
81              
82             =cut
83              
84             sub new {
85 687     687 1 2142 my $class = shift;
86 687         1430 my $self = bless {}, $class;
87 687 50       1680 croak( 'You have to provide base info.' ) if 0 == scalar @_;
88 687 50       1411 croak( 'Too many arguments to Pg::Explain::Buffers->new().' ) if 1 < scalar @_;
89 687         1082 my $arg = shift;
90 687 100       1736 if ( 'HASH' eq ref $arg ) {
    50          
91 571         1514 $self->_build_from_struct( $arg );
92             }
93             elsif ( '' eq ref $arg ) {
94 116         316 $self->_build_from_string( $arg );
95             }
96             else {
97 0         0 croak( "Don't know how to build Pg::Explain::Buffers using " . ref( $arg ) );
98             }
99 687         1748 return $self;
100             }
101              
102             =head2 add_timing
103              
104             Adds timing information to existing buffer info.
105              
106             Takes one argument, either a string or hashref to build data from.
107              
108             =cut
109              
110             sub add_timing {
111 28     28 1 49 my $self = shift;
112 28 50       70 croak( 'You have to provide base info.' ) if 0 == scalar @_;
113 28 50       73 croak( 'Too many arguments to Pg::Explain::Buffers->new().' ) if 1 < scalar @_;
114 28         52 my $arg = shift;
115 28 50       67 croak( "Don't know how to add timing info in Pg::Explain::Buffers using " . ref( $arg ) ) unless '' eq ref( $arg );
116 28 50       195 croak( "Invalid format of I/O Timing info: $arg" ) unless $arg =~ m{
117             \A
118             \s*
119             I/O \s Timings:
120             (
121             \s+
122             (?: read | write )
123             =
124             \d+\.\d+
125             )+
126             \s*
127             \z
128             }xms;
129              
130 28         197 my @matching = $arg =~ m{ (read|write) = (\d+\.\d+) }xg;
131 28 50       95 return if 0 == scalar @matching;
132 28         102 my %matching = @matching;
133 28         65 for my $key ( qw( read write ) ) {
134 56 100       161 next unless my $val = $matching{ $key };
135 33         109 $self->{ 'data' }->{ 'timings' }->{ $key } = $val;
136             }
137 28         105 return;
138             }
139              
140             =head2 as_text
141              
142             Returns text representation of stored buffers info, together with timings (if available).
143              
144             =cut
145              
146             sub as_text {
147 83     83 1 141 my $self = shift;
148 83 50       237 return unless $self->{ 'data' };
149 83 50       126 return if 0 == scalar keys %{ $self->{ 'data' } };
  83         241  
150 83         147 my @parts = ();
151 83         162 for my $type ( qw( shared local temp ) ) {
152 249 100       602 next unless my $x = $self->{ 'data' }->{ $type };
153 92         171 my @elements = map { $_ . '=' . $x->{ $_ } } grep { $x->{ $_ } } qw( hit read dirtied written );
  119         367  
  368         680  
154 92 50       224 next if 0 == scalar @elements;
155 92         326 push @parts, join( ' ', $type, @elements );
156             }
157 83 50       193 return if 0 == scalar @parts;
158 83         284 my $ret = sprintf 'Buffers: %s', join( ', ', @parts );
159 83 100       346 return $ret unless my $T = $self->{ 'data' }->{ 'timings' };
160 16         34 my $timing = join ' ', map { $_ . '=' . $T->{ $_ } } grep { $T->{ $_ } } qw{ read write };
  18         67  
  32         74  
161 16 50       43 return $ret unless $timing;
162 16         82 return $ret . "\nI/O Timings: " . $timing;
163             }
164              
165             =head2 get_struct
166              
167             Returns hash(ref) with all data about buffers from this object. Keys in this hash:
168              
169             =over
170              
171             =item * shared (with subkeys: hit, read, dirtied, written)
172              
173             =item * local (with subkeys: hit, read, dirtied, written)
174              
175             =item * temp (with subkeys: read, written)
176              
177             =item * timings (with subkeys: read, write
178              
179             =back
180              
181             Only elements with non-zero values are returned. If there are no elements to be returned, it returns undef.
182              
183             =cut
184              
185             sub get_struct {
186 105     105 1 193 my $self = shift;
187 105         168 my $d = $self->{ 'data' };
188 105         566 my $map = {
189             'shared' => [ qw{ hit read dirtied written } ],
190             'local' => [ qw{ hit read dirtied written } ],
191             'temp' => [ qw{ read written } ],
192             'timings' => [ qw{ read write } ],
193             };
194 105         211 my $ret = {};
195 105         164 while ( my ( $type, $subtypes ) = each %{ $map } ) {
  525         1393  
196 420 100       1023 next unless defined( my $t = $self->{ 'data' }->{ $type } );
197 142         191 for my $subtype ( @{ $subtypes } ) {
  142         256  
198 498 100       1080 next unless defined( my $val = $t->{ $subtype } );
199 178         473 $ret->{ $type }->{ $subtype } = $val;
200             }
201             }
202 105 100       176 return if 0 == scalar keys %{ $ret };
  105         287  
203 104         515 return $ret;
204             }
205              
206             =head2 data
207              
208             Accessor to internal data.
209              
210             =cut
211              
212             sub data {
213 2210     2210 1 2892 my $self = shift;
214 2210 100       4101 $self->{ 'data' } = $_[ 0 ] if 0 < scalar @_;
215 2210         6798 return $self->{ 'data' };
216             }
217              
218             =head1 OPERATORS
219              
220             To allow for easier work on buffer values + and - operators are overloaded, so you can:
221              
222             $buffers_out = $buffers1 - $buffers2;
223              
224             While processing subtraction, it is important that it's not possible to get negative values,
225             so if any value would drop below 0, it will get auto-adjusted to 0.
226              
227             =cut
228              
229             =head1 INTERNAL METHODS
230              
231             =head2 _build_from_struct
232              
233             Gets data out of provided HASH.
234              
235             =cut
236              
237             sub _build_from_struct {
238 571     571   894 my $self = shift;
239 571         883 my $in = shift;
240              
241 571         3617 my $map = {
242             'shared' => [ qw{ hit read dirtied written } ],
243             'local' => [ qw{ hit read dirtied written } ],
244             'temp' => [ qw{ read written } ],
245             'timings' => [ qw{ read write } ],
246             };
247              
248 571         1082 while ( my ( $type, $subtypes ) = each %{ $map } ) {
  2855         7725  
249 2284 100       4659 my $in_type = $type eq 'timings' ? 'I/O' : ucfirst( $type );
250 2284 100       3751 my $in_suffix = $type eq 'timings' ? 'Time' : 'Blocks';
251 2284         2799 for my $subtype ( @{ $subtypes } ) {
  2284         3571  
252 6852         9327 my $in_subtype = ucfirst( $subtype );
253 6852         11129 my $in_key = join ' ', $in_type, $in_subtype, $in_suffix;
254 6852 100       14848 next unless my $val = $in->{ $in_key };
255 165 100       425 next if 0 == $val;
256 98         440 $self->{ 'data' }->{ $type }->{ $subtype } = $val;
257             }
258             }
259              
260 571         2029 return;
261             }
262              
263             =head2 _build_from_string
264              
265             Gets data out of provided string.
266              
267             =cut
268              
269             sub _build_from_string {
270 116     116   187 my $self = shift;
271 116         182 my $in = shift;
272 116         387 my $single_type_re = qr{
273             (?:
274             (?: shared | local )
275             (?:
276             \s+
277             (?: hit | read | dirtied | written ) = [1-9]\d*
278             )+
279             |
280             temp
281             (?:
282             \s+
283             (?: read | written ) = [1-9]\d*
284             )+
285             )
286             }xms;
287 116 50       2360 croak( 'Invalid format of string for Pg::Explain::Buffers: ' . $in ) unless $in =~ m{
288             \A
289             \s*
290             Buffers:
291             \s+
292             (
293             $single_type_re
294             (?:
295             , \s+
296             $single_type_re
297             )*
298             )
299             \s*
300             \z
301             }xms;
302 116         384 my $plain_info = $1;
303 116         399 my @parts = split /,\s+/, $plain_info;
304 116         560 $self->{ 'data' } = {};
305              
306 116         241 for my $part ( @parts ) {
307 129         459 my @words = split /\s+/, $part;
308 129         258 my $type = shift @words;
309 129         229 for my $word ( @words ) {
310 159         504 my ( $op, $bufs ) = split /=/, $word;
311 159         738 $self->{ 'data' }->{ $type }->{ $op } = $bufs;
312             }
313             }
314              
315 116         383 return;
316             }
317              
318             =head2 _buffers_add
319              
320             Creates new Pg::Explain::Buffers object by adding values based on two objects. To be used like:
321              
322             my $result = $buffers1 + $buffers2;
323              
324             =cut
325              
326             sub _buffers_add {
327 8     8   38 my ( $left, $right ) = @_;
328 8 50       29 return unless 'Pg::Explain::Buffers' eq ref $left;
329 8 50       23 unless ( 'Pg::Explain::Buffers' eq ref $right ) {
330 0 0       0 return if defined $right;
331 0         0 my $res = Pg::Explain::Buffers->new( {} );
332 0         0 $res->data( clone( $left->data ) );
333 0         0 return $res;
334             }
335              
336 8         18 my $D = {};
337 8         56 my $map = {
338             'shared' => [ qw{ hit read dirtied written } ],
339             'local' => [ qw{ hit read dirtied written } ],
340             'temp' => [ qw{ read written } ],
341             'timings' => [ qw{ read write } ],
342             };
343              
344 8 50       22 my $L = $left->data ? clone( $left->data ) : {};
345 8 100       33 my $R = $right->data ? clone( $right->data ) : {};
346 8         19 while ( my ( $type, $subtypes ) = each %{ $map } ) {
  40         103  
347 32         44 for my $subtype ( @{ $subtypes } ) {
  32         50  
348 96   100     367 my $val = ( $L->{ $type }->{ $subtype } // 0 ) + ( $R->{ $type }->{ $subtype } // 0 );
      100        
349 96 100       202 next if $val <= 0;
350 22         60 $D->{ $type }->{ $subtype } = $val;
351             }
352             }
353 8 50       16 return if 0 == scalar keys %{ $D };
  8         38  
354              
355 8         30 my $ret = Pg::Explain::Buffers->new( {} );
356 8         27 $ret->data( $D );
357 8         66 return $ret;
358             }
359              
360             =head2 _buffers_subtract
361              
362             Creates new Pg::Explain::Buffers object by subtracting values based on two objects. To be used like:
363              
364             my $result = $buffers1 - $buffers2;
365              
366             =cut
367              
368             sub _buffers_subtract {
369 8     8   26 my ( $left, $right ) = @_;
370 8 50       31 return unless 'Pg::Explain::Buffers' eq ref $left;
371 8 50       24 return unless 'Pg::Explain::Buffers' eq ref $right;
372              
373 8         57 my $map = {
374             'shared' => [ qw{ hit read dirtied written } ],
375             'local' => [ qw{ hit read dirtied written } ],
376             'temp' => [ qw{ read written } ],
377             'timings' => [ qw{ read write } ],
378             };
379              
380 8 50       29 return unless $left->data;
381 8 100       23 unless ( $right->data ) {
382 1         5 my $res = Pg::Explain::Buffers->new( {} );
383 1         4 $res->data( clone( $left->data ) );
384 1         6 return $res;
385             }
386              
387 7         13 my $new_data = {};
388 7         14 while ( my ( $type, $subtypes ) = each %{ $map } ) {
  35         106  
389 28 100       51 next unless my $L = $left->data->{ $type };
390 13 100       46 if ( my $R = $right->data->{ $type } ) {
391 12         25 for my $subtype ( @{ $subtypes } ) {
  12         22  
392 40   100     184 my $val = ( $L->{ $subtype } // 0 ) - ( $R->{ $subtype } // 0 );
      100        
393              
394             # Weirdish comparison to get rid of floating point arithmetic errors, like:
395             # 32.874 - 18.153 - 14.721 => 3.5527136788005e-15
396 40 100       102 next if $val <= 0.00001;
397 4         12 $new_data->{ $type }->{ $subtype } = $val;
398             }
399             }
400             else {
401 1         9 $new_data->{ $type } = clone( $L );
402             }
403             }
404 7 100       13 return if 0 == scalar keys %{ $new_data };
  7         49  
405              
406 4         15 my $ret = Pg::Explain::Buffers->new( {} );
407 4         15 $ret->data( $new_data );
408 4         24 return $ret;
409             }
410              
411             =head2 _buffers_bool
412              
413             For checking if given variable is set, as in:
414              
415             $r = $buffers1 - $buffers2;
416             if ( $r ) {...}
417              
418             =cut
419              
420             sub _buffers_bool {
421 1534     1534   2465 my $self = shift;
422 1534 100       2915 return unless $self->data;
423 566         770 return 0 < scalar keys %{ $self->data };
  566         902  
424             }
425              
426             =head1 AUTHOR
427              
428             hubert depesz lubaczewski, C<< >>
429              
430             =head1 BUGS
431              
432             Please report any bugs or feature requests to C.
433              
434             =head1 SUPPORT
435              
436             You can find documentation for this module with the perldoc command.
437              
438             perldoc Pg::Explain::Buffers
439              
440             =head1 COPYRIGHT & LICENSE
441              
442             Copyright 2008-2021 hubert depesz lubaczewski, all rights reserved.
443              
444             This program is free software; you can redistribute it and/or modify it
445             under the same terms as Perl itself.
446              
447             =cut
448              
449             1; # End of Pg::Explain::Buffers