DebugInfo: Expand queries table with params & details
Apart from the ugly implementation, this is pretty neat.
This commit is contained in:
parent
76f55f277b
commit
cbccf045b7
2 changed files with 153 additions and 59 deletions
189
FU/DebugImpl.pm
189
FU/DebugImpl.pm
|
|
@ -1,6 +1,7 @@
|
|||
# Internal module used by FU.pm
|
||||
package FU::DebugImpl 0.5;
|
||||
use v5.36;
|
||||
use utf8;
|
||||
use experimental 'for_list';
|
||||
use FU;
|
||||
use FU::XMLWriter ':html5_', 'fragment', 'xml_escape';
|
||||
|
|
@ -140,23 +141,74 @@ my @sections = (
|
|||
},
|
||||
|
||||
sql => sub {
|
||||
return () if !$FU::REQ->{trace_sql};
|
||||
# TODO: Summarize main table, expand to display full query, params table, interpolated query
|
||||
table_ sub {
|
||||
my $queries = $FU::REQ->{trace_sql};
|
||||
return () if !$queries;
|
||||
|
||||
# Convert binary params to text.
|
||||
# For queries with text_params, assume the params are already valid for the text format.
|
||||
my @binparams = grep $_->{type} && !$_->{text}, map $_->{params}->@*, @$queries;
|
||||
my @arg = map +($_->{type}, $_->{bin}), @binparams;
|
||||
my @text;
|
||||
my $ok = !@arg || eval { @text = $FU::DB->bin2text(@arg); 1 };
|
||||
$binparams[$_]{text} = $text[$_] for 0..$#text;
|
||||
pre_ "Error converting binary parameters:\n$@" if !$ok;
|
||||
|
||||
input_ type => 'checkbox', id => "row${_}_c" for 0..$#{$queries};
|
||||
table_ class => 'sqlt', sub {
|
||||
thead_ sub { tr_ sub {
|
||||
td_ class => 'num', 'Exec';
|
||||
td_ class => 'num', 'Prep';
|
||||
td_ class => 'num', 'Rows';
|
||||
td_ 'Query';
|
||||
} };
|
||||
my $rows = 0;
|
||||
for my($i, $st) (builtin::indexed $queries->@*) {
|
||||
$rows += $st->{nrows};
|
||||
tr_ sub {
|
||||
td_ class => 'num', sprintf '%.1f ms', $st->{exec_time}*1000;
|
||||
td_ class => 'num', !defined $st->{prepare_time} ? '-' : $st->{prepare_time} ? sprintf '%.1f ms', $st->{prepare_time}*1000 : 'cache';
|
||||
td_ class => 'num', $st->{nrows};
|
||||
td_ class => 'sum', sub {
|
||||
label_ for => "row${i}_c", sub {
|
||||
span_ class => 'closed', '▶';
|
||||
span_ class => 'open', '▼';
|
||||
txt_ $st->{query} =~ s/[\r\n]/ /rg =~ s/\s\s+/ /rg =~ s/^\s+//r;
|
||||
};
|
||||
};
|
||||
};
|
||||
tr_ class => 'details', id => "row$i", sub {
|
||||
td_ '';
|
||||
td_ colspan => 3, sub {
|
||||
pre_ $st->{query};
|
||||
if ($st->{params}->@*) {
|
||||
strong_ 'Parameters:';
|
||||
table_ sub {
|
||||
tr_ sub {
|
||||
td_ class => 'num', sprintf '$%d =', $_+1;
|
||||
td_ class => 'code', sub {
|
||||
my $p = $st->{params}[$_]{text};
|
||||
!defined $p ? em_ 'null' : txt_ $p;
|
||||
};
|
||||
} for (0..$#{$st->{params}});
|
||||
};
|
||||
# XXX: Buggy when the query contains string literals with $n variables.
|
||||
strong_ 'Interpolated:';
|
||||
pre_ $st->{query} =~ s{\$([1-9][0-9]*)}{
|
||||
my $v = $st->{params}[$1-1]{text};
|
||||
defined $v ? $FU::DB->escape_literal($v) : 'NULL'
|
||||
}egr;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
tr_ sub {
|
||||
td_ class => 'num', sprintf '%.1f ms', $_->{exec_time}*1000;
|
||||
td_ class => 'num', !defined $_->{prepare_time} ? '-' : $_->{prepare_time} ? sprintf '%.1f ms', $_->{prepare_time}*1000 : 'cache';
|
||||
td_ class => 'num', $_->{nrows};
|
||||
td_ class => 'code', $_->{query};
|
||||
} for $FU::REQ->{trace_sql}->@*;
|
||||
td_ class => 'num', sprintf '%.1f ms', $FU::REQ->{trace_sqlexec}*1000;
|
||||
td_ class => 'num', !defined $FU::REQ->{trace_sqlprep} ? '-' : sprintf '%.1f ms', $FU::REQ->{trace_sqlprep}*1000;
|
||||
td_ class => 'num', $rows;
|
||||
td_ class => 'sum', 'total';
|
||||
} if @$queries > 1;
|
||||
};
|
||||
('Queries', scalar $FU::REQ->{trace_sql}->@*)
|
||||
('Queries', scalar @$queries)
|
||||
},
|
||||
|
||||
fu => sub {
|
||||
|
|
@ -245,7 +297,7 @@ my @sections = (
|
|||
td_ class => 'code', $_->[1];
|
||||
} for @$lst;
|
||||
};
|
||||
('Prepared statements', scalar @$lst)
|
||||
('Prepared stmts', scalar @$lst)
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -267,51 +319,8 @@ sub framework_($data) {
|
|||
head_ sub {
|
||||
title_ 'FU Debugging Interface';
|
||||
meta_ name => 'viewport', content => 'width=device-width, initial-scale=1.0, user-scalable=yes';
|
||||
link_ rel => 'stylesheet', type => 'text/css', media => 'all', href => '?css';
|
||||
style_ type => 'text/css', <<~_;
|
||||
html { box-sizing: border-box; color: #000; background: #fff }
|
||||
*, *:before, *:after { box-sizing: inherit }
|
||||
* { margin: 0; padding: 0; font: inherit; color: inherit }
|
||||
|
||||
/* Ugh, fixed positioning */
|
||||
header { position: fixed; top: 0; left: 0; width: 100%; height: 40px; z-index: 2 }
|
||||
nav { position: fixed; top: 38px; left: 0; width: 200px; z-index: 2 }
|
||||
main { margin: 0 0 0 200px }
|
||||
|
||||
header, nav { background: #eee }
|
||||
header { border-bottom: 2px solid #009 }
|
||||
nav { border-bottom: 2px solid #009; border-right: 2px solid #009 }
|
||||
|
||||
header { display: flex; justify-content: space-between; align-items: baseline; padding: 5px 10px }
|
||||
header h1 { font-size: 120%; font-weight: bold }
|
||||
header menu { list-style-type: none; display: flex; gap: 15px }
|
||||
|
||||
body > input { display: none }
|
||||
nav { padding-top: 20px }
|
||||
nav menu { list-style-type: none }
|
||||
nav a { display: block; width: 100%; text-decoration: none; padding: 2px 10px; cursor: pointer; white-space: nowrap }
|
||||
nav a:hover { background-color: #fff }
|
||||
nav a span { float: right; font-size: 80% }
|
||||
|
||||
main { padding: 0 10px 30px 10px }
|
||||
main h1 { background: #eee; padding: 5px 10px 5px 205px; margin: 40px -10px 10px -210px; scroll-margin-top: 40px; font-size: 130%; font-weight: bold }
|
||||
main h2 { margin: 20px 0 5px 0; font-size: 120%; font-weight: bold }
|
||||
|
||||
p, table, pre { margin: 5px 0 }
|
||||
pre { font-family: monospace; white-space: pre; overflow-x: auto; padding-bottom: 15px; /* for the scrollbar, kinda browser-specific */ }
|
||||
table { border-collapse: collapse }
|
||||
td { padding: 1px 10px 1px 0; font-size: 12px; vertical-align: top }
|
||||
td.code { font-family: monospace }
|
||||
tr:hover { background-color: #eee }
|
||||
thead { font-weight: bold }
|
||||
.num { text-align: right; white-space: nowrap }
|
||||
|
||||
section.tabs { position: relative; display: flex; flex-wrap: wrap; z-index: 1; }
|
||||
section.tabs summary { cursor: pointer; order: 0; display: block; padding: 3px 5px; margin-right: 10px; background: #ddd }
|
||||
section.tabs summary:hover, section.tabs details[open] summary { background: #eee }
|
||||
section.tabs details { display: contents }
|
||||
section.tabs details *:nth-child(2) { order: 1; width: 100% }
|
||||
|
||||
small { color: #555; font-size: 90% }
|
||||
_
|
||||
};
|
||||
body_ sub {
|
||||
|
|
@ -378,10 +387,23 @@ sub load($id) {
|
|||
fu->set_body(scalar <$fn>);
|
||||
}
|
||||
|
||||
sub css {
|
||||
# Awful CSS row hiding hack. I'm not sorry.
|
||||
state $css = join '', <DATA>, map qq{
|
||||
#row${_}_c:checked ~ * label[for=row${_}_c] .closed { display: none }
|
||||
#row${_}_c:not(:checked) ~ * label[for=row${_}_c] .open { display: none }
|
||||
#row${_}_c:not(:checked) ~ * #row${_} { display: none }
|
||||
}, 0..1000;
|
||||
}
|
||||
|
||||
sub render {
|
||||
my $q = fu->query;
|
||||
if (!$q) {
|
||||
fu->set_body(framework_ [{id => 'lst', title => 'Recent Requests', html => fragment \&listing_ }]);
|
||||
} elsif ($q eq 'css') {
|
||||
fu->set_header('content-type', 'text/css');
|
||||
fu->set_header('cache-control', 'max-age=86400');
|
||||
fu->set_body(css());
|
||||
} elsif ($q eq 'cur') {
|
||||
fu->set_body(framework_ collect);
|
||||
} elsif ($q eq 'last') {
|
||||
|
|
@ -415,3 +437,62 @@ sub save {
|
|||
}
|
||||
|
||||
1;
|
||||
|
||||
__DATA__
|
||||
html { box-sizing: border-box; color: #000; background: #fff }
|
||||
*, *:before, *:after { box-sizing: inherit }
|
||||
* { margin: 0; padding: 0; font: inherit; color: inherit }
|
||||
|
||||
/* Ugh, fixed positioning */
|
||||
header { position: fixed; top: 0; left: 0; width: 100%; height: 40px; z-index: 2 }
|
||||
nav { position: fixed; top: 38px; left: 0; width: 200px; z-index: 2 }
|
||||
main { margin: 0 0 0 200px }
|
||||
|
||||
header, nav { background: #eee }
|
||||
header { border-bottom: 2px solid #009 }
|
||||
nav { border-bottom: 2px solid #009; border-right: 2px solid #009 }
|
||||
|
||||
header { display: flex; justify-content: space-between; align-items: baseline; padding: 5px 10px }
|
||||
header h1 { font-size: 120%; font-weight: bold }
|
||||
header menu { list-style-type: none; display: flex; gap: 15px }
|
||||
|
||||
body > input { display: none }
|
||||
nav { padding-top: 20px }
|
||||
nav menu { list-style-type: none }
|
||||
nav a { display: block; width: 100%; text-decoration: none; padding: 2px 10px; cursor: pointer; white-space: nowrap }
|
||||
nav a:hover { background-color: #fff }
|
||||
nav a span { float: right; font-size: 80% }
|
||||
|
||||
main { padding: 0 10px 30px 10px }
|
||||
main h1 { background: #eee; padding: 5px 10px 5px 205px; margin: 40px -10px 10px -210px; scroll-margin-top: 40px; font-size: 130%; font-weight: bold }
|
||||
main h2 { margin: 20px 0 5px 0; font-size: 120%; font-weight: bold }
|
||||
|
||||
p, table, pre { margin: 5px 0 }
|
||||
pre { border-left: 2px dotted #999; padding-left: 5px; font-family: monospace; white-space: pre; overflow-x: auto; padding-bottom: 15px; /* for the scrollbar, kinda browser-specific */ }
|
||||
table { border-collapse: collapse }
|
||||
td { padding: 1px 10px 1px 0; font-size: 12px; vertical-align: top }
|
||||
td.code { font-family: monospace }
|
||||
tr:hover { background-color: #eee }
|
||||
thead { font-weight: bold }
|
||||
.num { text-align: right; white-space: nowrap }
|
||||
|
||||
section.tabs { position: relative; display: flex; flex-wrap: wrap; z-index: 1; }
|
||||
section.tabs summary { cursor: pointer; order: 0; display: block; padding: 3px 5px; margin-right: 10px; background: #ddd }
|
||||
section.tabs summary:hover, section.tabs details[open] summary { background: #eee }
|
||||
section.tabs details { display: contents }
|
||||
section.tabs details *:nth-child(2) { order: 1; width: 100% }
|
||||
|
||||
.sqlt { width: 100%; table-layout: fixed }
|
||||
.sqlt .num { width: 50px }
|
||||
.sqlt .num:first-child { width: 75px }
|
||||
.sqlt .num:nth-child(2) { width: 60px }
|
||||
.sqlt .sum { white-space: nowrap; font-family: monospace; overflow: hidden; text-overflow: ellipsis }
|
||||
.sqlt label { cursor: pointer }
|
||||
.sqlt label span { color: #555; display: inline-block; width: 15px }
|
||||
.sqlt tr.details { background: #fff }
|
||||
.sqlt tr.details > td { padding-bottom: 10px }
|
||||
input[id^=row] { display: none }
|
||||
|
||||
small { color: #555; font-size: 90% }
|
||||
em { font-style: italic }
|
||||
strong { font-weight: bold }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue