File Coverage

blib/lib/Pg/Explain/Buffers.pm
Criterion Covered Total %
statement 209 214 97.6
branch 79 106 74.5
condition 12 12 100.0
subroutine 24 24 100.0
pod 5 5 100.0
total 329 361 91.1


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 80     80   136603 use v5.18;
  80         352  
5 80     80   518 use strict;
  80         174  
  80         2203  
6 80     80   383 use warnings;
  80         199  
  80         5301  
7 80     80   468 use warnings qw( FATAL utf8 );
  80         162  
  80         5763  
8 80     80   5647 use utf8;
  80         3215  
  80         860  
9 80     80   3837 use open qw( :std :utf8 );
  80         1780  
  80         571  
10 80     80   16470 use Unicode::Normalize qw( NFC );
  80         4716  
  80         6244  
11 80     80   1299 use Unicode::Collate;
  80         13186  
  80         2916  
12 80     80   1170 use Encode qw( decode );
  80         21460  
  80         14872  
13              
14             if ( grep /\P{ASCII}/ => @ARGV ) {
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 80     80   587 use Carp;
  80         155  
  80         6692  
21 80     80   1056 use Clone qw( clone );
  80         780  
  80         5183  
22 80     80   1023 use autodie;
  80         21614  
  80         814  
23              
24             use overload
25 80         1067 '+' => \&_buffers_add,
26             '-' => \&_buffers_subtract,
27 80     80   518147 'bool' => \&_buffers_bool;
  80         32275  
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.9
36              
37             =cut
38              
39             our $VERSION = '2.9';
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 761     761 1 245032 my $class = shift;
86 761         1986 my $self = bless {}, $class;
87 761 50       2060 croak( 'You have to provide base info.' ) if 0 == scalar @_;
88 761 50       1755 croak( 'Too many arguments to Pg::Explain::Buffers->new().' ) if 1 < scalar @_;
89 761         1382 my $arg = shift;
90 761 100       2187 if ( 'HASH' eq ref $arg ) {
    50          
91 616         4093 $self->_build_from_struct( $arg );
92             }
93             elsif ( '' eq ref $arg ) {
94 145         468 $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 761         2251 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 47     47 1 83 my $self = shift;
112 47 50       114 croak( 'You have to provide base info.' ) if 0 == scalar @_;
113 47 50       117 croak( 'Too many arguments to Pg::Explain::Buffers->new().' ) if 1 < scalar @_;
114 47         86 my $arg = shift;
115 47 50       132 croak( "Don't know how to add timing info in Pg::Explain::Buffers using " . ref( $arg ) ) unless '' eq ref( $arg );
116              
117 47 50       508 croak( "Invalid format of I/O Timing info: $arg" ) unless $arg =~ m{
118             \A
119             \s*
120             I/O \s Timings:
121             (
122             (
123             \s+
124             (?: read | write )
125             =
126             \d+(\.\d+)?
127             )+
128             |
129             \s+
130             ( local | shared/local | shared | temp )
131             (
132             \s+
133             (?: read | write )
134             =
135             \d+(\.\d+)?
136             )+
137             (
138             , \s+
139             ( local | shared/local | shared | temp )
140             (
141             \s+
142             (?: read | write )
143             =
144             \d+(\.\d+)?
145             )+
146             )*
147             )
148             \s*
149             \z
150             }xms;
151              
152 47         505 my @matching = $arg =~ m{ (read|write) = (\d+(?:\.\d+)?) }xg;
153 47 50       147 return if 0 == scalar @matching;
154              
155 47         218 $arg =~ s/\A\s*//;
156 47         221 my $T = {
157             'info' => $arg,
158             };
159              
160 47         167 for ( my $i = 0 ; $i < scalar @matching ; $i += 2 ) {
161 67         551 $T->{ $matching[ $i ] } += $matching[ $i + 1 ];
162             }
163 47         132 $self->{ 'data' }->{ 'timings' } = $T;
164 47         184 return;
165             }
166              
167             =head2 as_text
168              
169             Returns text representation of stored buffers info, together with timings (if available).
170              
171             =cut
172              
173             sub as_text {
174 139     139 1 215 my $self = shift;
175 139 50       306 return unless $self->{ 'data' };
176 139 50       200 return if 0 == scalar keys %{ $self->{ 'data' } };
  139         349  
177 139         224 my @parts = ();
178 139         254 for my $type ( qw( shared local temp ) ) {
179 417 100       1083 next unless my $x = $self->{ 'data' }->{ $type };
180 153         330 my @elements = map { $_ . '=' . $x->{ $_ } } grep { $x->{ $_ } } qw( hit read dirtied written );
  227         767  
  612         1080  
181 153 50       356 next if 0 == scalar @elements;
182 153         452 push @parts, join( ' ', $type, @elements );
183             }
184 139 50       340 return if 0 == scalar @parts;
185 139         293 my $ret = sprintf 'Buffers: %s', join( ', ', @parts );
186 139 100       439 return $ret unless my $T = $self->{ 'data' }->{ 'timings' };
187 59         262 return $ret . "\n" . $T->{ 'info' };
188             }
189              
190             =head2 get_struct
191              
192             Returns hash(ref) with all data about buffers from this object. Keys in this hash:
193              
194             =over
195              
196             =item * shared (with subkeys: hit, read, dirtied, written)
197              
198             =item * local (with subkeys: hit, read, dirtied, written)
199              
200             =item * temp (with subkeys: read, written)
201              
202             =item * timings (with subkeys: read, write, info)
203              
204             =back
205              
206             Only elements with non-zero values are returned. If there are no elements to be returned, it returns undef.
207              
208             =cut
209              
210             sub get_struct {
211 128     128 1 221 my $self = shift;
212 128         236 my $d = $self->{ 'data' };
213 128         976 my $map = {
214             'shared' => [ qw{ hit read dirtied written } ],
215             'local' => [ qw{ hit read dirtied written } ],
216             'temp' => [ qw{ read written } ],
217             'timings' => [ qw{ read write info } ],
218             };
219 128         261 my $ret = {};
220 128         223 while ( my ( $type, $subtypes ) = each %{ $map } ) {
  640         1646  
221 512 100       1340 next unless defined( my $t = $self->{ 'data' }->{ $type } );
222 191         255 for my $subtype ( @{ $subtypes } ) {
  191         377  
223 695 100       2224 next unless defined( my $val = $t->{ $subtype } );
224 313         839 $ret->{ $type }->{ $subtype } = $val;
225             }
226             }
227 128 100       199 return if 0 == scalar keys %{ $ret };
  128         299  
228 127         667 return $ret;
229             }
230              
231             =head2 data
232              
233             Accessor to internal data.
234              
235             =cut
236              
237             sub data {
238 2714     2714 1 3814 my $self = shift;
239 2714 100       5090 $self->{ 'data' } = $_[ 0 ] if 0 < scalar @_;
240 2714         9067 return $self->{ 'data' };
241             }
242              
243             =head1 OPERATORS
244              
245             To allow for easier work on buffer values + and - operators are overloaded, so you can:
246              
247             $buffers_out = $buffers1 - $buffers2;
248              
249             While processing subtraction, it is important that it's not possible to get negative values,
250             so if any value would drop below 0, it will get auto-adjusted to 0.
251              
252             =cut
253              
254             =head1 INTERNAL METHODS
255              
256             =head2 _build_from_struct
257              
258             Gets data out of provided HASH.
259              
260             =cut
261              
262             sub _build_from_struct {
263 616     616   1020 my $self = shift;
264 616         951 my $in = shift;
265              
266 616         4385 my $map = {
267             'shared' => [ qw{ hit read dirtied written } ],
268             'local' => [ qw{ hit read dirtied written } ],
269             'temp' => [ qw{ read written } ],
270             };
271              
272 616         1221 while ( my ( $type, $subtypes ) = each %{ $map } ) {
  2464         7254  
273 1848 50       3833 my $in_type = $type eq 'timings' ? 'I/O' : ucfirst( $type );
274 1848 50       3314 my $in_suffix = $type eq 'timings' ? 'Time' : 'Blocks';
275 1848         2590 for my $subtype ( @{ $subtypes } ) {
  1848         3061  
276 6160         8777 my $in_subtype = ucfirst( $subtype );
277 6160         11385 my $in_key = join ' ', $in_type, $in_subtype, $in_suffix;
278 6160 100       16013 next unless my $val = $in->{ $in_key };
279 159 50       311 next if 0 == $val;
280 159         725 $self->{ 'data' }->{ $type }->{ $subtype } = $val;
281             }
282             }
283              
284             # Timing information changes depending on version, so let's build it appropriately
285 616         1142 my $T = {};
286 616         1049 for my $key ( sort grep { m{I/O (?:Read|Write) Time$} } keys %{ $in } ) {
  9572         19897  
  616         3798  
287 268 100       753 next if $in->{ $key } == 0;
288 44 100       110 if ( $key =~ /Read/ ) {
289 43         170 $T->{ 'read' } += $in->{ $key };
290             }
291             else {
292 1         4 $T->{ 'write' } += $in->{ $key };
293             }
294             }
295 616 100       1597 if ( 0 < scalar keys %{ $T } ) {
  616         2073  
296 43         120 $T->{ 'info' } = $self->_build_timing_info( $in );
297 43         108 $self->{ 'data' }->{ 'timings' } = $T;
298             }
299              
300 616         3126 return;
301             }
302              
303             =head2 _build_timing_info
304              
305             Based on data from structure from json/yaml/xml, build I/O Timings: info line for textual representation of explain.
306              
307             =cut
308              
309             sub _build_timing_info {
310 43     43   81 my $self = shift;
311 43         76 my $in = shift;
312              
313 43         103 my %parts = ();
314 43         66 for my $type ( qw( old shared local temp ) ) {
315 172         222 my @for_type = ();
316 172 100       315 my $type_prefix = $type eq 'old' ? '' : ( ucfirst( $type ) . ' ' );
317 172         251 my $read_key = $type_prefix . 'I/O Read Time';
318 172         210 my $write_key = $type_prefix . 'I/O Write Time';
319              
320             # +0 to make sure we're treating the thing as number, and not string
321             # The key can be absent from input, or it can be there, but be as number, or as string.
322             # The problem is that string '0.000' - as provided by YAML parsing, is causing issues, as it passes "if $val"
323 172 100 100     834 push @for_type, sprintf( 'read=%.3f', $in->{ $read_key } ) if ( ( $in->{ $read_key } // 0 ) + 0 ) > 0;
324 172 100 100     472 push @for_type, sprintf( 'write=%.3f', $in->{ $write_key } ) if ( ( $in->{ $write_key } // 0 ) + 0 ) > 0;
325 172 100       370 next if 0 == scalar @for_type;
326 43         149 $parts{ $type } = join( ' ', @for_type );
327             }
328 43 50       139 return if 0 == scalar keys %parts;
329              
330 43 100       139 if ( exists $in->{ 'Local I/O Read Time' } ) {
    100          
331              
332             # This is the newest format (pg17+)
333             # Timings: shared read=? write=?, local read=? write=?, temp read=? write=?
334 5         6 my @parts = ();
335 5         6 for my $type ( qw( shared local temp ) ) {
336 15 100       23 next unless $parts{ $type };
337 5         9 push @parts, $type . ' ' . $parts{ $type };
338             }
339 5         17 return sprintf( 'I/O Timings: %s', join( ', ', @parts ) );
340             }
341             elsif ( exists $in->{ 'Temp I/O Read Time' } ) {
342              
343             # This is format from pg15 to pg16
344             # I/O Timings: shared/local read=? write=?, temp read=? write=?
345 8         13 my @parts = ();
346 8         20 for my $type ( qw( old temp ) ) {
347 16 100       38 next unless $parts{ $type };
348 8 50       18 my $label = $type eq 'old' ? 'shared/local' : $type;
349 8         23 push @parts, $label . ' ' . $parts{ $type };
350             }
351 8         48 return sprintf( 'I/O Timings: %s', join( ', ', @parts ) );
352             }
353             else {
354             # This is the oldest format (pg14 and older)
355             # I/O Timings: read=? write=?
356 30         144 return sprintf( 'I/O Timings: %s', $parts{ 'old' } );
357             }
358             }
359              
360             =head2 _build_from_string
361              
362             Gets data out of provided string.
363              
364             =cut
365              
366             sub _build_from_string {
367 145     145   287 my $self = shift;
368 145         253 my $in = shift;
369 145         599 my $single_type_re = qr{
370             (?:
371             (?: shared | local )
372             (?:
373             \s+
374             (?: hit | read | dirtied | written ) = [1-9]\d*
375             )+
376             |
377             temp
378             (?:
379             \s+
380             (?: read | written ) = [1-9]\d*
381             )+
382             )
383             }xms;
384 145 50       5324 croak( 'Invalid format of string for Pg::Explain::Buffers: ' . $in ) unless $in =~ m{
385             \A
386             \s*
387             Buffers:
388             \s+
389             (
390             $single_type_re
391             (?:
392             , \s+
393             $single_type_re
394             )*
395             )
396             \s*
397             \z
398             }xms;
399 145         438 my $plain_info = $1;
400 145         572 my @parts = split /,\s+/, $plain_info;
401 145         1645 $self->{ 'data' } = {};
402              
403 145         343 for my $part ( @parts ) {
404 163         439 my @words = split /\s+/, $part;
405 163         359 my $type = shift @words;
406 163         306 for my $word ( @words ) {
407 226         784 my ( $op, $bufs ) = split /=/, $word;
408 226         1042 $self->{ 'data' }->{ $type }->{ $op } = $bufs;
409             }
410             }
411              
412 145         541 return;
413             }
414              
415             =head2 _buffers_add
416              
417             Creates new Pg::Explain::Buffers object by adding values based on two objects. To be used like:
418              
419             my $result = $buffers1 + $buffers2;
420              
421             =cut
422              
423             sub _buffers_add {
424 8     8   36 my ( $left, $right ) = @_;
425 8 50       38 return unless 'Pg::Explain::Buffers' eq ref $left;
426 8 50       30 unless ( 'Pg::Explain::Buffers' eq ref $right ) {
427 0 0       0 return if defined $right;
428 0         0 my $res = Pg::Explain::Buffers->new( {} );
429 0         0 $res->data( clone( $left->data ) );
430 0         0 return $res;
431             }
432              
433 8         14 my $D = {};
434 8         79 my $map = {
435             'shared' => [ qw{ hit read dirtied written } ],
436             'local' => [ qw{ hit read dirtied written } ],
437             'temp' => [ qw{ read written } ],
438             'timings' => [ qw{ read write } ],
439             };
440              
441 8 50       27 my $L = $left->data ? clone( $left->data ) : {};
442 8 100       22 my $R = $right->data ? clone( $right->data ) : {};
443 8         18 while ( my ( $type, $subtypes ) = each %{ $map } ) {
  40         120  
444 32         43 for my $subtype ( @{ $subtypes } ) {
  32         47  
445 96   100     380 my $val = ( $L->{ $type }->{ $subtype } // 0 ) + ( $R->{ $type }->{ $subtype } // 0 );
      100        
446 96 100       190 next if $val <= 0;
447 22         56 $D->{ $type }->{ $subtype } = $val;
448             }
449             }
450 8 50       12 return if 0 == scalar keys %{ $D };
  8         53  
451              
452 8         68 my $ret = Pg::Explain::Buffers->new( {} );
453 8         63 $ret->data( $D );
454 8         89 return $ret;
455             }
456              
457             =head2 _buffers_subtract
458              
459             Creates new Pg::Explain::Buffers object by subtracting values based on two objects. To be used like:
460              
461             my $result = $buffers1 - $buffers2;
462              
463             =cut
464              
465             sub _buffers_subtract {
466 8     8   20 my ( $left, $right ) = @_;
467 8 50       24 return unless 'Pg::Explain::Buffers' eq ref $left;
468 8 50       40 return unless 'Pg::Explain::Buffers' eq ref $right;
469              
470 8         51 my $map = {
471             'shared' => [ qw{ hit read dirtied written } ],
472             'local' => [ qw{ hit read dirtied written } ],
473             'temp' => [ qw{ read written } ],
474             'timings' => [ qw{ read write } ],
475             };
476              
477 8 50       23 return unless $left->data;
478 8 100       16 unless ( $right->data ) {
479 1         17 my $res = Pg::Explain::Buffers->new( {} );
480 1         4 $res->data( clone( $left->data ) );
481 1         6 return $res;
482             }
483              
484 7         13 my $new_data = {};
485 7         11 while ( my ( $type, $subtypes ) = each %{ $map } ) {
  35         85  
486 28 100       48 next unless my $L = $left->data->{ $type };
487 13 100       26 if ( my $R = $right->data->{ $type } ) {
488 12         52 for my $subtype ( @{ $subtypes } ) {
  12         23  
489 40   100     155 my $val = ( $L->{ $subtype } // 0 ) - ( $R->{ $subtype } // 0 );
      100        
490              
491             # Weirdish comparison to get rid of floating point arithmetic errors, like:
492             # 32.974 - 18.153 - 14.721 => 3.5527136788005e-15
493 40 100       80 next if $val <= 0.00001;
494 4         11 $new_data->{ $type }->{ $subtype } = $val;
495             }
496             }
497             else {
498 1         10 $new_data->{ $type } = clone( $L );
499             }
500             }
501 7 100       13 return if 0 == scalar keys %{ $new_data };
  7         61  
502              
503 4         19 my $ret = Pg::Explain::Buffers->new( {} );
504 4         12 $ret->data( $new_data );
505 4         21 return $ret;
506             }
507              
508             =head2 _buffers_bool
509              
510             For checking if given variable is set, as in:
511              
512             $r = $buffers1 - $buffers2;
513             if ( $r ) {...}
514              
515             =cut
516              
517             sub _buffers_bool {
518 1788     1788   3380 my $self = shift;
519 1788 100       5600 return unless $self->data;
520 816         1191 return 0 < scalar keys %{ $self->data };
  816         1354  
521             }
522              
523             =head1 AUTHOR
524              
525             hubert depesz lubaczewski, C<< >>
526              
527             =head1 BUGS
528              
529             Please report any bugs or feature requests to C.
530              
531             =head1 SUPPORT
532              
533             You can find documentation for this module with the perldoc command.
534              
535             perldoc Pg::Explain::Buffers
536              
537             =head1 COPYRIGHT & LICENSE
538              
539             Copyright 2008-2023 hubert depesz lubaczewski, all rights reserved.
540              
541             This program is free software; you can redistribute it and/or modify it
542             under the same terms as Perl itself.
543              
544             =cut
545              
546             1; # End of Pg::Explain::Buffers