File Coverage

blib/lib/Pg/Explain.pm
Criterion Covered Total %
statement 314 324 96.9
branch 139 164 84.7
condition 14 19 73.6
subroutine 44 44 100.0
pod 25 25 100.0
total 536 576 93.0


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 79     79   12971329 use v5.18;
  79         434  
5 79     79   450 use strict;
  79         403  
  79         8881  
6 79     79   421 use warnings;
  79         161  
  79         11667  
7 79     79   470 use warnings qw( FATAL utf8 );
  79         297  
  79         5002  
8 79     79   44974 use utf8;
  79         27314  
  79         493  
9 79     79   40575 use open qw( :std :utf8 );
  79         121294  
  79         553  
10 79     79   53003 use Unicode::Normalize qw( NFC );
  79         322502  
  79         8149  
11 79     79   56703 use Unicode::Collate;
  79         877016  
  79         4965  
12 79     79   50907 use Encode qw( decode );
  79         1422166  
  79         16786  
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 79     79   773 use Carp;
  79         169  
  79         6572  
21 79     79   43421 use Clone qw( clone );
  79         41122  
  79         5668  
22 79     79   7777 use autodie;
  79         251479  
  79         740  
23 79     79   479360 use List::Util qw( sum );
  79         178  
  79         7112  
24 79     79   52521 use Pg::Explain::StringAnonymizer;
  79         284  
  79         3755  
25 79     79   48994 use Pg::Explain::FromText;
  79         393  
  79         5735  
26 79     79   44279 use Pg::Explain::FromYAML;
  79         364  
  79         4591  
27 79     79   58586 use Pg::Explain::FromJSON;
  79         406  
  79         4244  
28 79     79   43097 use Pg::Explain::FromXML;
  79         433  
  79         474701  
29              
30             =head1 NAME
31              
32             Pg::Explain - Object approach at reading explain analyze output
33              
34             =head1 VERSION
35              
36             Version 2.9
37              
38             =cut
39              
40             our $VERSION = '2.9';
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 51975 sub source_format { my $self = shift; $self->{ 'source_format' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'source_format' }; }
  168         680  
  168         1079  
118 547 100   547 1 5461 sub planning_time { my $self = shift; $self->{ 'planning_time' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'planning_time' }; }
  547         1899  
  547         1966  
119 242 100   242 1 2084 sub planning_buffers { my $self = shift; $self->{ 'planning_buffers' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'planning_buffers' }; }
  242         681  
  242         993  
120 566 100   566 1 1058 sub execution_time { my $self = shift; $self->{ 'execution_time' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'execution_time' }; }
  566         2290  
  566         1683  
121 265 100   265 1 501 sub total_runtime { my $self = shift; $self->{ 'total_runtime' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'total_runtime' }; }
  265         902  
  265         910  
122 239 100   239 1 397 sub trigger_times { my $self = shift; $self->{ 'trigger_times' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'trigger_times' }; }
  239         582  
  239         1049  
123 250 100   250 1 12478 sub jit { my $self = shift; $self->{ 'jit' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'jit' }; }
  250         640  
  250         940  
124 109 100   109 1 237 sub query { my $self = shift; $self->{ 'query' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'query' }; }
  109         399  
  109         305  
125 76 100   76 1 2469 sub settings { my $self = shift; $self->{ 'settings' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'settings' }; }
  76         189  
  76         358  
