DebugInfo: Expand queries table with params & details

Apart from the ugly implementation, this is pretty neat.
This commit is contained in:
Yorhel 2025-05-01 11:48:08 +02:00
parent 76f55f277b
commit cbccf045b7
2 changed files with 153 additions and 59 deletions

23
FU.pm
View file

@ -121,11 +121,24 @@ sub query_trace($st,@) {
$REQ->{trace_nsqldirect}++ if !defined $st->prepare_time;
$REQ->{trace_sqlexec} += $st->exec_time;
$REQ->{trace_sqlprep} += $st->prepare_time if $st->prepare_time;
push $REQ->{trace_sql}->@*, {
query => $st->query, nrows => $st->nrows,
param_types => $st->param_types, param_values => $st->param_values,
exec_time => $st->exec_time, prepare_time => $st->prepare_time,
} if FU::debug;
if (FU::debug) {
my $t = $st->param_types;
my $v = $st->param_values;
my $txt = $st->get_text_params;
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 {
$DB = ref $INIT_DB eq 'CODE' ? $INIT_DB->() : FU::Pg->connect($INIT_DB);

View file

@ -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 }