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 74     74   82031 use v5.18;
  74         288  
5 74     74   444 use strict;
  74         173  
  74         2128  
6 74     74   396 use warnings;
  74         208  
  74         2211  
7 74     74   414 use warnings qw( FATAL utf8 );
  74         182  
  74         2423  
8 74     74   996 use utf8;
  74         201  
  74         484  
9 74     74   2456 use open qw( :std :utf8 );
  74         1469  
  74         424  
10 74     74   10942 use Unicode::Normalize qw( NFC );
  74         2417  
  74         4033  
11 74     74   1232 use Unicode::Collate;
  74         9023  
  74         2177  
12 74     74   997 use Encode qw( decode );
  74         10747  
  74         4512  
13              
14 1     1   8 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 74     74   37981 use Carp;
  74         176  
  74         6649  
21 74     74   946 use Clone qw( clone );
  74         3906  
  74         4490  
22 74     74   990 use autodie;
  74         14294  
  74         674  
23              
24             use overload
25 74         1032 '+' => \&_buffers_add,
26             '-' => \&_buffers_subtract,
27 74     74   414666 'bool' => \&_buffers_bool;
  74         15843  
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.3
36              
37             =cut
38              
39             our $VERSION = '2.3';
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 2268 my $class = shift;
86 687         1544 my $self = bless {}, $class;
87 687 50       1708 croak( 'You have to provide base info.' ) if 0 == scalar @_;
88 687 50       1493 croak( 'Too many arguments to Pg::Explain::Buffers->new().' ) if 1 < scalar @_;
89 687         1100 my $arg = shift;
90 687 100       1769 if ( 'HASH' eq ref $arg ) {
    50          
91 571         1540 $self->_build_from_struct( $arg );
92             }
93             elsif ( '' eq ref $arg ) {
94 116         307 $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         1822 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 48 my $self = shift;
112 28 50       98 croak( 'You have to provide base info.' ) if 0 == scalar @_;
113 28 50       117 croak( 'Too many arguments to Pg::Explain::Buffers->new().' ) if 1 < scalar @_;
114 28         47 my $arg = shift;
115 28 50       72 croak( "Don't know how to add timing info in Pg::Explain::Buffers using " . ref( $arg ) ) unless '' eq ref( $arg );
116 28 50       241 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         202 my @matching = $arg =~ m{ (read|write) = (\d+\.\d+) }xg;
131 28 50       87 return if 0 == scalar @matching;
132 28         94 my %matching = @matching;
133 28         62 for my $key ( qw( read write ) ) {
134 56 100       172 next unless my $val = $matching{ $key };
135 33         112 $self->{ 'data' }->{ 'timings' }->{ $key } = $val;
136             }
137 28         97 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 145 my $self = shift;
148 83 50       583 return unless $self->{ 'data' };
149 83 50       129 return if 0 == scalar keys %{ $self->{ 'data' } };
  83         241  
150 83         171 my @parts = ();
151 83         177 for my $type ( qw( shared local temp ) ) {
152 249 100       634 next unless my $x = $self->{ 'data' }->{ $type };
153 92         203 my @elements = map { $_ . '=' . $x->{ $_ } } grep { $x->{ $_ } } qw( hit read dirtied written );
  119         425  
  368         734  
154 92 50       234 next if 0 == scalar @elements;
155 92         312 push @parts, join( ' ', $type, @elements );
156             }
157 83 50       201 return if 0 == scalar @parts;
158 83         297 my $ret = sprintf 'Buffers: %s', join( ', ', @parts );
159 83 100       370 return $ret unless my $T = $self->{ 'data' }->{ 'timings' };
160 16         36 my $timing = join ' ', map { $_ . '=' . $T->{ $_ } } grep { $T->{ $_ } } qw{ read write };
  18         74  
  32         77  
161 16 50       50 return $ret unless $timing;
162 16         89 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 192 my $self = shift;
187 105         168 my $d = $self->{ 'data' };
188 105         602 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         239 my $ret = {};
195 105         170 while ( my ( $type, $subtypes ) = each %{ $map } ) {
  525         1419  
196 420 100       997 next unless defined( my $t = $self->{ 'data' }->{ $type } );
197 142         209 for my $subtype ( @{ $subtypes } ) {
  142         258  
198 498 100       1119 next unless defined( my $val = $t->{ $subtype } );
199 178         508 $ret->{ $type }->{ $subtype } = $val;
200             }
201             }
202 105 100       170 return if 0 == scalar keys %{ $ret };
  105         317  
203 104         463 return $ret;
204             }
205              
206             =head2 data
207              
208             Accessor to internal data.
209              
210             =cut
211              
212             sub data {
213 2210     2210 1 3087 my $self = shift;
214 2210 100       3995 $self->{ 'data' } = $_[ 0 ] if 0 < scalar @_;
215 2210         6951 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   911 my $self = shift;
239 571         856 my $in = shift;
240              
241 571         3807 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         1113 while ( my ( $type, $subtypes ) = each %{ $map } ) {
  2855         7918  
249 2284 100       4694 my $in_type = $type eq 'timings' ? 'I/O' : ucfirst( $type );
250 2284 100       3772 my $in_suffix = $type eq 'timings' ? 'Time' : 'Blocks';
251 2284         2877 for my $subtype ( @{ $subtypes } ) {
  2284         3679  
252 6852         9314 my $in_subtype = ucfirst( $subtype );
253 6852         10816 my $in_key = join ' ', $in_type, $in_subtype, $in_suffix;
254 6852 100       14949 next unless my $val = $in->{ $in_key };
255 165 100       406 next if 0 == $val;
256 98         461 $self->{ 'data' }->{ $type }->{ $subtype } = $val;
257             }
258             }
259              
260 571         2098 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   195 my $self = shift;
271 116         177 my $in = shift;
272 116         403 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       2347 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         352 my $plain_info = $1;
303 116         395 my @parts = split /,\s+/, $plain_info;
304 116         551 $self->{ 'data' } = {};
305              
306 116         274 for my $part ( @parts ) {
307 129         497 my @words = split /\s+/, $part;
308 129         279 my $type = shift @words;
309 129         239 for my $word ( @words ) {
310 159         489 my ( $op, $bufs ) = split /=/, $word;
311 159         752 $self->{ 'data' }->{ $type }->{ $op } = $bufs;
312             }
313             }
314              
315 116         367 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   35 my ( $left, $right ) = @_;
328 8 50       36 return unless 'Pg::Explain::Buffers' eq ref $left;
329 8 50       25 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         19 my $D = {};
337 8         51 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       25 my $L = $left->data ? clone( $left->data ) : {};
345 8 100       29 my $R = $right->data ? clone( $right->data ) : {};
346 8         22 while ( my ( $type, $subtypes ) = each %{ $map } ) {
  40         107  
347 32         41 for my $subtype ( @{ $subtypes } ) {
  32         55  
348 96   100     356 my $val = ( $L->{ $type }->{ $subtype } // 0 ) + ( $R->{ $type }->{ $subtype } // 0 );
      100        
349 96 100       192 next if $val <= 0;
350 22         65 $D->{ $type }->{ $subtype } = $val;
351             }
352             }
353 8 50       17 return if 0 == scalar keys %{ $D };
  8         28  
354              
355 8         42 my $ret = Pg::Explain::Buffers->new( {} );
356 8         29 $ret->data( $D );
357 8         67 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   28 my ( $left, $right ) = @_;
370 8 50       36 return unless 'Pg::Explain::Buffers' eq ref $left;
371 8 50       27 return unless 'Pg::Explain::Buffers' eq ref $right;
372              
373 8         56 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       30 return unless $left->data;
381 8 100       20 unless ( $right->data ) {
382 1         3 my $res = Pg::Explain::Buffers->new( {} );
383 1         4 $res->data( clone( $left->data ) );
384 1         7 return $res;
385             }
386              
387 7         15 my $new_data = {};
388 7         13 while ( my ( $type, $subtypes ) = each %{ $map } ) {
  35         104  
389 28 100       54 next unless my $L = $left->data->{ $type };
390 13 100       47 if ( my $R = $right->data->{ $type } ) {
391 12         18 for my $subtype ( @{ $subtypes } ) {
  12         29  
392 40   100     200 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       112 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       29 return if 0 == scalar keys %{ $new_data };
  7         56  
405              
406 4         17 my $ret = Pg::Explain::Buffers->new( {} );
407 4         14 $ret->data( $new_data );
408 4         27 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   2484 my $self = shift;
422 1534 100       2925 return unless $self->data;
423 566         793 return 0 < scalar keys %{ $self->data };
  566         903  
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