File Coverage

blib/lib/Pg/Explain.pm
Criterion Covered Total %
statement 315 323 97.5
branch 136 160 85.0
condition 14 19 73.6
subroutine 45 45 100.0
pod 25 25 100.0
total 535 572 93.5


line stmt bran cond sub pod time code
1             package Pg::Explain;
2              
3             # UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
4 72     72   6148762 use v5.18;
  72         880  
5 72     72   356 use strict;
  72         103  
  72         1484  
6 72     72   293 use warnings;
  72         136  
  72         2148  
7 72     72   296 use warnings qw( FATAL utf8 );
  72         103  
  72         2639  
8 72     72   37847 use utf8;
  72         923  
  72         309  
9 72     72   30354 use open qw( :std :utf8 );
  72         75213  
  72         361  
10 72     72   45311 use Unicode::Normalize qw( NFC );
  72         140679  
  72         5043  
11 72     72   47271 use Unicode::Collate;
  72         547043  
  72         2746  
12 72     72   38083 use Encode qw( decode );
  72         642790  
  72         6946  
13              
14 72     72   480 if ( grep /\P{ASCII}/ => @ARGV ) {
  72         154  
  72         743  
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 72     72   1281012 use Carp;
  72         145  
  72         4588  
21 72     72   28759 use Clone qw( clone );
  72         136049  
  72         3792  
22 72     72   5566 use autodie;
  72         142276  
  72         479  
23 72     72   310502 use List::Util qw( sum );
  72         140  
  72         6590  
24 72     72   33029 use Pg::Explain::StringAnonymizer;
  72         166  
  72         2317  
25 72     72   28736 use Pg::Explain::FromText;
  72         179  
  72         2665  
26 72     72   28852 use Pg::Explain::FromYAML;
  72         172  
  72         2241  
27 72     72   28143 use Pg::Explain::FromJSON;
  72         179  
  72         2420  
28 72     72   27581 use Pg::Explain::FromXML;
  72         183  
  72         279243  
29              
30             =head1 NAME
31              
32             Pg::Explain - Object approach at reading explain analyze output
33              
34             =head1 VERSION
35              
36             Version 2.2
37              
38             =cut
39              
40             our $VERSION = '2.2';
41              
42             =head1 SYNOPSIS
43              
44             Quick summary of what the module does.
45              
46             Perhaps a little code snippet.
47              
48             use Pg::Explain;
49              
50             my $explain = Pg::Explain->new('source_file' => 'some_file.out');
51             ...
52              
53             my $explain = Pg::Explain->new(
54             'source' => 'Seq Scan on tenk1 (cost=0.00..333.00 rows=10000 width=148)'
55             );
56             ...
57              
58              
59             =head1 FUNCTIONS
60              
61             =head2 source_format
62              
63             What is the detected format of source plan. One of: TEXT, JSON, YAML, OR XML.
64              
65             =head2 planning_time
66              
67             How much time PostgreSQL spent planning the query. In milliseconds.
68              
69             =head2 total_buffers
70              
71             All buffers used by query - for planning and execution. Mathematically: sum of planning_buffers and top_level->buffers.
72              
73             =head2 planning_buffers
74              
75             How much buffers PostgreSQL used for planning. Either undef or object of Pg::Explain::Buffers class.
76              
77             =head2 execution_time
78              
79             How much time PostgreSQL spent executing the query. In milliseconds.
80              
81             =head2 total_runtime
82              
83             How much time PostgreSQL spent working on this query. This was part of EXPLAIN OUTPUT only for PostgreSQL 9.3 or older.
84              
85             =head2 trigger_times
86              
87             Information about triggers that were called during execution of this query. Array of hashes, where each hash can contains:
88              
89             =over
90              
91             =item * name - name of the trigger
92              
93             =item * calls - how many times it was called
94              
95             =item * time - total time spent in all executions of this trigger
96              
97             =back
98              
99             =head2 jit
100              
101             Contains information about JIT timings, as object of Pg::Explain::JIT class.
102              
103             If there was no JIT info, it will return undef.
104              
105             =head2 query
106              
107             What query this explain is for. This is available only for auto-explain plans. If not available, it will be undef.
108              
109             =head2 settings
110              
111             If explain contains information about specific settings that were changed in Pg, this hashref will contain it.
112              
113             If there are none - if will be undef.
114              
115             =cut
116              
117 168 50   168 1 37083 sub source_format { my $self = shift; $self->{ 'source_format' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'source_format' }; }
  168         451  
  168         871  
118 464 100   464 1 3195 sub planning_time { my $self = shift; $self->{ 'planning_time' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'planning_time' }; }
  464         1219  
  464         1154  
119 188 100   188 1 273 sub planning_buffers { my $self = shift; $self->{ 'planning_buffers' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'planning_buffers' }; }
  188         399  
  188         422  
120 478 100   478 1 668 sub execution_time { my $self = shift; $self->{ 'execution_time' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'execution_time' }; }
  478         1164  
  478         1274  
121 237 100   237 1 334 sub total_runtime { my $self = shift; $self->{ 'total_runtime' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'total_runtime' }; }
  237         537  
  237         582  
122 211 100   211 1 267 sub trigger_times { my $self = shift; $self->{ 'trigger_times' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'trigger_times' }; }
  211         369  
  211         662  
123 196 100   196 1 6236 sub jit { my $self = shift; $self->{ 'jit' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'jit' }; }
  196         370  
  196         504  
124 104 100   104 1 163 sub query { my $self = shift; $self->{ 'query' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'query' }; }
  104         275  
  104         262  
125 70 100   70 1 120 sub settings { my $self = shift; $self->{ 'settings' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'settings' }; }
  70         145  
  70         186  
126              
127             sub total_buffers {
128 3     3 1 12 my $self = shift;
129 3 50       6 if ( $self->top_node->buffers ) {
130 3 100       8 return $self->top_node->buffers + $self->planning_buffers if $self->planning_buffers;
131 1         3 return $self->top_node->buffers;
132             }
133 0 0       0 return $self->planning_buffers if $self->planning_buffers;
134 0         0 return;
135             }
136              
137             =head2 add_trigger_time
138              
139             Adds new information about trigger time.
140              
141             It will be available at $node->trigger_times (returns arrayref)
142              
143             =cut
144              
145             sub add_trigger_time {
146 26     26 1 34 my $self = shift;
147 26 100       50 if ( $self->trigger_times ) {
148 16         20 push @{ $self->trigger_times }, @_;
  16         27  
149             }
150             else {
151 10         29 $self->trigger_times( [ @_ ] );
152             }
153 26         61 return;
154             }
155              
156             =head2 runtime
157              
158             How long did the query run. Tries to get the value from various sources (total_runtime, execution_time, or top_node->actual_time_last).
159              
160             =cut
161              
162             sub runtime {
163 5     5 1 68 my $self = shift;
164              
165 5   100     10 return $self->total_runtime // $self->execution_time // $self->top_node->actual_time_last;
      100        
166             }
167              
168             =head2 node
169              
170             Returns node with given id from current explain.
171              
172             If there is second argument present, and it's Pg::Explain::Node object, it sets internal cache for this id and this node.
173              
174             =cut
175              
176             sub node {
177 1568     1568 1 2026 my $self = shift;
178 1568         1954 my $id = shift;
179 1568 50       2778 return unless defined $id;
180 1568         1857 my $node = shift;
181 1568 100       4748 $self->{ 'node_by_id' }->{ $id } = $node if defined $node;
182 1568         2933 return $self->{ 'node_by_id' }->{ $id };
183             }
184              
185             =head2 source
186              
187             Returns original source (text version) of explain.
188              
189             =cut
190              
191             sub source {
192 510     510 1 16090 return shift->{ 'source' };
193             }
194              
195             =head2 source_filtered
196              
197             Returns filtered source explain.
198              
199             Currently there are only two filters:
200              
201             =over
202              
203             =item * remove quotes added by pgAdmin3
204              
205             =item * remove + character at the end of line, added by default psql config.
206              
207             =back
208              
209             =cut
210              
211             sub source_filtered {
212 510     510 1 785 my $self = shift;
213              
214 510         882 my $filtered = '';
215              
216             # use default variable, to avoid having to type variable name in all regexps below
217 510         1347 for ( split /\r?\n/, $self->source ) {
218              
219             # Remove separator lines from various types of borders
220 13692 100       20869 next if /^\+-+\+\z/;
221 13650 100       25360 next if /^[-─═]+\z/;
222 13506 100       20427 next if /^(?:├|╟|╠|╞)[─═]+(?:┤|╢|╣|╡)\z/;
223              
224             # Remove more horizontal lines
225 13474 50       18525 next if /^\+-+\+\z/;
226 13474 100       17706 next if /^└─+┘\z/;
227 13458 100       17931 next if /^╚═+╝\z/;
228 13442 100       17823 next if /^┌─+┐\z/;
229 13426 100       17908 next if /^╔═+╗\z/;
230              
231             # Remove frames around, handles |, ║, │
232 13410         23526 s/^(\||║|│)(.*)\1\z/$2/;
233              
234             # Remove quotes around lines, both ' and "
235 13410         21019 s/^(["'])(.*)\1\z/$2/;
236              
237             # Remove "+" line continuations
238 13410         24459 s/\s*\+\z//;
239              
240             # Remove "↵" line continuations
241 13410         25841 s/\s*↵\z//;
242              
243             # Remove "query plan" header
244 13410 100       20996 next if /^\s*QUERY PLAN\s*\z/;
245              
246             # Remove rowcount
247 13207 100       19418 next if /^\(\d+ rows?\)\z/;
248              
249             # Accumulate filtered source
250 13009         22261 $filtered .= $_ . "\n";
251             }
252              
253 510         2752 return $filtered;
254             }
255              
256             =head2 new
257              
258             Object constructor.
259              
260             Takes one of (only one!) (source, source_file) parameters, and either parses it from given source, or first reads given file.
261              
262             =cut
263              
264             sub new {
265 514     514 1 1383668 my $class = shift;
266 514         1284 my $self = bless {}, $class;
267 514         902 my %args;
268 514 100       1661 if ( 0 == scalar @_ ) {
269 1         23 croak( 'One of (source, source_file) parameters has to be provided)' );
270             }
271 513 50       2199 if ( 1 == scalar @_ ) {
    50          
272 0 0       0 if ( 'HASH' eq ref $_[ 0 ] ) {
273 0         0 %args = @{ $_[ 0 ] };
  0         0  
274             }
275             else {
276 0         0 croak( 'One of (source, source_file) parameters has to be provided)' );
277             }
278             }
279             elsif ( 1 == ( scalar( @_ ) % 2 ) ) {
280 0         0 croak( 'One of (source, source_file) parameters has to be provided)' );
281             }
282             else {
283 513         1844 %args = @_;
284             }
285              
286 513 100       1581 if ( $args{ 'source_file' } ) {
    50          
287 179 100       490 croak( 'Only one of (source, source_file) parameters has to be provided)' ) if $args{ 'source' };
288 178         448 $self->{ 'source_file' } = $args{ 'source_file' };
289 178         547 $self->_read_source_from_file();
290             }
291             elsif ( $args{ 'source' } ) {
292 334 100       1500 if ( Encode::is_utf8( $args{ 'source' } ) ) {
293 47         105 $self->{ 'source' } = $args{ 'source' };
294             }
295             else {
296 287         1318 $self->{ 'source' } = decode( 'UTF-8', $args{ 'source' } );
297             }
298             }
299             else {
300 0         0 croak( 'One of (source, source_file) parameters has to be provided)' );
301             }
302              
303             # Initialize jit to undef
304 511         25534 $self->{ 'jit' } = undef;
305              
306             # Initialize node_by_id hash to empty
307 511         1087 $self->{ 'node_by_id' } = {};
308              
309 511         1637 return $self;
310             }
311              
312             =head2 top_node
313              
314             This method returns the top node of parsed plan.
315              
316             For example - in this plan:
317              
318             QUERY PLAN
319             --------------------------------------------------------------
320             Limit (cost=0.00..0.01 rows=1 width=4)
321             -> Seq Scan on test (cost=0.00..14.00 rows=1000 width=4)
322              
323             top_node is Pg::Explain::Node element with type set to 'Limit'.
324              
325             Generally every output of plans should start with ->top_node(), and descend
326             recursively in it, using subplans(), initplans() and sub_nodes() methods.
327              
328             =cut
329              
330             sub top_node {
331 4574     4574 1 163504 my $self = shift;
332 4574 100       7870 $self->parse_source() unless $self->{ 'top_node' };
333 4574         10787 return $self->{ 'top_node' };
334             }
335              
336             =head2 parse_source
337              
338             Internally (from ->BUILD()) called function which checks which parser to use
339             (text, json, xml, yaml), runs appropriate function, and stores top level
340             node in $self->top_node.
341              
342             =cut
343              
344             sub parse_source {
345 510     510 1 132049 my $self = shift;
346              
347 510         1313 my $source = $self->source_filtered;
348              
349 510         815 my $parser;
350              
351 510 100       4675 if ( $source =~ m{^\s*}m ) {
    100          
    100          
    100          
    100          
352              
353             # Format used by both explain command and autoexplain module
354 67         233 $self->{ 'source_format' } = 'XML';
355 67         691 $parser = Pg::Explain::FromXML->new();
356             }
357             elsif ( $source =~ m{ ^ \s* \[ \s* \{ \s* "Plan" \s* : \s* \{ }xms ) {
358              
359             # Format used by explain command
360 70         233 $self->{ 'source_format' } = 'JSON';
361 70         666 $parser = Pg::Explain::FromJSON->new();
362             }
363             elsif ( $source =~ m{ ^ \s* \{ \s* "Query \s+ Text" \s* : \s* ".*", \s* "Plan" \s* : \s* \{ .* \} \s* \z }xms ) {
364              
365             # Format used by autoexplain module
366 4         14 $self->{ 'source_format' } = 'JSON';
367 4         36 $parser = Pg::Explain::FromJSON->new();
368             }
369             elsif ( $source =~ m{ ^ \s* - \s+ Plan: \s* \n }xms ) {
370              
371             # Format used by explain command
372 66         183 $self->{ 'source_format' } = 'YAML';
373 66         477 $parser = Pg::Explain::FromYAML->new();
374             }
375             elsif ( $source =~ m{ ^ \s* Query \s+ Text: \s+ ".*" \s+ Plan: \s* \n }xms ) {
376              
377             # Format used by autoexplain module
378 4         13 $self->{ 'source_format' } = 'YAML';
379 4         39 $parser = Pg::Explain::FromYAML->new();
380             }
381             else {
382             # Format used by both explain command and autoexplain module
383 299         702 $self->{ 'source_format' } = 'TEXT';
384 299         1942 $parser = Pg::Explain::FromText->new();
385             }
386              
387 510         1854 $parser->explain( $self );
388              
389 510         1742 $self->{ 'top_node' } = $parser->parse_source( $source );
390              
391 510         1783 $self->check_for_parallelism();
392              
393 510         1415 $self->check_for_exclusive_time_fixes();
394              
395 510         2798 return;
396             }
397              
398             =head2 check_for_exclusive_time_fixes
399              
400             Certain types of nodes (CTE Scans, and InitPlans) can cause issues with "naive" calculations of node exclusive time.
401              
402             To fix that whole tree will be scanned, and, if neccessary, node->exclusive_fix will be modified.
403              
404             =cut
405              
406             sub check_for_exclusive_time_fixes {
407 510     510 1 777 my $self = shift;
408 510         1350 $self->check_for_exclusive_time_fixes_cte();
409 510         1254 $self->check_for_exclusive_time_fixes_init();
410             }
411              
412             =head2 check_for_exclusive_time_fixes_cte
413              
414             Modifies node->exclusive_fix according to times that were used by CTEs.
415              
416             =cut
417              
418             sub check_for_exclusive_time_fixes_cte {
419 510     510 1 655 my $self = shift;
420              
421             # Safeguard against endless loop in some edge cases.
422 510 50       1100 return unless defined $self->{ 'top_node' };
423              
424             # There is no point in checking if the plan is not analyzed.
425 510 100       982 return unless $self->top_node->is_analyzed;
426              
427             # Find nodes that have any ctes in them
428 411 100       945 my @nodes_with_cte = grep { $_->ctes && 0 < scalar keys %{ $_->ctes } } ( $self->top_node, $self->top_node->all_recursive_subnodes );
  1284         2239  
  22         49  
429              
430             # For each node with cte in it...
431 411         994 for my $node ( @nodes_with_cte ) {
432              
433             # Find all nodes that are 'CTE Scan' - from given node, and all of its subnodes (recursively)
434 22         88 my @cte_scans = grep { $_->type eq 'CTE Scan' } ( $node, $node->all_recursive_subnodes );
  190         624  
435 22 50       84 next if 0 == scalar @cte_scans;
436              
437             # Iterate over defined ctes
438 22         41 while ( my ( $cte_name, $cte_node ) = each %{ $node->ctes } ) {
  53         122  
439              
440             # Find all CTE Scans that were scanning current CTE
441 31         61 my @matching_cte_scans = grep { $_->scan_on->{ 'cte_name' } eq $cte_name } @cte_scans;
  58         113  
442 31 50       85 next if 0 == scalar @matching_cte_scans;
443              
444             # How much time did Pg spend in given CTE itself
445 31         88 my $cte_total_time = $cte_node->total_inclusive_time;
446              
447             # How much time did all the CTE Scans used
448 31   50     103 my $total_time_of_scans = sum( map { $_->total_inclusive_time // 0 } @matching_cte_scans );
  36         79  
449              
450             # Don't fail on divide by 0, and don't warn on undef
451 31 50       75 next unless $total_time_of_scans;
452 31 100       81 next unless $cte_total_time;
453              
454             # Subtract exclusive time proportionally.
455 23         42 for my $scan ( grep { $_->total_inclusive_time } @matching_cte_scans ) {
  28         70  
456 28         101 $scan->exclusive_fix( $scan->exclusive_fix - ( $scan->total_inclusive_time / $total_time_of_scans ) * $cte_total_time );
457             }
458             }
459             }
460 411         629 return;
461             }
462              
463             =head2 check_for_exclusive_time_fixes_init
464              
465             Modifies node->exclusive_fix according to times that were used by InitScans.
466              
467             =cut
468              
469             sub check_for_exclusive_time_fixes_init {
470 510     510 1 707 my $self = shift;
471              
472             # Safeguard against endless loop in some edge cases.
473 510 50       1248 return unless defined $self->{ 'top_node' };
474              
475             # There is no point in checking if the plan is not analyzed.
476 510 100       1017 return unless $self->top_node->is_analyzed;
477              
478             # Find nodes that have any init plans in them
479 411 100       999 my @nodes_with_init = grep { $_->initplans && 0 < scalar @{ $_->initplans } } ( $self->top_node, $self->top_node->all_recursive_subnodes );
  1284         2049  
  34         69  
480              
481             # Check them all, one by one, to build "init-plan-visibility" info
482 411         795 for my $parent ( @nodes_with_init ) {
483              
484             # Nodes that see what initplan returned even if they don't refer to returned $*
485 34         93 my @all_implicits = map { $_->id } ( $parent, $parent->all_recursive_subnodes );
  113         189  
486 34         68 my %skip_self_implicit = ();
487              
488             # Scan all initplans
489 34         52 for my $idx ( 0 .. $#{ $parent->initplans } ) {
  34         67  
490 37         80 my $initnode = $parent->initplans->[ $idx ];
491              
492             # There is no point in adjusting things for no-time.
493 37 100       106 next unless $initnode->total_inclusive_time;
494              
495             # Place to store implicit and explicit nodes
496 36         81 my @implicitnodes = ();
497 36         50 my @explicitnodes = ();
498              
499 36         44 my $explicit_re;
500              
501             # If there is metainfo, we can build regexp to find nodes explicitly using this init
502 36 100       104 if ( $parent->initplans_metainfo->[ $idx ] ) {
503              
504             # List of $* variables that this initplan returns
505 22         46 my $returns_string = $parent->initplans_metainfo->[ $idx ]->{ 'returns' };
506 22         49 my @returns_numbers = ();
507 22         87 for my $element ( split /,/, $returns_string ) {
508 30 50       214 push @returns_numbers, $element if $element =~ s/\A\$(\d+)\z/$1/;
509             }
510 22         64 my $returns = join( '|', @returns_numbers );
511              
512             # Regular expression to check in extra-info for nodes.
513 22         443 $explicit_re = qr{\$(?:${returns})(?!\d)};
514             }
515              
516             # Add current node, and it's kids to skip list
517 36         144 for my $skip_node ( $initnode, $initnode->all_recursive_subnodes ) {
518 61         132 $skip_self_implicit{ $skip_node->id } = 1;
519             }
520              
521             # Iterate over all nodes that could have used data from this initplan
522 36         67 for my $user_id ( grep { !$skip_self_implicit{ $_ } } @all_implicits ) {
  118         247  
523              
524 54         120 my $user = $self->node( $user_id );
525              
526             # Add node to implicit ones,always
527 54         100 push @implicitnodes, $user;
528              
529             # If there is explicit_re, try to find what is using this int explicitly
530 54 100       124 next unless $explicit_re;
531 32 100       73 next unless $user->extra_info;
532 23         36 my $full_extra_info = join( "\n", @{ $user->extra_info } );
  23         49  
533 23 100       208 push @explicitnodes, $user if $full_extra_info =~ $explicit_re;
534             }
535              
536             # Total times
537 36   50     68 my $implicittime = sum( map { $_->total_exclusive_time // 0 } @implicitnodes ) // 0;
  54   50     185  
538 36   50     117 my $explicittime = sum( map { $_->total_exclusive_time // 0 } @explicitnodes ) // 0;
  18   100     41  
539              
540             # Where to adjusct exclusive time
541 36         64 my @adjust_these = ();
542 36         45 my $ratio;
543 36 100 66     168 if ( ( 0 < scalar @explicitnodes )
    50          
544             && ( $explicittime > $initnode->total_inclusive_time ) )
545             {
546 17         59 @adjust_these = @explicitnodes;
547 17         60 $ratio = $initnode->total_inclusive_time / $explicittime;
548             }
549             elsif ( $implicittime > $initnode->total_inclusive_time ) {
550 19         45 @adjust_these = @implicitnodes;
551 19         40 $ratio = $initnode->total_inclusive_time / $implicittime;
552             }
553              
554             # Actually adjust exclusive times
555 36         70 for my $node ( @adjust_these ) {
556 48 50       111 next unless $node->total_exclusive_time;
557 48         97 my $adjust = $ratio * $node->total_exclusive_time;
558 48         128 $node->exclusive_fix( $node->exclusive_fix - $adjust );
559             }
560             }
561             }
562              
563 411         622 return;
564             }
565              
566             =head2 check_for_parallelism
567              
568             Handles parallelism by setting "force_loops" if plan is analyzed and there are gather nodes.
569              
570             Generally, for each
571              
572             =cut
573              
574             sub check_for_parallelism {
575 510     510 1 786 my $self = shift;
576              
577             # Safeguard against endless loop in some edge cases.
578 510 50       1373 return unless defined $self->{ 'top_node' };
579              
580             # There is no point in checking if the plan is not analyzed.
581 510 100       1399 return unless $self->top_node->is_analyzed;
582              
583             # @nodes will contain list of nodes to check if they are Gather
584 411         1006 my @nodes = ( [ 1, $self->top_node ] );
585              
586 411         1168 while ( my $node_info = shift @nodes ) {
587              
588 1284         1791 my $workers = $node_info->[ 0 ];
589 1284         1475 my $node = $node_info->[ 1 ];
590              
591             # Set workers.
592 1284         2876 $node->workers( $workers );
593              
594             # These sub-nodes don't get workers.
595 1284 100       2390 push @nodes, map { [ $workers, $_ ] } @{ $node->initplans } if $node->initplans;
  37         89  
  34         83  
596 1284 100       2365 push @nodes, map { [ $workers, $_ ] } @{ $node->subplans } if $node->subplans;
  29         80  
  24         61  
597 1284 100       2446 push @nodes, map { [ $workers, $_ ] } values %{ $node->ctes } if $node->ctes;
  31         85  
  22         59  
598              
599             # If there are workers launched, set it as new workers value for recursive set.
600 1284 100       2406 $workers = 1 + $node->workers_launched if defined $node->workers_launched;
601              
602             # These things get new workers
603 1284 100       2200 push @nodes, map { [ $workers, $_ ] } @{ $node->sub_nodes } if $node->sub_nodes;
  776         2494  
  540         1062  
604             }
605 411         668 return;
606             }
607              
608             =head2 _read_source_from_file
609              
610             Helper function to read source from file.
611              
612             =cut
613              
614             sub _read_source_from_file {
615 178     178   298 my $self = shift;
616              
617 178         825 open my $fh, '<', $self->{ 'source_file' };
618 177         64394 local $/ = undef;
619 177         8939 my $content = <$fh>;
620 177         1092 close $fh;
621              
622 177         26264 delete $self->{ 'source_file' };
623 177         471 $self->{ 'source' } = $content;
624              
625 177         872 return;
626             }
627              
628             =head2 as_text
629              
630             Returns parsed plan back as plain text format (regenerated from in-memory structure).
631              
632             This is mostly useful for (future at the moment) anonymizations.
633              
634             =cut
635              
636             sub as_text {
637 89     89 1 2093 my $self = shift;
638              
639 89         222 my $textual = $self->top_node->as_text();
640              
641 89 100       294 if ( $self->planning_buffers ) {
642 8         17 $textual .= "Planning:\n";
643 8         14 my $buf_info = $self->planning_buffers->as_text;
644 8         35 $buf_info =~ s/^/ /gm;
645 8         19 $textual .= $buf_info . "\n";
646              
647             }
648 89 100       255 if ( $self->planning_time ) {
649 23         73 $textual .= "Planning time: " . $self->planning_time . " ms\n";
650             }
651 89 100       251 if ( $self->trigger_times ) {
652 5         7 for my $t ( @{ $self->trigger_times } ) {
  5         17  
653 13         74 $textual .= sprintf( "Trigger %s: time=%.3f calls=%d\n", $t->{ 'name' }, $t->{ 'time' }, $t->{ 'calls' } );
654             }
655             }
656 89 100       236 if ( $self->jit ) {
657 4         10 $textual .= $self->jit->as_text();
658             }
659 89 100       258 if ( $self->execution_time ) {
660 32         84 $textual .= "Execution time: " . $self->execution_time . " ms\n";
661             }
662 89 100       229 if ( $self->total_runtime ) {
663 17         33 $textual .= "Total runtime: " . $self->total_runtime . " ms\n";
664             }
665              
666 89         518 return $textual;
667             }
668              
669             =head2 get_struct
670              
671             Function which returns simple, not blessed, hashref with all information about the explain.
672              
673             This can be used for debug purposes, or as a base to print information to user.
674              
675             Output looks like this:
676              
677             {
678             'top_node' => {...}
679             'planning_time' => '12.34',
680             'planning_buffers' => {...},
681             'execution_time' => '12.34',
682             'total_runtime' => '12.34',
683             'trigger_times' => [
684             { 'name' => ..., 'time' => ..., 'calls' => ... },
685             ...
686             ],
687             }
688              
689             =cut
690              
691             sub get_struct {
692 50     50 1 17700 my $self = shift;
693 50         97 my $reply = {};
694 50         140 $reply->{ 'top_node' } = $self->top_node->get_struct;
695 50 100       129 $reply->{ 'planning_time' } = $self->planning_time if $self->planning_time;
696 50 100       142 $reply->{ 'planning_buffers' } = $self->planning_buffers->get_struct if $self->planning_buffers;
697 50 50       108 $reply->{ 'execution_time' } = $self->execution_time if $self->execution_time;
698 50 50       158 $reply->{ 'total_runtime' } = $self->total_runtime if $self->total_runtime;
699 50 100       111 $reply->{ 'trigger_times' } = clone( $self->trigger_times ) if $self->trigger_times;
700 50 50       147 $reply->{ 'query' } = $self->query if $self->query;
701 50 50       126 $reply->{ 'settings' } = $self->settings if $self->settings;
702              
703 50 100       114 if ( $self->jit ) {
704 8         15 $reply->{ 'jit' } = {};
705 8         15 $reply->{ 'jit' }->{ 'functions' } = $self->jit->functions;
706 8         19 $reply->{ 'jit' }->{ 'options' } = clone( $self->jit->options );
707 8         20 $reply->{ 'jit' }->{ 'timings' } = clone( $self->jit->timings );
708             }
709 50         170 return $reply;
710             }
711              
712             =head2 anonymize
713              
714             Used to remove all individual values from the explain, while still retaining
715             all values that are needed to see what's wrong.
716              
717             If there are any arguments, these are treated as strings, anonymized using
718             anonymizer used for plan, and are returned in the same order.
719              
720             This is mainly useful to anonymize queries.
721              
722             =cut
723              
724             sub anonymize {
725 14     14 1 477 my $self = shift;
726 14         37 my @extra_args = @_;
727              
728 14         104 my $anonymizer = Pg::Explain::StringAnonymizer->new();
729 14         37 $self->top_node->anonymize_gathering( $anonymizer );
730 14         72 $anonymizer->finalize();
731 14         52 $self->top_node->anonymize_substitute( $anonymizer );
732              
733 14 100       204 return if 0 == scalar @extra_args;
734              
735 1         2 return map { $anonymizer->anonymize_text( $_ ) } @extra_args;
  1         6  
736             }
737              
738             =head1 AUTHOR
739              
740             hubert depesz lubaczewski, C<< >>
741              
742             =head1 BUGS
743              
744             Please report any bugs or feature requests to C.
745              
746             =head1 SUPPORT
747              
748             You can find documentation for this module with the perldoc command.
749              
750             perldoc Pg::Explain
751              
752             =head1 COPYRIGHT & LICENSE
753              
754             Copyright 2008-2021 hubert depesz lubaczewski, all rights reserved.
755              
756             This program is free software; you can redistribute it and/or modify it
757             under the same terms as Perl itself.
758              
759              
760             =cut
761              
762             1; # End of Pg::Explain