@@ -48,6 +48,12 @@ function escapeHtml(text) {
4848 return div . innerHTML ;
4949}
5050
51+ function formatSql ( sql ) {
52+ const escaped = escapeHtml ( sql ) ;
53+ const keywordRegex = / \b ( S E L E C T | F R O M | W H E R E | J O I N | L E F T J O I N | R I G H T J O I N | I N N E R J O I N | O U T E R J O I N | O N | A N D | O R | O R D E R B Y | G R O U P B Y | H A V I N G | L I M I T | O F F S E T | I N S E R T I N T O | V A L U E S | U P D A T E | S E T | D E L E T E | C R E A T E T A B L E | A L T E R T A B L E | D R O P T A B L E | D I S T I N C T | C O U N T | S U M | A V G | M A X | M I N | U N I O N | E X I S T S | I N | I S N U L L | I S N O T N U L L | L I K E | B E T W E E N | C A S E | W H E N | T H E N | E L S E | E N D | A S C | D E S C | A S | W I T H | R E C U R S I V E ) \b / gi;
54+ return escaped . replace ( keywordRegex , ( match ) => `<span class="sql-keyword">${ match } </span>` ) ;
55+ }
56+
5157function getPathFromUrl ( url ) {
5258 try {
5359 const parsed = new URL ( url ) ;
@@ -152,6 +158,55 @@ function getRequestType(req) {
152158 return { class : 'type-xhr' , label : 'XHR' } ;
153159}
154160
161+ function renderWaterfallChart ( queries ) {
162+ if ( ! Array . isArray ( queries ) || queries . length === 0 ) return '' ;
163+
164+ let cumulativeTime = 0 ;
165+ const queriesWithStartTime = queries . map ( q => {
166+ const startTime = cumulativeTime ;
167+ cumulativeTime += q . dur ?? 0 ;
168+ return { ...q , start_time : startTime } ;
169+ } ) ;
170+
171+ const maxEndTime = cumulativeTime ;
172+
173+ const gridInterval = maxEndTime > 100 ? 20 : maxEndTime > 50 ? 10 : 5 ;
174+ const gridLines = [ ] ;
175+ for ( let t = 0 ; t <= maxEndTime ; t += gridInterval ) {
176+ const position = ( t / maxEndTime ) * 100 ;
177+ gridLines . push ( `
178+ <div class="waterfall-grid-line" style="left: ${ position } %"></div>
179+ <div class="waterfall-grid-label" style="left: ${ position } %">${ t } ms</div>
180+ ` ) ;
181+ }
182+
183+ return `<div class="waterfall-grid-lines">${ gridLines . join ( '' ) } </div>
184+ ${ queriesWithStartTime . map ( ( query , idx ) => {
185+ const duration = query . dur ?? 0 ;
186+ const startTime = query . start_time ?? 0 ;
187+ const durationClass = duration > 50 ? 'slow' : duration > 10 ? 'medium' : 'fast' ;
188+ const barWidth = maxEndTime > 0 ? ( duration / maxEndTime ) * 100 : 0 ;
189+ const barLeft = maxEndTime > 0 ? ( startTime / maxEndTime ) * 100 : 0 ;
190+
191+ return `
192+ <div class="query${ query . dup ? ' duplicate' : '' } " data-idx="${ idx } ">
193+ <div class="query-header">
194+ <div class="query-summary">
195+ <code>${ formatSql ( query . s ) } </code>
196+ </div>
197+ <span class="query-time">${ duration . toFixed ( 1 ) } ms</span>
198+ </div>
199+ <div class="waterfall-row">
200+ <span class="waterfall-start">${ startTime . toFixed ( 1 ) } ms</span>
201+ <div class="waterfall-chart">
202+ <div class="timing-bar ${ durationClass } " style="width: ${ barWidth } %; margin-left: ${ barLeft } %"></div>
203+ </div>
204+ <span class="waterfall-end">${ ( startTime + duration ) . toFixed ( 1 ) } ms</span>
205+ </div>
206+ </div>` ;
207+ } ) . join ( '' ) } `;
208+ }
209+
155210function renderEmptyState ( ) {
156211 const app = document . getElementById ( 'app' ) ;
157212 const isLocalDomain = pageUrl && (
@@ -211,18 +266,18 @@ function renderUI() {
211266 <a href="${ escapeHtml ( url ) } " target="_blank" class="url-link" title="Open in new tab" aria-label="Open request URL in new tab">↗</a>
212267 </div>
213268 <div class="metrics">
214- ${ renderMetric ( 'queries' , data . count ?? 0 ) }
215- ${ renderMetric ( 'db' , formatMs ( data . db_time ) , 'ms' ) }
216- ${ renderMetric ( 'app' , formatMs ( data . app_time ) , 'ms' ) }
217- ${ data . duplicates ?. length ? `<span class="dup-warn">⚠ ${ data . duplicates . length } dup</span>` : '' }
269+ ${ renderMetric ( 'queries' , data . c ?? 0 ) }
270+ ${ renderMetric ( 'db' , formatMs ( data . db ) , 'ms' ) }
271+ ${ renderMetric ( 'app' , formatMs ( data . app ) , 'ms' ) }
272+ ${ data . dup ?. length ? `<span class="dup-warn">⚠ ${ data . dup . length } dup</span>` : '' }
218273 <span class="metric-label">${ formatTime ( currentRequest . timestamp ) } </span>
219274 </div>
220275 </div>` ;
221276
222- if ( data . duplicates ? .length > 0 ) {
223- html += `<div class="dups"> ${ data . duplicates . map ( dup = >
224- `<div class="dup"><code> ${ escapeHtml ( dup . sql ) } </code> <span class="dup-time"> ${ ( dup . duration ?? 0 ) . toFixed ( 1 ) } ms</span></div>`
225- ) . join ( '' ) } </div>`;
277+ if ( Array . isArray ( data . q ) && data . q . length > 0 ) {
278+ html += `<div class="queries"><div class="waterfall-container" >
279+ ${ renderWaterfallChart ( data . q ) }
280+ </div> </div>` ;
226281 }
227282
228283 const hasPageOrDoc = requestHistory . some ( r => r . isMainPage || r . isDocument ) ;
@@ -249,10 +304,10 @@ function renderUI() {
249304 <a href="${ escapeHtml ( req . url ) } " target="_blank" class="url-link" title="Open in new tab" aria-label="Open request URL in new tab">↗</a>
250305 </div>
251306 <div class="hist-stats">
252- ${ renderMetric ( 'queries' , req . data . count ?? 0 ) }
253- ${ renderMetric ( 'db' , formatMs ( req . data . db_time ) , 'ms' ) }
254- ${ renderMetric ( 'app' , formatMs ( req . data . app_time ) , 'ms' ) }
255- ${ req . data . duplicates ?. length ? `<span class="dup-warn">⚠</span>` : '' }
307+ ${ renderMetric ( 'queries' , req . data . c ?? 0 ) }
308+ ${ renderMetric ( 'db' , formatMs ( req . data . db ) , 'ms' ) }
309+ ${ renderMetric ( 'app' , formatMs ( req . data . app ) , 'ms' ) }
310+ ${ req . data . dup ?. length ? `<span class="dup-warn">⚠</span>` : '' }
256311 <span class="metric-label">${ formatTime ( req . timestamp ) } </span>
257312 </div>
258313 </div>` ;
0 commit comments