Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions handlers/Main.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,62 @@ component extends="coldbox.system.RestHandler" {
);
}

/**
* Render a specific Lucee SQL view (grouped/timeline/slowest) for lazy loading
*/
function renderLuceeSqlView( event, rc, prc ){
event.paramValue( "viewType", "timeline" );
var profiler = variables.debuggerService.getProfilerById( rc.id );

if ( profiler.isEmpty() || !profiler.keyExists( "cfQueries" ) ) {
return "<div class='cbd-text-red'>Profiler not found or no SQL data</div>";
}

var formatter = variables.debuggerService.getFormatter();
var appPath = getSetting( "ApplicationPath" );

if ( rc.viewType == "grouped" ) {
return view(
view : "main/panels/requestTracker/luceeSqlGrouped",
module : "cbdebugger",
args : {
profiler : profiler,
debuggerService : variables.debuggerService,
debuggerConfig : variables.debuggerConfig,
formatter : formatter,
appPath : appPath
},
prePostExempt : true
);
} else if ( rc.viewType == "slowest" ) {
return view(
view : "main/panels/requestTracker/luceeSqlTable",
module : "cbdebugger",
args : {
sqlData : duplicate( profiler.cfQueries.all ).sort( function( a, b ){
return a.executionTime < b.executionTime ? 1 : -1;
} ),
debuggerService : variables.debuggerService,
formatter : formatter,
appPath : appPath
},
prePostExempt : true
);
} else {
return view(
view : "main/panels/requestTracker/luceeSqlTable",
module : "cbdebugger",
args : {
sqlData : profiler.cfQueries.all,
debuggerService : variables.debuggerService,
formatter : formatter,
appPath : appPath
},
prePostExempt : true
);
}
}

/**
* Export a profiler report as json
*/
Expand Down
61 changes: 61 additions & 0 deletions models/DebuggerService.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ component
variables.debugPassword = variables.debuggerConfig.debugPassword;
// uuid helper
variables.uuid = createObject( "java", "java.util.UUID" );
// Cache source lookups to avoid reading files for every SQL row.
variables.sourceFunctionCache = {};

// Store Environment struct
variables.environment = {
Expand Down Expand Up @@ -645,6 +647,65 @@ component
return getExceptionBean().openInEditorURL( argumentCollection = arguments );
}

