From cbccf045b71f22696c5d235345271b00a1f1ce51 Mon Sep 17 00:00:00 2001 From: Yorhel Date: Thu, 1 May 2025 11:48:08 +0200 Subject: [PATCH] DebugInfo: Expand queries table with params & details Apart from the ugly implementation, this is pretty neat. --- FU.pm | 23 ++++-- FU/DebugImpl.pm | 189 ++++++++++++++++++++++++++++++++++-------------- 2 files changed, 153 insertions(+), 59 deletions(-) diff --git a/FU.pm b/FU.pm index 316f273..8b8eaa3 100644 --- a/FU.pm +++ b/FU.pm @@ -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); diff --git a/FU/DebugImpl.pm b/FU/DebugImpl.pm index f02ceed..48c6cf1 100644 --- a/FU/DebugImpl.pm +++ b/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 '', , 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 }