126              
127             sub total_buffers {
128 3     3 1 16 my $self = shift;
129 3 50       12 if ( $self->top_node->buffers ) {
130 3 100       13 return $self->top_node->buffers + $self->planning_buffers if $self->planning_buffers;
131 1         4 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 33 my $self = shift;
147 26 100       54 if ( $self->trigger_times ) {
148 16         20 push @{ $self->trigger_times }, @_;
  16         28  
149             }
150             else {
151 10         21 $self->trigger_times( [ @_ ] );
152             }
153 26         80 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 101 my $self = shift;
164              
165 5   100     15 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 1639     1639 1 2713 my $self = shift;
178 1639         2712 my $id = shift;
179 1639 50       3723 return unless defined $id;
180 1639         2542 my $node = shift;
181 1639 100       6983 $self->{ 'node_by_id' }->{ $id } = $node if defined $node;
182 1639         4153 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 542     542 1 27353 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 542     542 1 1054 my $self = shift;
213              
214 542         1227 my $filtered = '';
215              
216             # use default variable, to avoid having to type variable name in all regexps below
217 542         1875 for ( split /\r?\n/, $self->source ) {
218              
219             # Remove separator lines from various types of borders
220 15310 100       31549 next if /^\+-+\+\z/;
221 15268 100       41805 next if /^[-─═]+\z/;
222 15098 100       37462 next if /^(?:├|╟|╠|╞)[─═]+(?:┤|╢|╣|╡)\z/;
223              
224             # Remove more horizontal lines
225 15066 50       28628 next if /^\+-+\+\z/;
226 15066 100       29672 next if /^└─+┘\z/;
227 15050 100       28194 next if /^╚═+╝\z/;
228 15034 100       27984 next if /^┌─+┐\z/;
229 15018 100       29661 next if /^╔═+╗\z/;
230              
231             # Remove frames around, handles |, ║, │
232 15002         30953 s/^(\||║|│)(.*)\1\z/$2/;
233              
234             # Remove quotes around lines, both ' and "
235 15002         31869 s/^(["'])(.*)\1\z/$2/;
236              
237             # Remove "+" line continuations
238 15002         42572 s/\s*\+\z//;
239              
240             # Remove "↵" line continuations
241 15002         40716 s/\s*↵\z//;
242              
243             # Remove "query plan" header
244 15002 100       33419 next if /^\s*QUERY PLAN\s*\z/;
245              
246             # Remove rowcount
247 14773 100       32659 next if /^\(\d+ rows?\)\z/;
248              
249             # Accumulate filtered source
250 14549         26920 $filtered .= $_ . "\n";
251             }
252              
253 542         5176 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 546     546 1 19379950 my $class = shift;
266 546         1869 my $self = bless {}, $class;
267 546         1327 my %args;
268 546 100       2803 if ( 0 == scalar @_ ) {
269 1         36 croak( 'One of (source, source_file) parameters has to be provided)' );
270             }
271 545 50       3494 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 545         2405 %args = @_;
284             }
285              
286 545 100       2559 if ( $args{ 'source_file' } ) {
    50          
287 183 100       726 croak( 'Only one of (source, source_file) parameters has to be provided)' ) if $args{ 'source' };
288 182         706 $self->{ 'source_file' } = $args{ 'source_file' };
289 182         860 $self->_read_source_from_file();
290             }
291             elsif ( $args{ 'source' } ) {
292 362 100       1929 if ( Encode::is_utf8( $args{ 'source' } ) ) {
293 48         232 $self->{ 'source' } = $args{ 'source' };
294             }
295             else {
296 314         3527 $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 543         22881 $self->{ 'jit' } = undef;
305              
306             # Initialize node_by_id hash to empty
307 543         1513 $self->{ 'node_by_id' } = {};
308              
309 543         4312 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 4886     4886 1 232347 my $self = shift;
332 4886 100       12230 $self->parse_source() unless $self->{ 'top_node' };
333 4886         31660 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 542     542 1 202299 my $self = shift;
346              
347 542         2022 my $source = $self->source_filtered;
348              
349 542         1175 my $parser;
350              
351 542 100       6984 if ( $source =~ m{^\s*}m ) {
    100          
    100          
    100          
    100          
352              
353             # Format used by both explain command and autoexplain module
354 68         422 $self->{ 'source_format' } = 'XML';
355 68         1052 $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 80         455 $self->{ 'source_format' } = 'JSON';
361 80         1086 $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         19 $self->{ 'source_format' } = 'JSON';
367 4         80 $parser = Pg::Explain::FromJSON->new();
368             }
369             elsif ( $source =~ m{ ^ \s* - \s+ Plan: \s* \n }xms ) {
370              
371             # Format used by explain command
372 67         350 $self->{ 'source_format' } = 'YAML';
373 67         882 $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         17 $self->{ 'source_format' } = 'YAML';
379 4         56 $parser = Pg::Explain::FromYAML->new();
380             }
381             else {
382             # Format used by both explain command and autoexplain module
383 319         1258 $self->{ 'source_format' } = 'TEXT';
384 319         3507 $parser = Pg::Explain::FromText->new();
385             }
386              
387 542         2802 $parser->explain( $self );
388              
389 542         4539 $self->{ 'top_node' } = $parser->parse_source( $source );
390              
391 542         3946 $self->check_for_parallelism();
392              
393 542         2325 $self->check_for_exclusive_time_fixes();
394              
395 542         4361 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 542     542 1 1062 my $self = shift;
408 542         2217 $self->check_for_exclusive_time_fixes_cte();
409 542         1982 $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 542     542 1 1001 my $self = shift;
420              
421             # Safeguard against endless loop in some edge cases.
422 542 50       1809 return unless defined $self->{ 'top_node' };
423              
424             # There is no point in checking if the plan is not analyzed.
425 542 100       1643 return unless $self->top_node->is_analyzed;
426              
427             # Find nodes that have any ctes in them
428 443 100       1284 my @nodes_with_cte = grep { $_->ctes && 0 < scalar keys %{ $_->ctes } } ( $self->top_node, $self->top_node->all_recursive_subnodes );
  1355         3053  
  22         60  
429              
430             # For each node with cte in it...
431 443         1297 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         84 my @cte_scans = grep { $_->type eq 'CTE Scan' } ( $node, $node->all_recursive_subnodes );
  190         365  
435 22 50       99 next if 0 == scalar @cte_scans;
436              
437             # Iterate over defined ctes
438 22         69 while ( my ( $cte_name, $cte_node ) = each %{ $node->ctes } ) {
  53         156  
439              
440             # Find all CTE Scans that were scanning current CTE
441 31         66 my @matching_cte_scans = grep { $_->scan_on->{ 'cte_name' } eq $cte_name } @cte_scans;
  58         155  
442 31 50       134 next if 0 == scalar @matching_cte_scans;
443              
444             # How much time did Pg spend in given CTE itself
445 31         233 my $cte_total_time = $cte_node->total_inclusive_time;
446              
447             # How much time did all the CTE Scans used
448 31   50     71 my $total_time_of_scans = sum( map { $_->total_inclusive_time // 0 } @matching_cte_scans );
  36         137  
449              
450             # Don't fail on divide by 0, and don't warn on undef
451 31 50       110 next unless $total_time_of_scans;
452 31 100       103 next unless $cte_total_time;
453              
454             # Subtract exclusive time proportionally.
455 23         49 for my $scan ( grep { $_->total_inclusive_time } @matching_cte_scans ) {
  28         93  
456 28         91 $scan->exclusive_fix( $scan->exclusive_fix - ( $scan->total_inclusive_time / $total_time_of_scans ) * $cte_total_time );
457             }
458             }
459             }
460 443         977 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 542     542 1 2889 my $self = shift;
471              
472             # Safeguard against endless loop in some edge cases.
473 542 50       1857 return unless defined $self->{ 'top_node' };
474              
475             # There is no point in checking if the plan is not analyzed.
476 542 100       1690 return unless $self->top_node->is_analyzed;
477              
478             # Find nodes that have any init plans in them
479 443 100       2363 my @nodes_with_init = grep { $_->initplans && 0 < scalar @{ $_->initplans } } ( $self->top_node, $self->top_node->all_recursive_subnodes );
  1355         2937  
  34         96  
480              
481             # Check them all, one by one, to build "init-plan-visibility" info
482 443         1311 for my $parent ( @nodes_with_init ) {
483              
484             # Nodes that see what initplan returned even if they don't refer to returned $*
485 34         120 my @all_implicits = map { $_->id } ( $parent, $parent->all_recursive_subnodes );
  113         313  
486 34         153 my %skip_self_implicit = ();
487              
488             # Scan all initplans
489 34         86 for my $idx ( 0 .. $#{ $parent->initplans } ) {
  34         102  
490 37         117 my $initnode = $parent->initplans->[ $idx ];
491              
492             # There is no point in adjusting things for no-time.
493 37 100       134 next unless $initnode->total_inclusive_time;
494              
495             # Place to store implicit and explicit nodes
496 36         80 my @implicitnodes = ();
497 36         95 my @explicitnodes = ();
498              
499 36         65 my $explicit_re;
500              
501             # If there is metainfo, we can build regexp to find nodes explicitly using this init
502 36 100       1650 if ( $parent->initplans_metainfo->[ $idx ] ) {
503              
504             # List of $* variables that this initplan returns
505 22         56 my $returns_string = $parent->initplans_metainfo->[ $idx ]->{ 'returns' };
506 22         45 my @returns_numbers = ();
507 22         115 for my $element ( split /,/, $returns_string ) {
508 30 50       279 push @returns_numbers, $element if $element =~ s/\A\$(\d+)\z/$1/;
509             }
510 22         70 my $returns = join( '|', @returns_numbers );
511              
512             # Regular expression to check in extra-info for nodes.
513 22         826 $explicit_re = qr{\$(?:${returns})(?!\d)};
514             }
515              
516             # Add current node, and it's kids to skip list
517 36         164 for my $skip_node ( $initnode, $initnode->all_recursive_subnodes ) {
518 61         172 $skip_self_implicit{ $skip_node->id } = 1;
519             }
520              
521             # Iterate over all nodes that could have used data from this initplan
522 36         92 for my $user_id ( grep { !$skip_self_implicit{ $_ } } @all_implicits ) {
  118         299  
523              
524 54         178 my $user = $self->node( $user_id );
525              
526             # Add node to implicit ones,always
527 54         112 push @implicitnodes, $user;
528              
529             # If there is explicit_re, try to find what is using this int explicitly
530 54 100       184 next unless $explicit_re;
531 32 100       96 next unless $user->extra_info;
532 23         41 my $full_extra_info = join( "\n", @{ $user->extra_info } );
  23         64  
533 23 100       260 push @explicitnodes, $user if $full_extra_info =~ $explicit_re;
534             }
535              
536             # Total times
537 36   50     82 my $implicittime = sum( map { $_->total_exclusive_time // 0 } @implicitnodes ) // 0;
  54   50     182  
538 36   50     168 my $explicittime = sum( map { $_->total_exclusive_time // 0 } @explicitnodes ) // 0;
  18   100     57  
539              
540             # Where to adjusct exclusive time
541 36         76 my @adjust_these = ();
542 36         64 my $ratio;
543 36 100 66     219 if ( ( 0 < scalar @explicitnodes )
    50          
544             && ( $explicittime > $initnode->total_inclusive_time ) )
545             {
546 17         44 @adjust_these = @explicitnodes;
547 17         145 $ratio = $initnode->total_inclusive_time / $explicittime;
548             }
549             elsif ( $implicittime > $initnode->total_inclusive_time ) {
550 19         110 @adjust_these = @implicitnodes;
551 19         2104 $ratio = $initnode->total_inclusive_time / $implicittime;
552             }
553              
554             # Actually adjust exclusive times
555 36         93 for my $node ( @adjust_these ) {
556 48 50       128 next unless $node->total_exclusive_time;
557 48         168 my $adjust = $ratio * $node->total_exclusive_time;
558 48         162 $node->exclusive_fix( $node->exclusive_fix - $adjust );
559             }
560             }
561             }
562              
563 443         1181 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 542     542 1 1236 my $self = shift;
576              
577             # Safeguard against endless loop in some edge cases.
578 542 50       2082 return unless defined $self->{ 'top_node' };
579              
580             # There is no point in checking if the plan is not analyzed.
581 542 100       2000 return unless $self->top_node->is_analyzed;
582              
583             # @nodes will contain list of nodes to check if they are Gather
584 443         1574 my @nodes = ( [ 1, $self->top_node ] );
585              
586 443         1756 while ( my $node_info = shift @nodes ) {
587              
588 1355         2665 my $workers = $node_info->[ 0 ];
589 1355         2300 my $node = $node_info->[ 1 ];
590              
591             # Set workers.
592 1355         4401 $node->workers( $workers );
593              
594             # These sub-nodes don't get workers.
595 1355 100       3404 push @nodes, map { [ $workers, $_ ] } @{ $node->initplans } if $node->initplans;
  37         123  
  34         98  
596 1355 100       3904 push @nodes, map { [ $workers, $_ ] } @{ $node->subplans } if $node->subplans;
  29         99  
  24         165  
597 1355 100       3698 push @nodes, map { [ $workers, $_ ] } values %{ $node->ctes } if $node->ctes;
  31         119  
  22         60  
598              
599             # If there are workers launched, set it as new workers value for recursive set.
600 1355 100       3642 $workers = 1 + $node->workers_launched if defined $node->workers_launched;
601              
602             # These things get new workers
603 1355 100       3403 push @nodes, map { [ $workers, $_ ] } @{ $node->sub_nodes } if $node->sub_nodes;
  815         3290  
  576         1481  
604             }
605 443         1080 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 182     182   394 my $self = shift;
616              
617 182         1408 open my $fh, '<', $self->{ 'source_file' };
618 181         77534 local $/ = undef;
619 181         16368 my $content = <$fh>;
620 181         1395 close $fh;
621              
622 181         40733 delete $self->{ 'source_file' };
623 181         682 $self->{ 'source' } = $content;
624              
625 181         1380 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 113     113 1 2597 my $self = shift;
638              
639 113         463 my $textual = $self->top_node->as_text();
640              
641 113 100       612 if ( $self->planning_buffers ) {
642 15         34 $textual .= "Planning:\n";
643 15         62 my $buf_info = $self->planning_buffers->as_text;
644 15         100 $buf_info =~ s/^/ /gm;
645 15         39 $textual .= $buf_info . "\n";
646              
647             }
648 113 100       530 if ( $self->planning_time ) {
649 42         105 $textual .= "Planning time: " . $self->planning_time . " ms\n";
650             }
651 113 100       526 if ( $self->trigger_times ) {
652 5         7 for my $t ( @{ $self->trigger_times } ) {
  5         27  
653 13         85 $textual .= sprintf( "Trigger %s: time=%.3f calls=%d\n", $t->{ 'name' }, $t->{ 'time' }, $t->{ 'calls' } );
654             }
655             }
656 113 100       422 if ( $self->jit ) {
657 6         16 $textual .= $self->jit->as_text();
658             }
659 113 100       413 if ( $self->execution_time ) {
660 56         161 $textual .= "Execution time: " . $self->execution_time . " ms\n";
661             }
662 113 100       410 if ( $self->total_runtime ) {
663 17         69 $textual .= "Total runtime: " . $self->total_runtime . " ms\n";
664             }
665              
666 113         882 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.44',
680             'planning_buffers' => {...},
681             'execution_time' => '12.44',
682             'total_runtime' => '12.44',
683             'trigger_times' => [
684             { 'name' => ..., 'time' => ..., 'calls' => ... },
685             ...
686             ],
687             }
688              
689             =cut
690              
691             sub get_struct {
692 54     54 1 24670 my $self = shift;
693 54         207 my $reply = {};
694 54         185 $reply->{ 'top_node' } = $self->top_node->get_struct;
695 54 100       196 $reply->{ 'planning_time' } = $self->planning_time if $self->planning_time;
696 54 100       199 $reply->{ 'planning_buffers' } = $self->planning_buffers->get_struct if $self->planning_buffers;
697 54 50       161 $reply->{ 'execution_time' } = $self->execution_time if $self->execution_time;
698 54 50       214 $reply->{ 'total_runtime' } = $self->total_runtime if $self->total_runtime;
699 54 100       205 $reply->{ 'trigger_times' } = clone( $self->trigger_times ) if $self->trigger_times;
700 54 50       208 $reply->{ 'query' } = $self->query if $self->query;
701 54 50       192 $reply->{ 'settings' } = $self->settings if $self->settings;
702              
703 54 100       231 if ( $self->jit ) {
704 12         34 $reply->{ 'jit' } = {};
705 12         30 $reply->{ 'jit' }->{ 'functions' } = $self->jit->functions;
706 12         31 $reply->{ 'jit' }->{ 'options' } = clone( $self->jit->options );
707 12         37 $reply->{ 'jit' }->{ 'timings' } = clone( $self->jit->timings );
708             }
709 54         243 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 18     18 1 1770 my $self = shift;
726 18         56 my @extra_args = @_;
727              
728 18         49 my $anonymizer = $self->{ 'anonymizer' };
729 18 100       77 if ( !$anonymizer ) {
730 16         178 $anonymizer = Pg::Explain::StringAnonymizer->new();
731 16         64 $self->top_node->anonymize_gathering( $anonymizer );
732 16         120 $anonymizer->finalize();
733 16         104 $self->top_node->anonymize_substitute( $anonymizer );
734 16         76 $self->{ 'anonymizer' } = $anonymizer;
735             }
736              
737 18 100       191 return if 0 == scalar @extra_args;
738              
739 3 50       33 return $anonymizer->anonymize_text( $extra_args[ 0 ] ) if 1 == scalar @extra_args;
740              
741 0           return map { $anonymizer->anonymize_text( $_ ) } @extra_args;
  0            
742             }
743              
744             =head1 AUTHOR
745              
746             hubert depesz lubaczewski, C<< >>
747              
748             =head1 BUGS
749              
750             Please report any bugs or feature requests to C.
751              
752             =head1 SUPPORT
753              
754             You can find documentation for this module with the perldoc command.
755              
756             perldoc Pg::Explain
757              
758             =head1 COPYRIGHT & LICENSE
759              
760             Copyright 2008-2023 hubert depesz lubaczewski, all rights reserved.
761              
762             This program is free software; you can redistribute it and/or modify it
763             under the same terms as Perl itself.
764              
765              
766             =cut
767              
768             1; # End of Pg::Explain