1
0
mirror of https://gitlab.com/depesz/explain.depesz.com.git synced 2025-11-29 23:08:18 +02:00
Files
2025-09-01 15:37:58 +02:00

1050 lines
50 KiB
Plaintext
Executable File

% layout 'default';
% my $node_type_docs = {
% 'Append' => 'https://www.depesz.com/2013/05/19/explaining-the-unexplainable-part-4/#append',
% 'Bitmap Heap Scan' => 'https://www.depesz.com/2013/04/27/explaining-the-unexplainable-part-2/#bitmap-heap-scan',
% 'Bitmap Index Scan' => 'https://www.depesz.com/2013/04/27/explaining-the-unexplainable-part-2/#bitmap-index-scan',
% 'CTE Scan' => 'https://www.depesz.com/2013/05/19/explaining-the-unexplainable-part-4/#cte-scan',
% 'Function Scan' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#function-scan',
% 'GroupAggregate' => 'https://www.depesz.com/2013/05/19/explaining-the-unexplainable-part-4/#group-aggregate',
% 'HashAggregate' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#hash-aggregate',
% 'Hash' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#hash',
% 'Hash Join' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#hash-join',
% 'HashSetOp' => 'https://www.depesz.com/2013/05/19/explaining-the-unexplainable-part-4/#hash-set-op',
% 'Index Only Scan' => 'https://www.depesz.com/2013/04/27/explaining-the-unexplainable-part-2/#index-only-scan',
% 'Index Scan Backward' => 'https://www.depesz.com/2013/04/27/explaining-the-unexplainable-part-2/#index-scan-backward',
% 'Index Scan' => 'https://www.depesz.com/2013/04/27/explaining-the-unexplainable-part-2/#index-scan',
% 'InitPlan' => 'https://www.depesz.com/2013/05/19/explaining-the-unexplainable-part-4/#init-plan',
% 'Limit' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#limit',
% 'Materialize' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#materialize',
% 'Memoize' => 'https://www.depesz.com/2025/08/04/waiting-for-postgresql-19-display-memoize-planner-estimates-in-explain/',
% 'Merge Join' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#merge-join',
% 'Nested Loop' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#nested-loop',
% 'Result' => 'https://www.depesz.com/2013/05/19/explaining-the-unexplainable-part-4/#result',
% 'Seq Scan' => 'https://www.depesz.com/2013/04/27/explaining-the-unexplainable-part-2/#seq-scan',
% 'Sort' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#sort',
% 'SubPlan' => 'https://www.depesz.com/2013/05/19/explaining-the-unexplainable-part-4/#sub-plan',
% 'Unique' => 'https://www.depesz.com/2013/05/19/explaining-the-unexplainable-part-4/#unique',
% 'Values Scan' => 'https://www.depesz.com/2013/05/19/explaining-the-unexplainable-part-4/#values-scan',
% 'Hash Full Join' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#join-modifiers',
% 'Hash Left Join' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#join-modifiers',
% 'Hash Right Join' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#join-modifiers',
% 'Merge Full Join' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#join-modifiers',
% 'Merge Left Join' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#join-modifiers',
% 'Merge Right Join' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#join-modifiers',
% 'Nested Loop Right Join' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#join-modifiers',
% 'Nested Loop Full Join' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#join-modifiers',
% 'Nested Loop Left Join' => 'https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/#join-modifiers',
% };
% my $guc_docs = {
% 'constraint_exclusion' => 'query.html#GUC-CONSTRAINT-EXCLUSION',
% 'cpu_index_tuple_cost' => 'query.html#GUC-CPU-INDEX-TUPLE-COST',
% 'cpu_operator_cost' => 'query.html#GUC-CPU-OPERATOR-COST',
% 'cpu_tuple_cost' => 'query.html#GUC-CPU-TUPLE-COST',
% 'cursor_tuple_fraction' => 'query.html#GUC-CURSOR-TUPLE-FRACTION',
% 'effective_cache_size' => 'query.html#GUC-EFFECTIVE-CACHE-SIZE',
% 'effective_io_concurrency' => 'resource.html#GUC-EFFECTIVE-IO-CONCURRENCY',
% 'enable_bitmapscan' => 'query.html#GUC-ENABLE-BITMAPSCAN',
% 'enable_nestloop' => 'query.html#GUC-ENABLE-NESTLOOP',
% 'enable_partitionwise_aggregate' => 'query.html#GUC-ENABLE-PARTITIONWISE-AGGREGATE',
% 'enable_partitionwise_join' => 'query.html#GUC-ENABLE-PARTITIONWISE-JOIN',
% 'enable_seqscan' => 'query.html#GUC-ENABLE-SEQSCAN',
% 'force_parallel_mode' => 'developer.html#GUC-FORCE-PARALLEL-MODE',
% 'from_collapse_limit' => 'query.html#GUC-FROM-COLLAPSE-LIMIT',
% 'geqo_effort' => 'query.html#GUC-GEQO-EFFORT',
% 'geqo_threshold' => 'query.html#GUC-GEQO-THRESHOLD',
% 'hash_mem_multiplier' => 'resource.html#GUC-HASH-MEM-MULTIPLIER',
% 'jit' => 'query.html#GUC-JIT',
% 'jit_above_cost' => 'query.html#GUC-JIT-ABOVE-COST',
% 'jit_inline_above_cost' => 'query.html#GUC-JIT-INLINE-ABOVE-COST',
% 'jit_optimize_above_cost' => 'query.html#GUC-JIT-OPTIMIZE-ABOVE-COST',
% 'join_collapse_limit' => 'query.html#GUC-JOIN-COLLAPSE-LIMIT',
% 'maintenance_io_concurrency' => 'resource.html#GUC-MAINTENANCE-IO-CONCURRENCY',
% 'max_parallel_workers' => 'resource.html#GUC-MAX-PARALLEL-WORKERS',
% 'max_parallel_workers_per_gather' => 'resource.html#GUC-MAX-PARALLEL-WORKERS-PER-GATHER',
% 'min_parallel_index_scan_size' => 'query.html#GUC-MIN-PARALLEL-INDEX-SCAN-SIZE',
% 'min_parallel_table_scan_size' => 'query.html#GUC-MIN-PARALLEL-TABLE-SCAN-SIZE',
% 'parallel_setup_cost' => 'query.html#GUC-PARALLEL-SETUP-COST',
% 'parallel_tuple_cost' => 'query.html#GUC-PARALLEL-TUPLE-COST',
% 'plan_cache_mode' => 'query.html#GUC-PLAN-CACHE_MODE',
% 'random_page_cost' => 'query.html#GUC-RANDOM-PAGE-COST',
% 'search_path' => 'client.html#GUC-SEARCH-PATH',
% 'seq_page_cost' => 'query.html#GUC-SEQ-PAGE-COST',
% 'temp_buffers' => 'resource.html#GUC-TEMP-BUFFERS',
% 'work_mem' => 'resource.html#GUC-WORK-MEM',
% };
% my $id = stash( 'id' );
% my $title = stash( 'title' );
% my $full_title = $title ? "$id : $title" : $id;
% title $full_title;
%# c = e|i|x|m (colorize = exclusive|inclusive|rows-x|mixed)
%# ve - visibility of "exclusive" column
%# vi - visibility of "inclusive" column
%# ...
% my @cookie = grep { $_->name eq 'explain' } @{ $self->req->cookies };
% my $cookie = scalar @cookie ? Mojo::ByteStream->new( $cookie[0]->value )->url_unescape : '';
% my @cfg = split /\|/, $cookie || '';
% my $cfg = {};
%
% for ( @cfg ) {
% next unless $_ =~ /\A(c|vu|ve|vi|vx|vr|vl)\=([exim01]{1})\z/;
% $cfg->{ $1 } = $2;
% }
%
% $cfg->{ c } = 'm' unless $cfg->{ c } && $cfg->{ c } =~ /\A(e|i|x|m)\z/;
%
% for ( qw( vu ve vi vx vr vl ) ) {
% next if exists $cfg->{ $_ } && $cfg->{ $_ } =~ /\A(0|1)\z/;
% $cfg->{ $_ } = 1;
% }
% my $buffers = $explain->total_buffers ? 1 : 0;
% my $buf_r = 0;
% my $buf_w = 0;
% if ( $buffers ) {
% my $BD = $explain->total_buffers->data;
% $buf_r = ( $BD->{'shared'}->{'read'} // 0 ) + ( $BD->{'local'}->{'read'} // 0 ) + ( $BD->{'temp'}->{'read'} // 0 );
% $buf_w = ( $BD->{'shared'}->{'written'} // 0 ) + ( $BD->{'local'}->{'written'} // 0 ) + ( $BD->{'temp'}->{'written'} // 0 );
% }
% my $global_node_id = 0;
% my $table_node_id_to_explain_node_id = {};
% my $explain_node_id_to_table_node_id = {};
% my $prev_row_level = 0;
% my $prev_row_class = 'odd';
% my $block = undef;
%
% $block = begin
%
% my ( $node, $level, $parent ) = @_;
% my $full_execution_time = $explain->runtime || 0;
%
% $level ||= 0;
% $parent = '' unless defined $parent;
%
% my $exclusive_point = 1;
% my $inclusive_point = 1;
% my $rows_point = 1;
%
% if ( $explain->top_node->total_inclusive_time && defined( $node->total_exclusive_time ) && defined( $node->total_inclusive_time ) && $node->total_exclusive_time ne '' && $node->total_inclusive_time ne '' ) {
%
% $exclusive_point = $node->total_exclusive_time / $full_execution_time;
%
% if ( $exclusive_point > 0.9 ) { $exclusive_point = 4; }
% elsif ( $exclusive_point > 0.5 ) { $exclusive_point = 3; }
% elsif ( $exclusive_point > 0.1 ) { $exclusive_point = 2; }
% else { $exclusive_point = 1; }
%
% $inclusive_point = $node->total_inclusive_time / $full_execution_time;
%
% if ( $inclusive_point > 0.9 ) { $inclusive_point = 4; }
% elsif ( $inclusive_point > 0.5 ) { $inclusive_point = 3; }
% elsif ( $inclusive_point > 0.1 ) { $inclusive_point = 2; }
% else { $inclusive_point = 1; }
%
% if ( defined $node->total_rows && defined $node->total_rows_removed && 0 < $node->total_rows + $node->total_rows_removed ) {
% my $rows_removed_frac = $node->total_rows_removed / ( $node->total_rows_removed + $node->total_rows );
% if ( $rows_removed_frac > 0.9 ) { $rows_point = 4; }
% elsif ( $rows_removed_frac > 0.5 ) { $rows_point = 3; }
% elsif ( $rows_removed_frac > 0.1 ) { $rows_point = 2; }
% else { $rows_point = 1; }
%
% }
% }
%
% my $rows_x = 0;
% my $rows_x_mark = '';
% my $rows_x_point = 1;
%
% if ( ( $node->estimated_rows > 0 ) && ( ( $node->actual_rows // 0 ) > 0 ) ) {
%
% if ( $node->actual_rows > $node->estimated_rows ) {
% $rows_x = $node->actual_rows / $node->estimated_rows;
% $rows_x_mark = 'down';
% } else {
% $rows_x = $node->estimated_rows / $node->actual_rows;
% $rows_x_mark = 'up';
% }
%
% if ( $rows_x > 1000 ) { $rows_x_point = 4; }
% elsif ( $rows_x > 100 ) { $rows_x_point = 3; }
% elsif ( $rows_x > 10 ) { $rows_x_point = 2; }
% else { $rows_x_point = 1; }
% }
%# tr class="(n|ip|sp) (even|odd) c-(mix|1|2|3|4)"
%# means:
%# n(ode)/i(nit)p(lan)/s(ub)p(lan)
%# c(olor)-mix(ed)|1|2|3|4
%#
%# td class="(u|e|i|x|r|l|n) (c-(1|2|3|4))?"
%# means:
%# n(u)mber|e(xclusive time)/i(nclusive time)/(rows-)x/r(ows)/l(oops)/n(ode)
%# c(olor)-1|2|3|4
%#
%# important!
%# content of td.r(ows)/td.l(oops) must be wrapped in "span" element (because Opera...)
% my $margin = ( $level + 1 ) * 22;
% my $row_color = $cfg->{ c };
% if ( $row_color eq 'e' ) { $row_color = $exclusive_point; }
% elsif ( $row_color eq 'i' ) { $row_color = $inclusive_point; }
% elsif ( $row_color eq 'x' ) { $row_color = $rows_x_point; }
% my $row_class = $prev_row_class;
%
% if ( $level != $prev_row_level ) {
%
% $row_class = $prev_row_class eq 'even' ? 'odd' : 'even';
%
% $prev_row_level = $level;
% $prev_row_class = $row_class;
% }
%
% $row_class .= ' c-' . $row_color;
% my $node_id = $global_node_id++;
% $table_node_id_to_explain_node_id->{$global_node_id} = $node->id;
% $explain_node_id_to_table_node_id->{$node->id} = $global_node_id;
<tr id="l<%= $global_node_id =%>" class="n <%= $row_class %>" data-node_id="<%= $node_id =%>" data-node_parent="<%= $parent =%>" data-level="<%= $level =%>" data-e="<%= $exclusive_point =%>" data-i="<%= $inclusive_point =%>" data-x="<%= $rows_x_point =%>">
<td class="u <%= $cfg->{ vu } ? '' : ' tight' %>"><a href="#l<%= $global_node_id =%>"><%= $global_node_id =%>.</a></td>
<td class="e c-<%= $exclusive_point =%><%= $cfg->{ ve } ? '' : ' tight' %>">
<span><%= commify_number( sprintf '%.3f', defined $node->total_exclusive_time ? $node->total_exclusive_time : 0 ) =%></span>
</td>
<td class="i c-<%= $inclusive_point =%><%= $cfg->{ vi } ? '' : ' tight' %>">
<span><%= commify_number( sprintf '%.3f', defined $node->total_inclusive_time ? $node->total_inclusive_time : 0 ) =%></span>
</td>
<td class="x c-<%= $rows_x_point =%><%= $cfg->{ vx } ? '' : ' tight' %>">
<span>
<%== $rows_x_mark eq 'up' ? '&uarr;' : '&darr;' %>
<%= commify_number( sprintf '%.1f', $rows_x ) %>
</span>
</td>
<td class="r<%= $cfg->{ vr } ? '' : ' tight' %> c-<%= $rows_point %>"><span>
<%= commify_number( $node->total_rows ) =%></span>
% if ( $node->total_rows_removed > 1 ) {
<br/>
- <%= commify_number( $node->total_rows_removed ) %>
% }
</td>
<td class="l<%= $cfg->{ vl } ? '' : ' tight' %>"><span>
<%= commify_number( $node->actual_loops ) =%>
% if ( $node->workers > 1 ) {
<br/>
/ <%= $node->workers %>
% }
</span>
</td>
% if ( $buffers ) {
% my $eb = $node->total_exclusive_buffers;
% if ($eb) {
% my $BD = $eb->data;
% if ( $buf_r ) {
% my $v = ( $BD->{'shared'}->{'read'} // 0 ) + ( $BD->{'local'}->{'read'} // 0 ) + ( $BD->{'temp'}->{'read'} // 0 );
% $v = 0 unless $v;
% my $c = $buf_r == 0 ? 1 :
% $v > $buf_r * 0.9 ? 4 :
% $v > $buf_r * 0.5 ? 3 :
% $v > $buf_r * 0.1 ? 2 :
% 1;
<td class="bufr c-<%= $c %>">
<%= humanize_size( 8192 * $v ) %>
% if ( $BD->{'timings'}->{'read'} ) {
<br>in&nbsp;<%= commify_number( $BD->{'timings'}->{'read'} ) %>&nbsp;ms
<br>~&nbsp;<%= humanize_size( 8192 * $v * 1000 / $BD->{'timings'}->{'read'} ) %>/s
% }
</td>
% }
% if ( $buf_w ) {
% my $v = ( $BD->{'shared'}->{'written'} // 0 ) + ( $BD->{'local'}->{'written'} // 0 ) + ( $BD->{'temp'}->{'written'} // 0 );
% $v = 0 unless $v;
% my $c = $buf_w == 0 ? 1 :
% $v > $buf_w * 0.9 ? 4 :
% $v > $buf_w * 0.5 ? 3 :
% $v > $buf_w * 0.1 ? 2 :
% 1;
<td class="bufw c-<%= $c %>">
<%= humanize_size( 8192 * $v ) %>
% if ( $BD->{'timings'}->{'write'} ) {
<br>in&nbsp;<%= commify_number( $BD->{'timings'}->{'write'} ) %>&nbsp;ms
<br>~&nbsp;<%= humanize_size( 8192 * $v * 1000 / $BD->{'timings'}->{'write'} ) %>/s
% }
</td>
% }
% } else {
% if ( $buf_r ) {
<td class="bufr">0</td>
% }
% if ( $buf_w ) {
<td class="bufw">0</td>
% }
% }
% }
<td class="n">
<div class="n" style="margin-left:<%= $margin =%>px">
<div class="ico">&rarr;</div>
<p>
<span class="node">
% if ( $node_type_docs->{ $node->type }) {
<a href="<%= $node_type_docs->{ $node->type } %>"><%= $node->type %></a>
% } else {
<%= $node->type %>
% }
% if ( $node->type =~ m{^(Parallel )?Bitmap Heap Scan$} ) {
on <%= $node->scan_on->{ table_name } %> <%= $node->scan_on->{ table_alias } || '' %>
% }
% elsif ( 'Bitmap Index Scan' eq $node->type ) {
on <%= $node->scan_on->{ index_name } %>
% }
% elsif ( $node->type =~ m{^(Parallel )?Index (Only )?Scan( Backward)?$} ) {
using <%= $node->scan_on->{ index_name } %> on <%= $node->scan_on->{ table_name } %> <%= $node->scan_on->{ table_alias } || '' %>
% }
% elsif ( $node->type =~ m{^(Parallel )?Seq Scan$} ) {
on <%= $node->scan_on->{ table_name } %> <%= $node->scan_on->{ table_alias } || '' %>
% }
% elsif ( $node->type =~ m{^(Parallel )?Tid Scan$} ) {
on <%= $node->scan_on->{ table_name } %> <%= $node->scan_on->{ table_alias } || '' %>
% }
% elsif ( ( 'Insert' eq $node->type ) && ( $node->scan_on ) ) {
on <%= $node->scan_on->{ table_name } %> <%= $node->scan_on->{ table_alias } || '' %>
% }
% elsif ( ( 'Update' eq $node->type ) && ( $node->scan_on ) ) {
on <%= $node->scan_on->{ table_name } %> <%= $node->scan_on->{ table_alias } || '' %>
% }
% elsif ( ( 'Delete' eq $node->type ) && ( $node->scan_on ) ) {
on <%= $node->scan_on->{ table_name } %> <%= $node->scan_on->{ table_alias } || '' %>
% }
% elsif ( 'Foreign Scan' eq $node->type ) {
% if ( defined $node->scan_on ) {
on <%= $node->scan_on->{ table_name } %> <%= $node->scan_on->{ table_alias } || '' %>
% }
% }
% elsif ( 'CTE Scan' eq $node->type ) {
on <%= $node->scan_on->{ cte_name } %> <%= $node->scan_on->{ cte_alias } || '' %>
% }
% elsif ( 'WorkTable Scan' eq $node->type ) {
on <%= $node->scan_on->{ worktable_name } %> <%= $node->scan_on->{ worktable_alias } || '' %>
% }
% elsif ( 'Function Scan' eq $node->type ) {
on <%= $node->scan_on->{ function_name } %> <%= $node->scan_on->{ function_alias } || '' %>
% }
% elsif ( 'Subquery Scan' eq $node->type ) {
on <%= $node->scan_on->{ subquery_name } %>
% }
</span>
<span class="est">
(cost=<%= commify_number( $node->estimated_startup_cost ) =%>..<%= commify_number( $node->estimated_total_cost ) %>
rows=<%= commify_number( $node->estimated_rows ) %>
width=<%= commify_number( $node->estimated_row_width ) =%>)
</span>
% if ( $node->is_analyzed ) {
<span class="act">
% if ( $node->never_executed ) {
(never executed)
% } else {
(actual
% if ( defined $node->actual_time_first ) {
time=<%= commify_number( $node->actual_time_first ) =%>..<%= commify_number( $node->actual_time_last ) %>
% }
rows=<%= commify_number( $node->actual_rows ) %>
loops=<%= commify_number( $node->actual_loops ) =%>)
% }
</span>
% }
</p>
% if ( ( $node->extra_info ) || ( $node->buffers ) ) {
<ul class="ex-nfo">
% if ( $node->extra_info ) {
% for my $line ( @{ $node->extra_info } ) {
<li><%= commify_numbers_inside( $line ) =%></li>
% }
% }
% if ( $node->buffers ) {
% for my $line ( split /\n/, $node->buffers->as_text ) {
<li><%= commify_numbers_inside( $line ) =%></li>
% }
% }
</ul>
% }
</div>
</td>
</tr>
% if ( $node->can( 'ctes' ) ) {
% if ( $node->ctes ) {
% for my $cte ( @{ $node->cte_order } ) {
% my $cte_node_id = $global_node_id++;
<tr id="l<%= $global_node_id =%>" class="cte" data-node_id="<%= $cte_node_id =%>" data-node_parent="<%= $node_id =%>" data-level="<%= $level =%>">
<td class="u<%= $cfg->{ vu } ? '' : ' tight' %>"><span><a href="#l<%= $global_node_id =%>"><%= $global_node_id =%>.</a></span></td>
<td class="e<%= $cfg->{ ve } ? '' : ' tight' %>"><span>&nbsp;</span></td>
<td class="i<%= $cfg->{ vi } ? '' : ' tight' %>"><span>&nbsp;</span></td>
<td class="x<%= $cfg->{ vx } ? '' : ' tight' %>"><span>&nbsp;</span></td>
<td class="r<%= $cfg->{ vr } ? '' : ' tight' %>"><span>&nbsp;</span></td>
<td class="l<%= $cfg->{ vl } ? '' : ' tight' %>"><span>&nbsp;</span></td>
% if ( $buf_r ) {
<td class="bufr"></td>
% }
% if ( $buf_w ) {
<td class="bufw"></td>
% }
<td class="n">
<div class="n" style="margin-left:<%= $margin =%>px">
<p><span>CTE <%= $cte %></span></p>
</div>
</td>
</tr>
%== $block->( $node->cte( $cte ), $level + 1, $cte_node_id );
% }
% }
% }
% if ( $node->initplans ) {
% for my $idx ( 0 .. $#{ $node->initplans } ) {
% my $initnode = $node->initplans->[$idx];
% my $meta = $node->initplans_metainfo->[$idx] if $node->initplans_metainfo;
% my $ip_node_id = $global_node_id++;
<tr id="l<%= $global_node_id =%>" class="ip" data-node_id="<%= $ip_node_id =%>" data-node_parent="<%= $node_id =%>" data-level="<%= $level =%>">
<td class="u<%= $cfg->{ vu } ? '' : ' tight' %>"><span><a href="#l<%= $global_node_id =%>"><%= $global_node_id =%>.</a></span></td>
<td class="e<%= $cfg->{ ve } ? '' : ' tight' %>"><span>&nbsp;</span></td>
<td class="i<%= $cfg->{ vi } ? '' : ' tight' %>"><span>&nbsp;</span></td>
<td class="x<%= $cfg->{ vx } ? '' : ' tight' %>"><span>&nbsp;</span></td>
<td class="r<%= $cfg->{ vr } ? '' : ' tight' %>"><span>&nbsp;</span></td>
<td class="l<%= $cfg->{ vl } ? '' : ' tight' %>"><span>&nbsp;</span></td>
% if ( $buf_r ) {
<td class="bufr"></td>
% }
% if ( $buf_w ) {
<td class="bufw"></td>
% }
<td class="n">
<div class="n" style="margin-left:<%= $margin =%>px">
% if ($meta) {
<p><span>InitPlan <%= $meta->{'name'} %> (for <%= $node->type %>) (returns <%= $meta->{'returns'} %>)</span></p>
% } else {
<p><span>InitPlan (for <%= $node->type %>)</span></p>
% }
</div>
</td>
</tr>
%== $block->( $initnode, $level + 1, $ip_node_id );
% }
% }
% if ( $node->sub_nodes ) {
% for ( @{ $node->sub_nodes } ) {
%== $block->( $_, $level + 1, $node_id );
% }
% }
% if ( $node->subplans ) {
% my $sp_node_id = $global_node_id++;
<tr id="l<%= $global_node_id =%>" class="sp" data-node_id="<%= $sp_node_id =%>" data-node_parent="<%= $node_id =%>" data-level="<%= $level =%>">
<td class="u<%= $cfg->{ vu } ? '' : ' tight' %>"><span><a href="#l<%= $global_node_id =%>"><%= $global_node_id =%>.</a></span></td>
<td class="e<%= $cfg->{ ve } ? '' : ' tight' %>"><span>&nbsp;</span></td>
<td class="i<%= $cfg->{ vi } ? '' : ' tight' %>"><span>&nbsp;</span></td>
<td class="x<%= $cfg->{ vx } ? '' : ' tight' %>"><span>&nbsp;</span></td>
<td class="r<%= $cfg->{ vr } ? '' : ' tight' %>"><span>&nbsp;</span></td>
<td class="l<%= $cfg->{ vl } ? '' : ' tight' %>"><span>&nbsp;</span></td>
% if ( $buf_r ) {
<td class="bufr"></td>
% }
% if ( $buf_w ) {
<td class="bufw"></td>
% }
<td class="n">
<div class="n" style="margin-left:<%= $margin =%>px">
<p><span>SubPlan (for <%= $node->type %>)</span></p>
</div>
</td>
</tr>
% for ( @{ $node->subplans } ) {
%== $block->( $_, $level + 1, $sp_node_id );
% }
% }
% end;
<h1>Result: <%= $full_title %></h1>
% if ( flash( 'delete_key' ) ) {
<div class="message messageNice">
<p class="message">To delete this plan, you can use <a href="<%= url_for( 'delete', id => $id, key => flash( 'delete_key' ) )=%>">this link</a>.</p>
<p class="hint">This link will not be shown any more, so you might want to bookmark it, just in case.</p>
</div>
% }
<div class="explain-form">
<form id="explain-form" class="hidden" method="get" action="<%= url_for 'current' %>" autocomplete="off">
<h3>Color mode:</h3>
<ul>
<li>
<input type="radio" name="c" id="ce" value="e"<%= $cfg->{ c } eq 'e' ? ' checked="checked"' : '' %> />
<label for="ce">exclusive</label>
</li>
<li>
<input type="radio" name="c" id="ci" value="i"<%= $cfg->{ c } eq 'i' ? ' checked="checked"' : '' %> />
<label for="ci">inclusive</label>
</li>
<li>
<input type="radio" name="c" id="cx" value="x"<%= $cfg->{ c } eq 'x' ? ' checked="checked"' : '' %> />
<label for="cx">rows x</label>
</li>
<li>
<input type="radio" name="c" id="cm" value="m"<%= $cfg->{ c } eq 'm' ? ' checked="checked"' : '' %> />
<label for="cm">mixed</label>
</li>
</ul>
<h3>Visible columns:</h3>
<ul>
<li>
<input type="checkbox" name="vu" id="vu" value="1"<%= $cfg->{ vu } ? ' checked="checked"' : '' %> />
<label for="vu">#</label>
</li>
<li>
<input type="checkbox" name="ve" id="ve" value="1"<%= $cfg->{ ve } ? ' checked="checked"' : '' %> />
<label for="ve">exclusive</label>
</li>
<li>
<input type="checkbox" name="vi" id="vi" value="1"<%= $cfg->{ vi } ? ' checked="checked"' : '' %> />
<label for="vi">inclusive</label>
</li>
<li>
<input type="checkbox" name="vx" id="vx" value="1"<%= $cfg->{ vx } ? ' checked="checked"' : '' %> />
<label for="vx">rows x</label>
</li>
<li>
<input type="checkbox" name="vr" id="vr" value="1"<%= $cfg->{ vr } ? ' checked="checked"' : '' %> />
<label for="vr">rows</label>
</li>
<li>
<input type="checkbox" name="vl" id="vl" value="1"<%= $cfg->{ vl } ? ' checked="checked"' : '' %> />
<label for="vl">loops</label>
</li>
</ul>
<div class="fe fe-buttons">
<button type="submit" name="save-settings" id="save-settings"><span>Save settings</span></button>
</div>
</form>
<a href="#" onclick="$.fn.explain( 'toggleCfgForm', this ); return false;" onkeypress="return this.onclick( );"><span>Settings</span></a>
</div>
% if ( stash('optimization_path') ) {
<h3>Optimization path:</h3>
<ul>
% for my $opt ( @{ stash('optimization_path') }) {
<li><a href="<%= url_for( 'show', id => $opt->{'id'} ) =%>">#<%= $opt->{'id'} %> : <%= $opt->{'title'} %></a></li>
% }
</ul>
</h3>
% }
% if ( stash('suboptimizations') ) {
<h3>Optimization(s) for this plan:</h3>
<ul>
% for my $opt ( @{ stash('suboptimizations') }) {
<li><a href="<%= url_for( 'show', id => $opt->{'id'} ) =%>">#<%= $opt->{'id'} %> : <%= $opt->{'title'} %></a></li>
% }
</ul>
% }
<div class="result">
<div class="plea">
<form id="new-optimization" method="post" action="<%= url_for 'new-optimization' %>">
<input type="hidden" name="original" value="<%= $id %>"/>
<button type="submit" name="add-optimization" id="add-optimization"><span>Add optimization</span></button>
</form>
</div>
<div class="tabs">
<ul class="clearfix">
<li class="html">
<a href="#html"
title="view HTML"
class="current"
onclick="console.log('this is', this); $( this ).explain( 'toggleView', 'html', this ); return false;"
onkeypress="return this.onclick( );">HTML</a>
</li>
<li class="source">
<a href="#source"
title="view source explain"
onclick="$( this ).explain( 'toggleView', 'source', this ); return false;"
onkeypress="return this.onclick( );">SOURCE</a>
</li>
% if ( $explain->source_format ne 'TEXT' ) {
<li class="text">
<a href="#text"
title="view as text explain"
onclick="$( this ).explain( 'toggleView', 'text', this ); return false;"
onkeypress="return this.onclick( );">TEXT</a>
</li>
% }
% if ( stash('hinter') ) {
<li class="hints">
<a href="#hints"
title="view plan hints"
onclick="$( this ).explain( 'toggleView', 'hints', this ); return false;"
onkeypress="return this.onclick( );">HINTS</a>
</li>
% }
% if ( stash('query') ) {
<li class="query">
<a href="#query"
title="view query"
onclick="$( this ).explain( 'toggleView', 'query', this ); return false;"
onkeypress="return this.onclick( );">QUERY</a>
</li>
% }
% if ( stash('bquery') ) {
<li class="bquery">
<a href="#bquery"
title="view reformatted query"
onclick="$( this ).explain( 'toggleView', 'bquery', this ); return false;"
onkeypress="return this.onclick( );">REFORMATTED QUERY</a>
</li>
% }
% if ( stash('comments') ) {
<li class="comments">
<a href="#comments"
title="view reformatted query"
onclick="$( this ).explain( 'toggleView', 'comments', this ); return false;"
onkeypress="return this.onclick( );">COMMENTS</a>
</li>
% }
<li class="stats">
<a href="#stats"
title="view plan stats"
onclick="$( this ).explain( 'toggleView', 'stats', this ); return false;"
onkeypress="return this.onclick( );">STATS</a>
</li>
</ul>
</div>
<div class="result-html res-tab">
<table id="explain">
<thead>
<tr>
<th class="u<%= $cfg->{ vu } ? '' : ' tight' %>">
<a href="<%= url_for 'help' %>#col-no">#</a>
</th>
<th class="e<%= $cfg->{ ve } ? '' : ' tight' %>">
<a href="<%= url_for 'help' %>#col-exclusive">exclusive</a>
</th>
<th class="i<%= $cfg->{ vi } ? '' : ' tight' %>">
<a href="<%= url_for 'help' %>#col-inclusive">inclusive</a>
</th>
<th class="x<%= $cfg->{ vx } ? '' : ' tight' %>">
<a href="<%= url_for 'help' %>#col-rows-x">rows x</a>
</th>
<th class="r<%= $cfg->{ vr } ? '' : ' tight' %>">
<a href="<%= url_for 'help' %>#col-rows">rows</a>
</th>
<th class="l<%= $cfg->{ vl } ? '' : ' tight' %>">
<a href="<%= url_for 'help' %>#col-loops">loops</a>
</th>
% if ( $buf_r ) {
<th>
<a href="<%= url_for 'help' %>#col-read">read</a>
</th>
% }
% if ( $buf_w ) {
<th>
<a href="<%= url_for 'help' %>#col-written">written</a>
</th>
% }
<th class="n">
<a href="<%= url_for 'help' %>#col-node">node</a>
</th>
</tr>
</thead>
<tbody>
%== $block->( $explain->top_node );
</tbody>
</table>
% if ( (defined $explain->planning_time) || (defined $explain->execution_time) || (defined $explain->total_runtime) ) {
% my $trclass="single";
% $trclass="both" if (defined $explain->planning_time) && (defined $explain->execution_time);
<table id="total-times">
% if (defined $explain->planning_buffers) {
<tr class="<%= $trclass %> planningbuffers">
<th>Planning I/O</th>
<td class="separator">:</td>
<td class="value">
% for my $line ( split /\n/, $explain->planning_buffers->as_text ) {
<%= commify_numbers_inside( $line ) =%><br>
% }
</td>
</tr>
% }
% if (defined $explain->planning_time) {
<tr class="<%= $trclass %> planning">
<th>Planning time</th>
<td class="separator">:</td>
<td class="value"><%= commify_number( $explain->planning_time ) %> ms</td>
</tr>
% }
% if (defined $explain->execution_time) {
<tr class="<%= $trclass %> execution">
<th>Execution time</th>
<td class="separator">:</td>
<td><span class="value"><%= commify_number( $explain->execution_time ) %></span> <span class="unit">ms</span></td>
</tr>
% }
% if (defined $explain->total_runtime) {
<tr class="<%= $trclass %> execution">
<th>Total runtime</th>
<td class="separator">:</td>
<td><span class="value"><%= commify_number( $explain->total_runtime ) %></span> <span class="unit">ms</span></td>
</tr>
% }
</table>
% }
% if ( $explain->settings ) {
<table id="plan-settings">
<thead>
<tr><th colspan="3">Custom settings:</th></tr>
</thead>
<tbody>
% for my $name ( sort keys %{ $explain->settings }) {
<tr>
<th>
<%= $name %>
% if ( $guc_docs->{ $name }) {
<a href="https://www.postgresql.org/docs/current/runtime-config-<%= $guc_docs->{ $name } %>" target='_new'>?</a>
% }
</th>
<td><%= $explain->settings->{ $name } %></td>
</tr>
% }
</tbody>
</table>
% }
% if (defined $explain->jit) {
% my $full_execution_time = $explain->runtime || 0;
<table id="jit-info">
<thead>
<tr><th colspan="2" class="top" title="Just-in-Time Compilation">JIT:<a href="https://www.postgresql.org/docs/current/jit.html" target='_new'>?</a></th></tr>
</thead>
<tbody>
<tr class="jit-fun"><th>Functions:</th><td><%= $explain->jit->functions %></td></tr>
<tr><th colspan="2">Options:</th></tr>
% my $opts = $explain->jit->options;
% for my $name ( sort keys %{ $opts } ) {
<tr class="jit-option">
<th><%= $name %></th>
<td>
% if ( $opts->{$name} ) {
<span>&check;</span> (true)
% } else {
<span>&cross;</span> (false)
% }
</td>
</tr>
% }
<tr><th colspan="2">Timing:</th></tr>
% my $ts = $explain->jit->timings;
% for my $name ( sort keys %{ $ts } ) {
% my $val = $ts->{$name};
% my $row_color = 1;
% my $total_time = '' eq ref($val) ? $val : $val->{'Total'};
% if ( $full_execution_time ) {
% my $t_point = $total_time / $full_execution_time;
% if ( $t_point > 0.9 ) { $row_color = 4; }
% elsif ( $t_point > 0.5 ) { $row_color = 3; }
% elsif ( $t_point > 0.1 ) { $row_color = 2; }
% }
<tr class="jit-timing c-<%= $row_color %>"><th><%= $name %></th>
<td><%= commify_number( $total_time ) %> ms
% if ( 'HASH' eq ref( $val ) ) {
<ul class="jit-details">
% for my $subname ( sort grep { $_ ne 'Total' } keys %{ $val } ) {
<li><%= $subname %> <%= commify_number( $val->{$subname} ) %> ms</li>
% }
</ul>
% }
</td></tr>
% }
</tbody>
</table>
% }
% if ( defined $explain->trigger_times ) {
% my $full_execution_time = $explain->runtime || 0;
<table id="trigger-times">
<thead>
<tr><th colspan="4" class="top">Trigger times:</th></tr>
<tr><th>Trigger Name:</th><th>Total time:</th><th>Calls:</th><th>Average time:</th></tr>
</thead>
<tbody>
% for my $trg ( @{ $explain->trigger_times } ) {
% my $row_color = 1;
% if ( $full_execution_time ) {
% my $trigger_point = $trg->{'time'} / $full_execution_time;
% if ( $trigger_point > 0.9 ) { $row_color = 4; }
% elsif ( $trigger_point > 0.5 ) { $row_color = 3; }
% elsif ( $trigger_point > 0.1 ) { $row_color = 2; }
% }
<tr class="c-<%= $row_color %>">
<th class="name"><%= $trg->{'name'} %></th>
<td class="time"><span class="value"><%= commify_number( sprintf '%.03f', $trg->{'time'} ) %></span> <span class="unit">ms</span></td>
<td class="count"><span class="value"><%= commify_number( $trg->{'calls'} ) %></span></td>
<td class="time"><span class="value"><%= commify_number( sprintf '%.03f', $trg->{'time'} / $trg->{'calls'} ) %></span> <span class="unit">ms</span></td>
</tr>
% }
</tbody>
</table>
% }
</div>
<div class="result-source res-tab hidden">
<pre id="source"><code class="<%= lc( $explain->source_format ) =%>"><%= $explain->source =%></code></pre>
<button class="copy">Copy source to clipboard</button>
</div>
% if ( $explain->source_format ne 'TEXT' ) {
<div class="result-text res-tab hidden">
<pre id="text"><code><%= $explain->as_text =%></code></pre>
<button class="copy">Copy text to clipboard</button>
</div>
% }
% if ( stash('hinter') ) {
% my $H = stash('hinter')->hints;
<div class="result-hints res-tab hidden">
% if ( 1 == scalar @{ $H } ) {
<h1>I have one hint for you:</h1>
% } else {
<h1>I have <%= scalar @{ $H } %> hints for you:</h1>
% }
<ol>
% for my $hint ( @{ $H }) {
% my $node_num = $explain_node_id_to_table_node_id->{ $hint->node->id };
<li>
% if ( $hint->type eq 'DISK_SORT' ) {
<p>You have <a href="#l<%= $node_num %>" target="_new">sort node (#<%= $node_num %>)</a> that is using disk space to sort.</p>
<p>This is because your <a href="https://www.postgresql.org/docs/current/runtime-config-<%= $guc_docs->{ 'work_mem' } %>" target='_new'>work_mem</a> setting is too low.</p>
<p>Increasing it can make the sort run in memory, or, at least, use less of disk. Sort used <%= $hint->details->[0] %>kB, so you would need to set your work_mem to <em>at least</em> that much to have a chance at sorting in memory only.</p>
% } elsif ( $hint->type eq 'INDEXABLE_SEQSCAN_SIMPLE' ) {
% my $table_name = $hint->node->scan_on->{'table_name'};
% my $column_name = $hint->details->[0];
% my $operator = $hint->details->[1];
% my $fetched_rows = $hint->node->total_rows + $hint->node->total_rows_removed;
% my $dropped_rows = $hint->node->total_rows_removed;
<p>You have <a href="#l<%= $node_num %>" target="_new"><%= $hint->node->type %> (#<%= $node_num %>)</a> that could use an index.</p>
<p>This node searches in table <em><%= $table_name %></em> using <em><%= $operator %></em> operator on column <em><%= $column_name %></em>. In process it fetches from disk <%= commify_number( $fetched_rows ) %> rows, just to drop
% if ( $fetched_rows == $dropped_rows ) {
<em>all</em>
% } else {
<%= commify_number( $dropped_rows ) %>
% }
of them!</p>
<p>This should be possible to speed up by creating this index:</p>
<pre><code class="sql">CREATE INDEX CONCURRENTLY <%= db_ident_quote( "explain_depesz_com_hint_${id}_${node_num}" ) %> ON <%= db_ident_quote($table_name) %> ( <%= db_ident_quote($column_name) %> );</code></pre>
% } elsif ( $hint->type eq 'INDEXABLE_SEQSCAN_MULTI_EQUAL_AND') {
% my $table_name = $hint->node->scan_on->{'table_name'};
% my @columns = map { $_->{'column'} } @{ $hint->details };
% my $fetched_rows = $hint->node->total_rows + $hint->node->total_rows_removed;
% my $dropped_rows = $hint->node->total_rows_removed;
<p>You have <a href="#l<%= $node_num %>" target="_new"><%= $hint->node->type %> (#<%= $node_num %>)</a> that could use an index.</p>
<p>This node searches in table <em><%= $table_name %></em> using many comparisons using <em>=</em> operator, on columns:
% for my $column_no ( 0 .. $#columns ) {
% if ($column_no == $#columns ) {
, and
% } elsif ( $column_no > 0 ) {
,
% }
<em><%= $columns[$column_no] %></em>
% }
. In process it fetches from disk <%= commify_number( $fetched_rows ) %> rows, just to drop
% if ( $fetched_rows == $dropped_rows ) {
<em>all</em>
% } else {
<%= commify_number( $dropped_rows ) %>
% }
of them!</p>
<p>Unfortunately I can't tell which index strategy will be the best option for you - you have to check it for yourself. Potential ideas:</p>
<ul>
<li>Make one index each on all of the listed columns:
<pre><code class="sql"><% for my $column_no ( 0 .. $#columns ) { =%>
CREATE INDEX CONCURRENTLY <%= db_ident_quote( "explain_depesz_com_hint_${id}_${node_num}_s_${column_no}" ) %> ON <%= db_ident_quote($table_name) %> ( <%= db_ident_quote($columns[$column_no]) %> );
<% } =%></code></pre>
</li>
<li>Make one index on all of the columns (order can be important), like:</li>
<pre><code class="sql">CREATE INDEX CONCURRENTLY <%= db_ident_quote( "explain_depesz_com_hint_${id}_${node_num}_a" ) %> ON <%= db_ident_quote($table_name) %> ( <%= join ', ', map { db_ident_quote($_) } reverse @columns %> );</code></pre>
<li>Make index on some of the columns (the ones that are most selective), like:</li>
<pre><code class="sql">CREATE INDEX CONCURRENTLY <%= db_ident_quote( "explain_depesz_com_hint_${id}_${node_num}_f" ) %> ON <%= db_ident_quote($table_name) %> ( <%= join ', ', map { db_ident_quote($_) } reverse @columns[1,-1] %> );</code></pre>
<li>Make partial index on some of the columns with condition based on condition that you use very often or always, like:</li>
<pre><code class="sql">CREATE INDEX CONCURRENTLY <%= db_ident_quote( "explain_depesz_com_hint_${id}_${node_num}_p" ) %> ON <%= db_ident_quote($table_name) %> ( <%= db_ident_quote( $columns[1] ) %> ) WHERE <%= db_ident_quote( $columns[0] ) %> = <%= $hint->details->[0]->{'value'} %>;</code></pre>
</ul>
% }
</li>
% }
</ol>
</div>
% }
% if ( stash('query') ) {
<div class="result-query res-tab hidden">
<pre id="query"><code class="sql"><%= stash('query') %></code></pre>
<button class="copy">Copy query to clipboard</button>
</div>
% }
% if ( stash('bquery') ) {
<div class="result-bquery res-tab hidden">
<pre id="bquery"><code class="sql"><%= stash('bquery') %></code></pre>
<button class="copy">Copy query to clipboard</button>
</div>
% }
% if ( stash('comments') ) {
<div class="result-comments res-tab hidden">
<pre id="comments"><code class="sql"><%= stash('comments') %></code></pre>
<button class="copy">Copy comments to clipboard</button>
</div>
% }
<div class="result-stats res-tab hidden">
% if ( $buffers ) {
<h1>I/O stats</h1>
<ul>
% if ( $buf_r ) {
<li>Query read <%= humanize_size( 8192 * $buf_r ) %> from disk (or system disk cache)
% my $t = $explain->total_buffers->data->{'timings'}->{'read'};
% if ( $t ) {
in <%= commify_number( $t) %> ms, at ~ <%= humanize_size( 8192 * $buf_r * 1000 / $t ) %>/s
% }
</li>
% }
% if ( $buf_w ) {
<li>Query wrote <%= humanize_size( 8192 * $buf_w ) %> to disk
% my $t = $explain->total_buffers->data->{'timings'}->{'write'};
% if ( $t ) {
in <%= commify_number( $t) %> ms, at ~ <%= humanize_size( 8192 * $buf_w * 1000 / $t ) %>/s
% }
</li>
% }
</ul>
% }
<h1>Per node type stats</h1>
<table class="stats">
<thead>
<tr><th>node type</th><th>count</th><th>sum of times</th><th>% of query</th></tr>
</thead>
<tbody>
% for my $node_type ( sort keys %{ $stats->{'nodes'} } ) {
<tr class="table-detail">
<td class="node-type"><%= $node_type %></td>
<td class="count"><%= $stats->{'nodes'}->{$node_type}->{'count'} %></td>
<td class="time"><%= commify_number( sprintf '%.03f ms', $stats->{'nodes'}->{$node_type}->{'time'} || 0 ) %></td>
<td class="percent">
<% my $total = $explain->top_node->total_inclusive_time || 0; %>
<% my $current = $stats->{'nodes'}->{$node_type}->{'time'} || 0; %>
<% my $percent = $total == 0 ? 0 : 100 * $current / $total; %>
<%= sprintf '%.1f %%', $percent %>
</td>
</tr>
% }
</tbody>
</table>
<h1>Per table stats</h1>
<table class="stats">
<thead>
<tr><th>Table name</th><th>Scan count</th><th>Total time</th><th>% of query</th></tr>
<tr><th>scan type</th><th>count</th><th>sum of times</th><th>% of table</th></tr>
</thead>
<tbody>
% for my $table_name ( sort keys %{ $stats->{'tables'} } ) {
<tr class="table-summary">
<td class="table-name"><%= $table_name %></td>
<td class="count"><%= $stats->{'tables'}->{$table_name}->{':total'}->{'count'} %></td>
<td class="time"><%= commify_number( sprintf '%.03f ms', $stats->{'tables'}->{$table_name}->{':total'}->{'time'} || 0 ) %></td>
<td class="percent">
<% my $total = $explain->top_node->total_inclusive_time || 0; %>
<% my $current = $stats->{'tables'}->{$table_name}->{':total'}->{'time'} || 0; %>
<% my $percent = $total == 0 ? 0 : 100 * $current / $total; %>
<%= sprintf '%.1f %%', $percent %>
</td>
</tr>
% for my $scan_type ( sort grep { ! /^:/ } keys %{ $stats->{'tables'}->{$table_name} } ) {
<tr class="table-detail">
<td class="scan-type"><%= $scan_type %></td>
<td class="count"><%= $stats->{'tables'}->{$table_name}->{$scan_type}->{'count'} %></td>
<td class="time"><%= commify_number( sprintf '%.03f ms', $stats->{'tables'}->{$table_name}->{$scan_type}->{'time'} || 0 ) %></td>
<td class="percent">
<% my $total = $stats->{'tables'}->{$table_name}->{':total'}->{'time'} || 0; %>
<% my $current = $stats->{'tables'}->{$table_name}->{$scan_type}->{'time'} || 0; %>
<% my $percent = $total == 0 ? 0 : 100 * $current / $total; %>
<%= sprintf '%.1f %%', $percent %>
</td>
</tr>
% }
% }
</tbody>
</table>
</div>
</div>
<% content_for 'head' => begin %>
<link rel="stylesheet" href="<%= url_for '/' %>css/highlight-styles/tomorrow-night.css">
<script src="<%= url_for '/' %>js/highlight.pack.js"></script>
<script>
$( document ).ready( function( ) {
/* startup */
$.fn.explain( 'init', $( '#explain-form' ), $( '#explain' ) );
hljs.initHighlightingOnLoad();
} );
</script>
<% end %>