/**
* Resolve the function name that contains a source location like path.cfc:123.
*/
string function getFunctionNameForSource( required string source ){
if ( !len( arguments.source ) ) {
return "";
}

if ( variables.sourceFunctionCache.keyExists( arguments.source ) ) {
return variables.sourceFunctionCache[ arguments.source ];
}

var result = "";

try {
var lineMatch = arguments.source.reFind( ":([0-9]+)$", 1, true );

if ( !lineMatch.len.len() || lineMatch.len[ 2 ] == 0 ) {
variables.sourceFunctionCache[ arguments.source ] = result;
return result;
}

var lineNumber = arguments.source.mid( lineMatch.pos[ 2 ], lineMatch.len[ 2 ] );
var template = arguments.source.left( lineMatch.pos[ 1 ] - 1 );

if ( !fileExists( template ) ) {
var normalizedTemplate = template.replace( "\root\", "\", "one" ).replace( "/root/", "/", "one" );

if ( fileExists( normalizedTemplate ) ) {
template = normalizedTemplate;
}
}

if ( !fileExists( template ) || !isNumeric( lineNumber ) ) {
variables.sourceFunctionCache[ arguments.source ] = result;
return result;
}

var lines = fileRead( template ).listToArray( chr( 10 ), true );
var startLine = min( val( lineNumber ), lines.len() );
var functionRE = "(^|\s)(private|public|remote|package)?\s*([\w\.\[\]]+\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(";

for ( var i = startLine; i >= 1; i-- ) {
var declaration = lines[ i ].replace( chr( 13 ), "", "all" ).trim();
var match = declaration.reFindNoCase( functionRE, 1, true );

if ( match.len.len() >= 5 && match.len[ 5 ] > 0 ) {
result = declaration.mid( match.pos[ 5 ], match.len[ 5 ] );
break;
}
}
} catch ( any e ) {
result = "";
}

variables.sourceFunctionCache[ arguments.source ] = result;
return result;
}

/**
* Get the exception bean helper lazy loaded
*
Expand Down
39 changes: 34 additions & 5 deletions models/Formatter.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,7 @@ component singleton {
.listToArray( variables.NEW_LINE )
.map( ( item ) => item.trim() )
// comma spacing
.map( ( item ) => item.reReplace(
"\s*(?![^()]*\))(,)\s*",
",#variables.NEW_LINE##indent#",
"all"
) )
.map( ( item ) => breakOnCommasOutsideParentheses( item, indent ) )
// Parenthesis spacing
.map( ( item ) => item.reReplace( "\((\w)", "( \1", "all" ) )
.map( ( item ) => item.reReplace( "(\w)\)", "\1 )", "all" ) )
Expand All @@ -72,6 +68,39 @@ component singleton {
.toList( variables.NEW_LINE );
}

/**
* Regex lookaheads are too expensive for large SQL strings on Lucee 7.
*/
private string function breakOnCommasOutsideParentheses( required string target, required string indent ){
var result = [];
var depth = 0;
var length = arguments.target.len();

for ( var i = 1; i <= length; i++ ) {
var char = arguments.target.mid( i, 1 );

if ( char == "(" ) {
depth++;
} else if ( char == ")" && depth > 0 ) {
depth--;
}

if ( char == "," && depth == 0 ) {
result.append( ",#variables.NEW_LINE##arguments.indent#" );

while ( i < length && arguments.target.mid( i + 1, 1 ).reFind( "\s" ) ) {
i++;
}

continue;
}

result.append( char );
}

return result.toList( "" );
}

/**
* Format an incoming json string to a pretty version
*
Expand Down
131 changes: 131 additions & 0 deletions views/main/panels/requestTracker/luceeSqlGrouped.cfm
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<cfparam name="args.profiler">
<cfparam name="args.debuggerService">
<cfparam name="args.formatter">
<cfparam name="args.appPath">
<cfoutput>
<table
border="0"
align="center"
cellpadding="0"
cellspacing="1"
class="cbd-tables">
<thead>
<tr>
<th width="5%">Count</th>
<th>Query</th>
</tr>
</thead>
<tbody>
<cfloop array="#args.profiler.cfQueries.grouped.keyArray()#" index="sqlHash">
<tr>
<td align="center">
<div class="cbd-badge-light">
#args.profiler.cfQueries.grouped[ sqlHash ].count#
</div>
</td>
<td>
<code id="acfSql-groupsql-#sqlHash#">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 cbd-floatRight cbd-text-pre mt5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
title="Copy SQL to Clipboard"
style="width: 50px; height: 50px; cursor: pointer;"
onclick="coldboxDebugger.copyToClipboard( 'acfSql-groupsql-#sqlHash#' )"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<cfset withoutDumbWhitespace = args.formatter.prettySql( args.profiler.cfQueries.grouped[ sqlHash ].sql )>
<pre>#withoutDumbWhitespace#</pre>
</code>
</td>
</tr>
<tr>
<td></td>
<td>
<table border="0" align="center" cellpadding="0" cellspacing="1" class="cbd-tables">
<thead>
<tr>
<th width="15%">Timestamp</th>
<th width="15%">Execution Time</th>
<th width="15%">Datasource</th>
<th>Source/Params</th>
</tr>
</thead>
<tbody>
<cfloop array="#args.profiler.cfQueries.grouped[ sqlHash ].records#" index="q">
<cfset rowId = createUUID()>
<cfset functionName = q.src.len() ? args.debuggerService.getFunctionNameForSource( q.src ) : "">
<cfset displaySource = q.src.replace( "\root\", "\", "one" ).replace( "/root/", "/", "one" )>
<cfset editorSource = displaySource>
<tr>
<td align="center">
#timeFormat(
args.debuggerService.fromEpoch( q.startTime ),
"hh:MM:SS.l tt"
)#
</td>
<td align="center">
#numberFormat( q.executionTime / 1000000 )# ms
</td>
<td align="center">
#( q.datasource ?: "QoQ" )#
</td>
<td>
<cfif q.src.len()>
<div class="mb10 mt10 cbd-params" style="background: ##f8fbff; border-color: ##b7d7ff; color: ##1f2937; line-height: 1.35;">
<!--- Title --->
<strong>Called From:</strong>
<!--- Open in Editor--->
<cfif args.debuggerService.openInEditorURL( event, editorSource ) NEQ "">
<div class="cbd-floatRight">
<a
class="cbd-button"
target="_self"
rel ="noreferrer noopener"
title="Open in Editor"
href="#args.debuggerService.openInEditorURL( event, editorSource )#"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
</a>
</div>
</cfif>
<!--- Template Path --->
<div title="#encodeForHTMLAttribute( q.src )#" style="font-family: Consolas, Monaco, monospace; font-size: 12px; margin-top: 2px; word-break: break-all;">
#displaySource#
</div>
<cfif functionName.len()>
<div class="mt5" style="display: flex; align-items: center; gap: 6px; flex-wrap: wrap;">
<strong>Function: </strong>
<span
id="luceeSql-group-function-#rowId#"
style="background: ##e0f2fe; border: 1px solid ##7dd3fc; border-radius: 4px; color: ##075985; font-family: Consolas, Monaco, monospace; font-size: 12px; font-weight: 700; padding: 2px 6px;"
>#functionName#</span>
<button
type="button"
class="cbd-button"
title="Copy function name"
style="font-size: 11px; padding: 2px 7px;"
onclick="coldboxDebugger.copyToClipboard( 'luceeSql-group-function-#rowId#' )"
>
Copy
</button>
</div>
</cfif>
</div>
</cfif>
</td>
</tr>
</cfloop>
</tbody>
</table>
</td>
</tr>
</cfloop>
</tbody>
</table>
</cfoutput>
Loading