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
23
FU.pm
23
FU.pm
|
|
@ -121,11 +121,24 @@ sub query_trace($st,@) {
|
||||||
$REQ->{trace_nsqldirect}++ if !defined $st->prepare_time;
|
$REQ->{trace_nsqldirect}++ if !defined $st->prepare_time;
|
||||||
$REQ->{trace_sqlexec} += $st->exec_time;
|
$REQ->{trace_sqlexec} += $st->exec_time;
|
||||||
$REQ->{trace_sqlprep} += $st->prepare_time if $st->prepare_time;
|
$REQ->{trace_sqlprep} += $st->prepare_time if $st->prepare_time;
|
||||||
push $REQ->{trace_sql}->@*, {
|
if (FU::debug) {
|
||||||
query => $st->query, nrows => $st->nrows,
|
my $t = $st->param_types;
|
||||||
param_types => $st->param_types, param_values => $st->param_values,
|
my $v = $st->param_values;
|
||||||
exec_time => $st->exec_time, prepare_time => $st->prepare_time,
|
my $txt = $st->get_text_params;
|
||||||
} if FU::debug;
|
push $REQ->{trace_sql}->@*, {
|
||||||
|
query => $st->query, nrows => $st->nrows,
|
||||||
|
exec_time => $st->exec_time, prepare_time => $st->prepare_time,
|
||||||
|
# Store the binary value when we're in binary params mode, that way
|
||||||
|
# we don't have to keep a reference to the original perl value and
|
||||||
|
# we can defer & batch the conversion to text.
|
||||||
|
params => [ map +{
|
||||||
|
type => $t->[$_],
|
||||||
|
!defined $v->[$_] ? (text => undef) :
|
||||||
|
$txt ? (text => "$v->[$_]")
|
||||||
|
: (bin => $DB->perl2bin($t->[$_], $v->[$_]))
|
||||||
|
}, 0..$#$v ],
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sub _connect_db {
|
sub _connect_db {
|
||||||
$DB = ref $INIT_DB eq 'CODE' ? $INIT_DB->() : FU::Pg->connect($INIT_DB);
|
$DB = ref $INIT_DB eq 'CODE' ? $INIT_DB->() : FU::Pg->connect($INIT_DB);
|
||||||
|
|
|
||||||
189
FU/DebugImpl.pm
189
FU/DebugImpl.pm
|
|
@ -1,6 +1,7 @@
|
||||||
# Internal module used by FU.pm
|
# Internal module used by FU.pm
|
||||||
package FU::DebugImpl 0.5;
|
package FU::DebugImpl 0.5;
|
||||||
use v5.36;
|
use v5.36;
|
||||||
|
use utf8;
|
||||||
use experimental 'for_list';
|
use experimental 'for_list';
|
||||||
use FU;
|
use FU;
|
||||||
use FU::XMLWriter ':html5_', 'fragment', 'xml_escape';
|
use FU::XMLWriter ':html5_', 'fragment', 'xml_escape';
|
||||||
|
|
@ -140,23 +141,74 @@ my @sections = (
|
||||||
},
|
},
|
||||||
|
|
||||||
sql => sub {
|
sql => sub {
|
||||||
return () if !$FU::REQ->{trace_sql};
|
my $queries = $FU::REQ->{trace_sql};
|
||||||
# TODO: Summarize main table, expand to display full query, params table, interpolated query
|
return () if !$queries;
|
||||||
table_ sub {
|
|
||||||
|
# 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 {
|
thead_ sub { tr_ sub {
|
||||||
td_ class => 'num', 'Exec';
|
td_ class => 'num', 'Exec';
|
||||||
td_ class => 'num', 'Prep';
|
td_ class => 'num', 'Prep';
|
||||||
td_ class => 'num', 'Rows';
|
td_ class => 'num', 'Rows';
|
||||||
td_ 'Query';
|
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 {
|
tr_ sub {
|
||||||
td_ class => 'num', sprintf '%.1f ms', $_->{exec_time}*1000;
|
td_ class => 'num', sprintf '%.1f ms', $FU::REQ->{trace_sqlexec}*1000;
|
||||||
td_ class => 'num', !defined $_->{prepare_time} ? '-' : $_->{prepare_time} ? sprintf '%.1f ms', $_->{prepare_time}*1000 : 'cache';
|
td_ class => 'num', !defined $FU::REQ->{trace_sqlprep} ? '-' : sprintf '%.1f ms', $FU::REQ->{trace_sqlprep}*1000;
|
||||||
td_ class => 'num', $_->{nrows};
|
td_ class => 'num', $rows;
|
||||||
td_ class => 'code', $_->{query};
|
td_ class => 'sum', 'total';
|
||||||
} for $FU::REQ->{trace_sql}->@*;
|
} if @$queries > 1;
|
||||||
};
|
};
|
||||||
('Queries', scalar $FU::REQ->{trace_sql}->@*)
|
('Queries', scalar @$queries)
|
||||||
},
|
},
|
||||||
|
|
||||||
fu => sub {
|
fu => sub {
|
||||||
|
|
@ -245,7 +297,7 @@ my @sections = (
|
||||||
td_ class => 'code', $_->[1];
|
td_ class => 'code', $_->[1];
|
||||||
} for @$lst;
|
} for @$lst;
|
||||||
};
|
};
|
||||||
('Prepared statements', scalar @$lst)
|
('Prepared stmts', scalar @$lst)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -267,51 +319,8 @@ sub framework_($data) {
|
||||||
head_ sub {
|
head_ sub {
|
||||||
title_ 'FU Debugging Interface';
|
title_ 'FU Debugging Interface';
|
||||||
meta_ name => 'viewport', content => 'width=device-width, initial-scale=1.0, user-scalable=yes';
|
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', <<~_;
|
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 {
|
body_ sub {
|
||||||
|
|
@ -378,10 +387,23 @@ sub load($id) {
|
||||||
fu->set_body(scalar <$fn>);
|
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 {
|
sub render {
|
||||||
my $q = fu->query;
|
my $q = fu->query;
|
||||||
if (!$q) {
|
if (!$q) {
|
||||||
fu->set_body(framework_ [{id => 'lst', title => 'Recent Requests', html => fragment \&listing_ }]);
|
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') {
|
} elsif ($q eq 'cur') {
|
||||||
fu->set_body(framework_ collect);
|
fu->set_body(framework_ collect);
|
||||||
} elsif ($q eq 'last') {
|
} elsif ($q eq 'last') {
|
||||||
|
|
@ -415,3 +437,62 @@ sub save {
|
||||||
}
|
}
|
||||||
|
|
||||||
1;
|
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