File Coverage

blib/lib/Pg/Explain.pm
Criterion Covered Total %
statement 317 327 96.9
branch 139 164 84.7
condition 14 19 73.6
subroutine 45 45 100.0
pod 25 25 100.0
total 540 580 93.1


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 74     74   7394032 use v5.18;
  74         1251  
5 74     74   433 use strict;
  74         166  
  74         1813  
6 74     74   407 use warnings;
  74         172  
  74         2327  
7 74     74   370 use warnings qw( FATAL utf8 );
  74         191  
  74         3265  
8 74     74   45411 use utf8;
  74         1142  
  74         420  
9 74     74   35109 use open qw( :std :utf8 );
  74         91523  
  74         494  
10 74     74   53719 use Unicode::Normalize qw( NFC );
  74         162840  
  74         5887  
11 74     74   53899 use Unicode::Collate;
  74         649300  
  74         3082  
12 74     74   43300 use Encode qw( decode );
  74         775932  
  74         7868  
13              
14 74     74   548 if ( grep /\P{ASCII}/ => @ARGV ) {
  74         157  
  74         992  
15             @ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
16             }
17              
18             # UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
19              
20 74     74   1571800 use Carp;
  74         185  
  74         5114  
21 74     74   31314 use Clone qw( clone );
  74         157299  
  74         4737  
22 74     74   5360 use autodie;
  74         159341  
  74         649  
23 74     74   391548 use List::Util qw( sum );
  74         168  
  74         7322  
24 74     74   37052 use Pg::Explain::StringAnonymizer;
  74         194  
  74         2593  
25 74     74   32797 use Pg::Explain::FromText;
  74         221  
  74         3275  
26 74     74   30768 use Pg::Explain::FromYAML;
  74         188  
  74         2756  
27 74     74   31246 use Pg::Explain::FromJSON;
  74         206  
  74         2763  
28 74     74   30891 use Pg::Explain::FromXML;
  74         205  
  74         345301  
29              
30             =head1 NAME
31              
32             Pg::Explain - Object approach at reading explain analyze output
33              
34             =head1 VERSION
35              
36             Version 2.4
37              
38             =cut
39              
40             our $VERSION = '2.4';
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 53345 sub source_format { my $self = shift; $self->{ 'source_format' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'source_format' }; }
  168         500  
  168         806  
118 470 100   470 1 5686 sub planning_time { my $self = shift; $self->{ 'planning_time' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'planning_time' }; }
  470         1511  
  470         1344  
119 190 100   190 1 309 sub planning_buffers { my $self = shift; $self->{ 'planning_buffers' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'planning_buffers' }; }
  190         481  
  190         524  
120 484 100   484 1 791 sub execution_time { my $self = shift; $self->{ 'execution_time' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'execution_time' }; }
  484         1321  
  484         1295  
121 239 100   239 1 388 sub total_runtime { my $self = shift; $self->{ 'total_runtime' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'total_runtime' }; }
  239         625  
  239         674  
122 213 100   213 1 316 sub trigger_times { my $self = shift; $self->{ 'trigger_times' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'trigger_times' }; }
  213         468  
  213         726  
123 198 100   198 1 4659 sub jit { my $self = shift; $self->{ 'jit' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'jit' }; }
  198         470  
  198         592  
124 104 100   104 1 203 sub query { my $self = shift; $self->{ 'query' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'query' }; }
  104         343  
  104         257  
125 72 100   72 1 157 sub settings { my $self = shift; $self->{ 'settings' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'settings' }; }
  72         196  
  72         242  
126              
127             sub total_buffers {
128 3     3 1 12 my $self = shift;
129 3 50       9 if ( $self->top_node->buffers ) {
130 3 100       10 return $self->top_node->buffers + $self->planning_buffers if $self->planning_buffers;
131 1         6 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 46 my $self = shift;
147 26 100       56 if ( $self->trigger_times ) {
148 16         27 push @{ $self->trigger_times }, @_;
  16         32  
149             }
150             else {
151 10         40 $self->trigger_times( [ @_ ] );
152             }
153 26         73 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 80 my $self = shift;
164              
165 5   100     13 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 1575     1575 1 2462 my $self = shift;
178 1575         2462 my $id = shift;
179 1575 50       3314 return unless defined $id;
180 1575         2276 my $node = shift;
181 1575 100       5684 $self->{ 'node_by_id' }->{ $id } = $node if defined $node;
182 1575         3482 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 512     512 1 18673 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 512     512 1 832 my $self = shift;
213              
214 512         1303 my $filtered = '';
215              
216             # use default variable, to avoid having to type variable name in all regexps below
217 512         1306 for ( split /\r?\n/, $self->source ) {
218              
219             # Remove separator lines from various types of borders
220 13720 100       26278 next if /^\+-+\+\z/;
221 13678 100       31083 next if /^[-─═]+\z/;
222 13533 100       24761 next if /^(?:├|╟|╠|╞)[─═]+(?:┤|╢|╣|╡)\z/;
223              
224             # Remove more horizontal lines
225 13501 50       22055 next if /^\+-+\+\z/;
226 13501 100       21912 next if /^└─+┘\z/;
227 13485 100       21658 next if /^╚═+╝\z/;
228 13469 100       21766 next if /^┌─+┐\z/;
229 13453 100       21265 next if /^╔═+╗\z/;
230              
231             # Remove frames around, handles |, ║, │
232 13437         28469 s/^(\||║|│)(.*)\1\z/$2/;
233              
234             # Remove quotes around lines, both ' and "
235 13437         25797 s/^(["'])(.*)\1\z/$2/;
236              
237             # Remove "+" line continuations
238 13437         29591 s/\s*\+\z//;
239              
240             # Remove "↵" line continuations
241 13437         32392 s/\s*↵\z//;
242              
243             # Remove "query plan" header
244 13437 100       25490 next if /^\s*QUERY PLAN\s*\z/;
245              
246             # Remove rowcount
247 13233 100       23685 next if /^\(\d+ rows?\)\z/;
248              
249             # Accumulate filtered source
250 13034         27258 $filtered .= $_ . "\n";
251             }
252              
253 512         3115 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 516     516 1 1651785 my $class = shift;
266 516         1405 my $self = bless {}, $class;
267 516         967 my %args;
268 516 100       1775 if ( 0 == scalar @_ ) {
269 1         23 croak( 'One of (source, source_file) parameters has to be provided)' );
270             }
271 515 50       2302 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 515         1615 %args = @_;
284             }
285              
286 515 100       1710 if ( $args{ 'source_file' } ) {
    50          
287 179 100       530 croak( 'Only one of (source, source_file) parameters has to be provided)' ) if $args{ 'source' };
288 178         497 $self->{ 'source_file' } = $args{ 'source_file' };
289 178         559 $self->_read_source_from_file();
290             }
291             elsif ( $args{ 'source' } ) {
292 336 100       1607 if ( Encode::is_utf8( $args{ 'source' } ) ) {
293 47         132 $self->{ 'source' } = $args{ 'source' };
294             }
295             else {
296 289         1213 $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 513         30117 $self->{ 'jit' } = undef;
305              
306             # Initialize node_by_id hash to empty
307 513         1151 $self->{ 'node_by_id' } = {};
308              
309 513         1805 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 4594     4594 1 181626 my $self = shift;
332 4594 100       9421 $self->parse_source() unless $self->{ 'top_node' };
333 4594         12440 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 512     512 1 168096 my $self = shift;
346              
347 512         1431 my $source = $self->source_filtered;
348              
349 512         962 my $parser;
350              
351 512 100       4932 if ( $source =~ m{^\s*}m ) {
    100          
    100          
    100          
    100          
352              
353             # Format used by both explain command and autoexplain module
354 67         263 $self->{ 'source_format' } = 'XML';
355 67         705 $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         232 $self->{ 'source_format' } = 'JSON';
361 70         630 $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         56 $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         218 $self->{ 'source_format' } = 'YAML';
373 66         563 $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         15 $self->{ 'source_format' } = 'YAML';
379 4         47 $parser = Pg::Explain::FromYAML->new();
380             }
381             else {
382             # Format used by both explain command and autoexplain module
383 301         783 $self->{ 'source_format' } = 'TEXT';
384 301         2141 $parser = Pg::Explain::FromText->new();
385             }
386              
387 512         2037 $parser->explain( $self );
388              
389 512         1952 $self->{ 'top_node' } = $parser->parse_source( $source );
390              
391 512         1914 $self->check_for_parallelism();
392              
393 512         1634 $self->check_for_exclusive_time_fixes();
394              
395 512         2838 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 512     512 1 867 my $self = shift;
408 512         1471 $self->check_for_exclusive_time_fixes_cte();
409 512         1397 $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 512     512 1 817 my $self = shift;
420              
421             # Safeguard against endless loop in some edge cases.
422 512 50       1245 return unless defined $self->{ 'top_node' };
423              
424             # There is no point in checking if the plan is not analyzed.
425 512 100       1213 return unless $self->top_node->is_analyzed;
426              
427             # Find nodes that have any ctes in them
428 413 100       1252 my @nodes_with_cte = grep { $_->ctes && 0 < scalar keys %{ $_->ctes } } ( $self->top_node, $self->top_node->all_recursive_subnodes );
  1291         2727  
  22         71  
429              
430             # For each node with cte in it...
431 413         1148 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         79 my @cte_scans = grep { $_->type eq 'CTE Scan' } ( $node, $node->all_recursive_subnodes );
  190         378  
435 22 50       74 next if 0 == scalar @cte_scans;
436              
437             # Iterate over defined ctes
438 22         49 while ( my ( $cte_name, $cte_node ) = each %{ $node->ctes } ) {
  53         127  
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         124  
442 31 50       79 next if 0 == scalar @matching_cte_scans;
443              
444             # How much time did Pg spend in given CTE itself
445 31         111 my $cte_total_time = $cte_node->total_inclusive_time;
446              
447             # How much time did all the CTE Scans used
448 31   50     64 my $total_time_of_scans = sum( map { $_->total_inclusive_time // 0 } @matching_cte_scans );
  36         100  
449              
450             # Don't fail on divide by 0, and don't warn on undef
451 31 50       77 next unless $total_time_of_scans;
452 31 100       123 next unless $cte_total_time;
453              
454             # Subtract exclusive time proportionally.
455 23         54 for my $scan ( grep { $_->total_inclusive_time } @matching_cte_scans ) {
  28         69  
456 28         98 $scan->exclusive_fix( $scan->exclusive_fix - ( $scan->total_inclusive_time / $total_time_of_scans ) * $cte_total_time );
457             }
458             }
459             }
460 413         751 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 512     512 1 876 my $self = shift;
471              
472             # Safeguard against endless loop in some edge cases.
473 512 50       1354 return unless defined $self->{ 'top_node' };
474              
475             # There is no point in checking if the plan is not analyzed.
476 512 100       1086 return unless $self->top_node->is_analyzed;
477              
478             # Find nodes that have any init plans in them
479 413 100       1217 my @nodes_with_init = grep { $_->initplans && 0 < scalar @{ $_->initplans } } ( $self->top_node, $self->top_node->all_recursive_subnodes );
  1291         2593  
  34         88  
480              
481             # Check them all, one by one, to build "init-plan-visibility" info
482 413         1097 for my $parent ( @nodes_with_init ) {
483              
484             # Nodes that see what initplan returned even if they don't refer to returned $*
485 34         137 my @all_implicits = map { $_->id } ( $parent, $parent->all_recursive_subnodes );
  113         242  
486 34         81 my %skip_self_implicit = ();
487              
488             # Scan all initplans
489 34         75 for my $idx ( 0 .. $#{ $parent->initplans } ) {
  34         90  
490 37         106 my $initnode = $parent->initplans->[ $idx ];
491              
492             # There is no point in adjusting things for no-time.
493 37 100       137 next unless $initnode->total_inclusive_time;
494              
495             # Place to store implicit and explicit nodes
496 36         107 my @implicitnodes = ();
497 36         79 my @explicitnodes = ();
498              
499 36         137 my $explicit_re;
500              
501             # If there is metainfo, we can build regexp to find nodes explicitly using this init
502 36 100       171 if ( $parent->initplans_metainfo->[ $idx ] ) {
503              
504             # List of $* variables that this initplan returns
505 22         61 my $returns_string = $parent->initplans_metainfo->[ $idx ]->{ 'returns' };
506 22         52 my @returns_numbers = ();
507 22         108 for my $element ( split /,/, $returns_string ) {
508 30 50       263 push @returns_numbers, $element if $element =~ s/\A\$(\d+)\z/$1/;
509             }
510 22         97 my $returns = join( '|', @returns_numbers );
511              
512             # Regular expression to check in extra-info for nodes.
513 22         501 $explicit_re = qr{\$(?:${returns})(?!\d)};
514             }
515              
516             # Add current node, and it's kids to skip list
517 36         151 for my $skip_node ( $initnode, $initnode->all_recursive_subnodes ) {
518 61         170 $skip_self_implicit{ $skip_node->id } = 1;
519             }
520              
521             # Iterate over all nodes that could have used data from this initplan
522 36         90 for my $user_id ( grep { !$skip_self_implicit{ $_ } } @all_implicits ) {
  118         296  
523              
524 54         163 my $user = $self->node( $user_id );
525              
526             # Add node to implicit ones,always
527 54         118 push @implicitnodes, $user;
528              
529             # If there is explicit_re, try to find what is using this int explicitly
530 54 100       172 next unless $explicit_re;
531 32 100       102 next unless $user->extra_info;
532 23         49 my $full_extra_info = join( "\n", @{ $user->extra_info } );
  23         60  
533 23 100       250 push @explicitnodes, $user if $full_extra_info =~ $explicit_re;
534             }
535              
536             # Total times
537 36   50     88 my $implicittime = sum( map { $_->total_exclusive_time // 0 } @implicitnodes ) // 0;
  54   50     157  
538 36   50     152 my $explicittime = sum( map { $_->total_exclusive_time // 0 } @explicitnodes ) // 0;
  18   100     48  
539              
540             # Where to adjusct exclusive time
541 36         93 my @adjust_these = ();
542 36         73 my $ratio;
543 36 100 66     233 if ( ( 0 < scalar @explicitnodes )
    50          
544             && ( $explicittime > $initnode->total_inclusive_time ) )
545             {
546 17         51 @adjust_these = @explicitnodes;
547 17         48 $ratio = $initnode->total_inclusive_time / $explicittime;
548             }
549             elsif ( $implicittime > $initnode->total_inclusive_time ) {
550 19         42 @adjust_these = @implicitnodes;
551 19         55 $ratio = $initnode->total_inclusive_time / $implicittime;
552             }
553              
554             # Actually adjust exclusive times
555 36         129 for my $node ( @adjust_these ) {
556 48 50       137 next unless $node->total_exclusive_time;
557 48         132 my $adjust = $ratio * $node->total_exclusive_time;
558 48         149 $node->exclusive_fix( $node->exclusive_fix - $adjust );
559             }
560             }
561             }
562              
563 413         771 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 512     512 1 870 my $self = shift;
576              
577             # Safeguard against endless loop in some edge cases.
578 512 50       1493 return unless defined $self->{ 'top_node' };
579              
580             # There is no point in checking if the plan is not analyzed.
581 512 100       1387 return unless $self->top_node->is_analyzed;
582              
583             # @nodes will contain list of nodes to check if they are Gather
584 413         1110 my @nodes = ( [ 1, $self->top_node ] );
585              
586 413         1412 while ( my $node_info = shift @nodes ) {
587              
588 1291         2123 my $workers = $node_info->[ 0 ];
589 1291         1763 my $node = $node_info->[ 1 ];
590              
591             # Set workers.
592 1291         3551 $node->workers( $workers );
593              
594             # These sub-nodes don't get workers.
595 1291 100       2812 push @nodes, map { [ $workers, $_ ] } @{ $node->initplans } if $node->initplans;
  37         118  
  34         103  
596 1291 100       2971 push @nodes, map { [ $workers, $_ ] } @{ $node->subplans } if $node->subplans;
  29         83  
  24         90  
597 1291 100       2841 push @nodes, map { [ $workers, $_ ] } values %{ $node->ctes } if $node->ctes;
  31         106  
  22         70  
598              
599             # If there are workers launched, set it as new workers value for recursive set.
600 1291 100       2890 $workers = 1 + $node->workers_launched if defined $node->workers_launched;
601              
602             # These things get new workers
603 1291 100       2687 push @nodes, map { [ $workers, $_ ] } @{ $node->sub_nodes } if $node->sub_nodes;
  781         2615  
  544         1233  
604             }
605 413         844 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   304 my $self = shift;
616              
617 178         786 open my $fh, '<', $self->{ 'source_file' };
618 177         72117 local $/ = undef;
619 177         9469 my $content = <$fh>;
620 177         1174 close $fh;
621              
622 177         30030 delete $self->{ 'source_file' };
623 177         487 $self->{ 'source' } = $content;
624              
625 177         963 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 91     91 1 1928 my $self = shift;
638              
639 91         355 my $textual = $self->top_node->as_text();
640              
641 91 100       397 if ( $self->planning_buffers ) {
642 8         45 $textual .= "Planning:\n";
643 8         33 my $buf_info = $self->planning_buffers->as_text;
644 8         67 $buf_info =~ s/^/ /gm;
645 8         27 $textual .= $buf_info . "\n";
646              
647             }
648 91 100       332 if ( $self->planning_time ) {
649 25         86 $textual .= "Planning time: " . $self->planning_time . " ms\n";
650             }
651 91 100       295 if ( $self->trigger_times ) {
652 5         9 for my $t ( @{ $self->trigger_times } ) {
  5         13  
653 13         91 $textual .= sprintf( "Trigger %s: time=%.3f calls=%d\n", $t->{ 'name' }, $t->{ 'time' }, $t->{ 'calls' } );
654             }
655             }
656 91 100       328 if ( $self->jit ) {
657 4         15 $textual .= $self->jit->as_text();
658             }
659 91 100       352 if ( $self->execution_time ) {
660 34         127 $textual .= "Execution time: " . $self->execution_time . " ms\n";
661             }
662 91 100       285 if ( $self->total_runtime ) {
663 17         49 $textual .= "Total runtime: " . $self->total_runtime . " ms\n";
664             }
665              
666 91         640 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 50     50 1 22775 my $self = shift;
693 50         111 my $reply = {};
694 50         147 $reply->{ 'top_node' } = $self->top_node->get_struct;
695 50 100       161 $reply->{ 'planning_time' } = $self->planning_time if $self->planning_time;
696 50 100       162 $reply->{ 'planning_buffers' } = $self->planning_buffers->get_struct if $self->planning_buffers;
697 50 50       166 $reply->{ 'execution_time' } = $self->execution_time if $self->execution_time;
698 50 50       178 $reply->{ 'total_runtime' } = $self->total_runtime if $self->total_runtime;
699 50 100       136 $reply->{ 'trigger_times' } = clone( $self->trigger_times ) if $self->trigger_times;
700 50 50       186 $reply->{ 'query' } = $self->query if $self->query;
701 50 50       140 $reply->{ 'settings' } = $self->settings if $self->settings;
702              
703 50 100       169 if ( $self->jit ) {
704 8         27 $reply->{ 'jit' } = {};
705 8         19 $reply->{ 'jit' }->{ 'functions' } = $self->jit->functions;
706 8         16 $reply->{ 'jit' }->{ 'options' } = clone( $self->jit->options );
707 8         24 $reply->{ 'jit' }->{ 'timings' } = clone( $self->jit->timings );
708             }
709 50         199 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 17     17 1 634 my $self = shift;
726 17         46 my @extra_args = @_;
727              
728 17         49 my $anonymizer = $self->{ 'anonymizer' };
729 17 100       65 if ( !$anonymizer ) {
730 15         138 $anonymizer = Pg::Explain::StringAnonymizer->new();
731 15         47 $self->top_node->anonymize_gathering( $anonymizer );
732 15         94 $anonymizer->finalize();
733 15         78 $self->top_node->anonymize_substitute( $anonymizer );
734 15         74 $self->{ 'anonymizer' } = $anonymizer;
735             }
736              
737 17 100       158 return if 0 == scalar @extra_args;
738              
739 3 50       24 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-2021 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