Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions changelog.d/debugbar-externalized-assets.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- The development debug bar's static CSS and JavaScript are now maintained as standalone files (`vendor/wheels/public/assets/css/debugbar.css`, `vendor/wheels/public/assets/js/debugbar.js`) and included into the bar, eliminating the CFML `##`-escaped inline blocks (a documented "unescaped `#` crashes the suite" hazard) and trimming the per-response debug payload via inter-tag whitespace collapse. The bar remains development-only and unchanged in production (#3210)
1 change: 1 addition & 0 deletions changelog.d/deploy-warmup-up-endpoint.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- New apps scaffolded with `wheels new` now ship a `/up` liveness/warm-up endpoint (`app/controllers/Up.cfc` + route). `wheels deploy`'s proxy healthcheck already probes `/up` before traffic cutover, so the dispatch → controller → render path is compiled on a freshly deployed node before the first real visitor — moving the one-time cold-start compile (the bulk of first-request latency) off user traffic. The production-config guide documents the warm-up recipe and recommends setting the engine's template-inspection mode to `never` in production (#3210)
1 change: 1 addition & 0 deletions changelog.d/protected-methods-o1-lookup.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- The per-request action-dispatch gate now tests the protected-helper list with an O(1) `StructKeyExists` lookup instead of an O(n) `ListFindNoCase` scan over the ~100-250 framework helper names. A companion `application.wheels.protectedControllerMethodsLookup` struct-as-set is built once at application start alongside the existing comma-list (which is retained); case-insensitive matching is unchanged (#3210)
1 change: 1 addition & 0 deletions changelog.d/schema-column-cache.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Database column metadata is now memoized per datasource+table in `application.wheels.cache.schema` when `cacheDatabaseSchema` is on (the default). Previously every model class init issued a fresh `cfdbinfo type="columns"` JDBC catalog round-trip — a significant first-request cost on remote or wide-schema databases, re-paid on every reload and for every model sharing a table. The cache is rebuilt on reload, so schema changes are still picked up on reload (the same contract as the model and controller config caches) (#3210)
25 changes: 25 additions & 0 deletions cli/lucli/templates/app/app/controllers/Up.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
component extends="Controller" {

/**
* Liveness / warm-up endpoint.
*
* `wheels deploy`'s proxy healthcheck probes `/up` before flipping traffic
* to a freshly deployed node (the default healthcheck path), and load
* balancers can use it as a readiness check. Returning 200 here also
* compiles the dispatch -> controller -> render path on the new host, so the
* first real visitor sees warm latency instead of the one-time first-request
* compile (which is otherwise the bulk of cold-start time).
*
* To warm your hottest ORM metadata too, touch your key models here before
* cutover, e.g.:
*
* model("Post").count();
*
* Keep this action read-only, cheap, and free of authentication so the probe
* is never blocked.
*/
function index() {
renderText("OK");
}

}
5 changes: 5 additions & 0 deletions cli/lucli/templates/app/config/routes.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
mapper()
// CLI-Appends-Here

// Liveness / warm-up endpoint. `wheels deploy`'s proxy healthcheck probes
// `/up` before traffic cutover; a 200 here also compiles the request path
// so the first real visitor gets warm latency. See app/controllers/Up.cfc.
.get(name="up", to="up##index")

// The "wildcard" call below enables automatic mapping of "controller/action" type routes.
// This way you don't need to explicitly add a route every time you create a new action in a controller.
.wildcard()
Expand Down
18 changes: 18 additions & 0 deletions vendor/wheels/Global.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -4389,6 +4389,24 @@ return local.$wheels;
return protectedMethods;
}

/**
* Convert the comma-list returned by `$buildProtectedControllerMethods()`
* into a struct-as-set so `$callAction()` can perform an O(1)
* `StructKeyExists` membership test on the per-request dispatch hot path
* instead of an O(n) `ListFindNoCase` scan over ~100-250 helper names.
* CFML struct keys are case-insensitive by default, preserving the prior
* `ListFindNoCase` semantics (an action named `ENV` is still rejected like
* `env`). Stored on `application.wheels.protectedControllerMethodsLookup`
* alongside the list, which is retained for callers expecting that shape.
*/
public struct function $protectedControllerMethodsLookup(required string methods) {
var lookup = {};
for (var name in ListToArray(arguments.methods)) {
lookup[name] = true;
}
return lookup;
}

/**
* Re-evaluate the given global-includes file into `application.wo`'s
* variables/this scope. Invoked from the bare `?reload=true` soft-reload
Expand Down
2 changes: 1 addition & 1 deletion vendor/wheels/controller/processing.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ component {
* Internal function.
*/
public void function $callAction(required string action) {
if (Left(arguments.action, 1) == "$" || ListFindNoCase(application.wheels.protectedControllerMethods, arguments.action)) {
if (Left(arguments.action, 1) == "$" || StructKeyExists(application.wheels.protectedControllerMethodsLookup, arguments.action)) {
// A helper-named or $-prefixed action is treated exactly like a
// missing action: it 404s (see #2845 and CLAUDE.md Anti-Pattern 8).
// Route through $throwErrorOrShow404Page — mirroring RecordNotFound /
Expand Down
23 changes: 23 additions & 0 deletions vendor/wheels/databaseAdapters/Base.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,23 @@ component output=false extends="wheels.Global"{
local.args.username = variables.username;
local.args.password = variables.password;
local.args.table = arguments.tableName;

// Column metadata is a JDBC catalog round-trip (cfdbinfo type="columns")
// fetched once per model class. On remote / wide-schema databases that
// round-trip dominates first-request latency, and it is otherwise re-paid
// on every reload and for every model sharing a table. When
// cacheDatabaseSchema is on, memoize the result per datasource+table in
// application scope. A reload rebuilds the application scope, so schema
// changes are still picked up on reload — the same contract as the model
// and controller config caches. (perf)
local.cacheSchema = $get("cacheDatabaseSchema");
if (local.cacheSchema) {
local.cacheKey = Hash(variables.dataSource & Chr(31) & arguments.tableName);
if (StructKeyExists(application.wheels.cache.schema, local.cacheKey)) {
return application.wheels.cache.schema[local.cacheKey];
}
}

if ($get("showErrorInformation")) {
try {
local.rv = $getColumnInfo(argumentCollection = local.args);
Expand All @@ -430,6 +447,12 @@ component output=false extends="wheels.Global"{
} else {
local.rv = $getColumnInfo(argumentCollection = local.args);
}

if (local.cacheSchema) {
lock name="wheels.cache.schema" type="exclusive" timeout="10" {
application.wheels.cache.schema[local.cacheKey] = local.rv;
}
}
return local.rv;
}

Expand Down
8 changes: 8 additions & 0 deletions vendor/wheels/events/onapplicationstart.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ component {
application.$wheels.cache.page = {};
application.$wheels.cache.partial = {};
application.$wheels.cache.query = {};
application.$wheels.cache.schema = {};
application.$wheels.cacheLastCulledAt = Now();

// Set up paths to various folders in the framework. When the app
Expand Down Expand Up @@ -400,6 +401,13 @@ component {
// helpers like env(), model(), redirectTo() are never URL-invokable.
application.$wheels.protectedControllerMethods = application.wo.$buildProtectedControllerMethods();

// Companion struct-as-set for O(1) membership checks on the dispatch hot
// path (see $callAction()); the comma-list above is kept for callers that
// expect that shape.
application.$wheels.protectedControllerMethodsLookup = application.wo.$protectedControllerMethodsLookup(
application.$wheels.protectedControllerMethods
);

// Enable the main GUI Component
if (application.$wheels.enablePublicComponent) {
application.$wheels.public = application.wo.$createObjectFromRoot(path = "wheels", fileName = "Public", method = "$init");
Expand Down
96 changes: 4 additions & 92 deletions vendor/wheels/events/onrequestend/debug.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -83,51 +83,9 @@ OR (StructKeyExists(url, "format") AND ListFindNoCase("json,xml,csv,pdf", url.fo
</cfif>
</cfloop>
<!--- cfformat-ignore-start --->
<cfoutput>
<cfsavecontent variable="local.wdbHtml"><cfoutput>
<div id="wheels-debugbar" style="all:initial;position:fixed;bottom:0;left:0;right:0;z-index:99999;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,sans-serif;">
<style>
##wheels-debugbar *{box-sizing:border-box;margin:0;padding:0;}
##wheels-debugbar{position:fixed;bottom:0;left:0;right:0;z-index:99999;font-size:13px;line-height:1.4;}
##wheels-debugbar .wdb-bar{display:flex;align-items:center;background:##1e1e2e;color:##cdd6f4;height:36px;padding:0 8px;gap:2px;border-top:1px solid ##45475a;user-select:none;}
##wheels-debugbar .wdb-bar a,##wheels-debugbar .wdb-bar span{color:##cdd6f4;text-decoration:none;}
##wheels-debugbar .wdb-tab{display:flex;align-items:center;gap:5px;padding:0 10px;height:36px;cursor:pointer;border:none;background:none;color:##cdd6f4;font-size:12px;font-family:inherit;white-space:nowrap;transition:background .15s;}
##wheels-debugbar .wdb-tab:hover{background:##313244;}
##wheels-debugbar .wdb-tab.active{background:##313244;color:##89b4fa;border-top:2px solid ##89b4fa;padding-top:2px;}
##wheels-debugbar .wdb-tab svg{width:14px;height:14px;fill:currentColor;flex-shrink:0;}
##wheels-debugbar .wdb-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;border-radius:9px;font-size:10px;font-weight:700;line-height:1;}
##wheels-debugbar .wdb-badge-green{background:##28a74533;color:##a6e3a1;}
##wheels-debugbar .wdb-badge-yellow{background:##ffc10733;color:##f9e2af;}
##wheels-debugbar .wdb-badge-red{background:##dc354533;color:##f38ba8;}
##wheels-debugbar .wdb-badge-blue{background:##89b4fa33;color:##89b4fa;}
##wheels-debugbar .wdb-sep{width:1px;height:20px;background:##45475a;margin:0 4px;}
##wheels-debugbar .wdb-spacer{flex:1;}
##wheels-debugbar .wdb-panel{display:none;position:fixed;bottom:36px;left:0;right:0;max-height:50vh;background:##1e1e2e;border-top:1px solid ##45475a;overflow-y:auto;color:##cdd6f4;padding:0;}
##wheels-debugbar .wdb-panel.open{display:block;}
##wheels-debugbar .wdb-panel-header{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;border-bottom:1px solid ##313244;position:sticky;top:0;background:##1e1e2e;z-index:1;}
##wheels-debugbar .wdb-panel-header h3{font-size:14px;font-weight:600;color:##cdd6f4;}
##wheels-debugbar .wdb-panel-body{padding:12px 16px;}
##wheels-debugbar .wdb-table{width:100%;border-collapse:collapse;}
##wheels-debugbar .wdb-table th{text-align:left;padding:6px 12px;font-size:11px;font-weight:600;color:##a6adc8;text-transform:uppercase;letter-spacing:.5px;background:##181825;border-bottom:1px solid ##313244;}
##wheels-debugbar .wdb-table td{padding:6px 12px;border-bottom:1px solid ##313244;font-size:12px;vertical-align:top;}
##wheels-debugbar .wdb-table tr:hover td{background:##31324433;}
##wheels-debugbar .wdb-kv{display:grid;grid-template-columns:180px 1fr;gap:0;}
##wheels-debugbar .wdb-kv dt{padding:6px 12px;font-weight:600;color:##a6adc8;font-size:12px;border-bottom:1px solid ##313244;}
##wheels-debugbar .wdb-kv dd{padding:6px 12px;font-size:12px;border-bottom:1px solid ##313244;word-break:break-word;}
##wheels-debugbar .wdb-kv dd code{font-family:'SF Mono',SFMono-Regular,Menlo,Consolas,monospace;background:##313244;padding:1px 5px;border-radius:3px;font-size:11px;}
##wheels-debugbar .wdb-timing-row{display:flex;align-items:center;gap:8px;margin-bottom:6px;}
##wheels-debugbar .wdb-timing-label{width:100px;font-size:12px;color:##a6adc8;text-align:right;}
##wheels-debugbar .wdb-timing-bar-bg{flex:1;height:20px;background:##313244;border-radius:3px;overflow:hidden;position:relative;}
##wheels-debugbar .wdb-timing-bar{height:100%;border-radius:3px;display:flex;align-items:center;padding-left:6px;font-size:10px;font-weight:600;color:##1e1e2e;min-width:30px;}
##wheels-debugbar .wdb-close-btn{background:none;border:none;color:##a6adc8;cursor:pointer;font-size:18px;padding:4px 8px;border-radius:4px;line-height:1;}
##wheels-debugbar .wdb-close-btn:hover{background:##45475a;color:##cdd6f4;}
##wheels-debugbar .wdb-link-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px;padding:4px 0;}
##wheels-debugbar .wdb-link-card{display:flex;align-items:center;gap:8px;padding:10px 12px;background:##313244;border-radius:6px;color:##cdd6f4;text-decoration:none;font-size:12px;font-weight:500;transition:background .15s;}
##wheels-debugbar .wdb-link-card:hover{background:##45475a;}
##wheels-debugbar .wdb-link-card svg{width:16px;height:16px;fill:##89b4fa;flex-shrink:0;}
##wheels-debugbar .wdb-env-dot{width:8px;height:8px;border-radius:50%;display:inline-block;}
##wheels-debugbar .wdb-section{margin-bottom:16px;}
##wheels-debugbar .wdb-section-title{font-size:11px;font-weight:700;color:##89b4fa;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid ##313244;}
</style>
<style><cfinclude template="/wheels/public/assets/css/debugbar.css"></style>

<!--- ============ COLLAPSED BAR ============ --->
<div class="wdb-bar" id="wdb-bar">
Expand Down Expand Up @@ -517,53 +475,7 @@ OR (StructKeyExists(url, "format") AND ListFindNoCase("json,xml,csv,pdf", url.fo
</button>
</div>

<script>
(function(){
var activePanel=null;
window.wdbToggle=function(name){
var panels=document.querySelectorAll('##wheels-debugbar .wdb-panel');
var tabs=document.querySelectorAll('##wheels-debugbar .wdb-tab');
if(activePanel===name){
wdbClosePanel();
return;
}
for(var i=0;i<panels.length;i++)panels[i].classList.remove('open');
var p=document.getElementById('wdb-panel-'+name);
if(p)p.classList.add('open');
for(var j=0;j<tabs.length;j++)tabs[j].classList.remove('active');
var t=document.getElementById('wdb-tab-'+name);
if(t)t.classList.add('active');
activePanel=name;
};
window.wdbClosePanel=function(){
var panels=document.querySelectorAll('##wheels-debugbar .wdb-panel');
var tabs=document.querySelectorAll('##wheels-debugbar .wdb-tab');
for(var i=0;i<panels.length;i++)panels[i].classList.remove('open');
for(var j=0;j<tabs.length;j++)tabs[j].classList.remove('active');
activePanel=null;
};
window.wdbMinimize=function(){
wdbClosePanel();
document.getElementById('wheels-debugbar').style.display='none';
document.getElementById('wdb-minimized').style.display='block';
try{sessionStorage.setItem('wdb-hidden','1');}catch(e){}
};
window.wdbRestore=function(){
document.getElementById('wheels-debugbar').style.display='';
document.getElementById('wdb-minimized').style.display='none';
try{sessionStorage.removeItem('wdb-hidden');}catch(e){}
};
window.wdbEnvSwitch=function(el){
var target=el.getAttribute('data-wdb-reload');
if(!target)return false;
var pw=window.prompt('Enter the reload password to switch environments:');
if(pw===null||pw==='')return false;
window.location.href=target+'&password='+encodeURIComponent(pw);
return false;
};
try{if(sessionStorage.getItem('wdb-hidden')==='1')wdbMinimize();}catch(e){}
})();
</script>
<script><cfinclude template="/wheels/public/assets/js/debugbar.js"></script>
</div>
</cfoutput>
</cfoutput></cfsavecontent><cfoutput>#ReReplace(local.wdbHtml, "(?m)>\s+<", "><", "all")#</cfoutput>
<!--- cfformat-ignore-end --->
41 changes: 41 additions & 0 deletions vendor/wheels/public/assets/css/debugbar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#wheels-debugbar *{box-sizing:border-box;margin:0;padding:0;}
#wheels-debugbar{position:fixed;bottom:0;left:0;right:0;z-index:99999;font-size:13px;line-height:1.4;}
#wheels-debugbar .wdb-bar{display:flex;align-items:center;background:#1e1e2e;color:#cdd6f4;height:36px;padding:0 8px;gap:2px;border-top:1px solid #45475a;user-select:none;}
#wheels-debugbar .wdb-bar a,#wheels-debugbar .wdb-bar span{color:#cdd6f4;text-decoration:none;}
#wheels-debugbar .wdb-tab{display:flex;align-items:center;gap:5px;padding:0 10px;height:36px;cursor:pointer;border:none;background:none;color:#cdd6f4;font-size:12px;font-family:inherit;white-space:nowrap;transition:background .15s;}
#wheels-debugbar .wdb-tab:hover{background:#313244;}
#wheels-debugbar .wdb-tab.active{background:#313244;color:#89b4fa;border-top:2px solid #89b4fa;padding-top:2px;}
#wheels-debugbar .wdb-tab svg{width:14px;height:14px;fill:currentColor;flex-shrink:0;}
#wheels-debugbar .wdb-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;border-radius:9px;font-size:10px;font-weight:700;line-height:1;}
#wheels-debugbar .wdb-badge-green{background:#28a74533;color:#a6e3a1;}
#wheels-debugbar .wdb-badge-yellow{background:#ffc10733;color:#f9e2af;}
#wheels-debugbar .wdb-badge-red{background:#dc354533;color:#f38ba8;}
#wheels-debugbar .wdb-badge-blue{background:#89b4fa33;color:#89b4fa;}
#wheels-debugbar .wdb-sep{width:1px;height:20px;background:#45475a;margin:0 4px;}
#wheels-debugbar .wdb-spacer{flex:1;}
#wheels-debugbar .wdb-panel{display:none;position:fixed;bottom:36px;left:0;right:0;max-height:50vh;background:#1e1e2e;border-top:1px solid #45475a;overflow-y:auto;color:#cdd6f4;padding:0;}
#wheels-debugbar .wdb-panel.open{display:block;}
#wheels-debugbar .wdb-panel-header{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;border-bottom:1px solid #313244;position:sticky;top:0;background:#1e1e2e;z-index:1;}
#wheels-debugbar .wdb-panel-header h3{font-size:14px;font-weight:600;color:#cdd6f4;}
#wheels-debugbar .wdb-panel-body{padding:12px 16px;}
#wheels-debugbar .wdb-table{width:100%;border-collapse:collapse;}
#wheels-debugbar .wdb-table th{text-align:left;padding:6px 12px;font-size:11px;font-weight:600;color:#a6adc8;text-transform:uppercase;letter-spacing:.5px;background:#181825;border-bottom:1px solid #313244;}
#wheels-debugbar .wdb-table td{padding:6px 12px;border-bottom:1px solid #313244;font-size:12px;vertical-align:top;}
#wheels-debugbar .wdb-table tr:hover td{background:#31324433;}
#wheels-debugbar .wdb-kv{display:grid;grid-template-columns:180px 1fr;gap:0;}
#wheels-debugbar .wdb-kv dt{padding:6px 12px;font-weight:600;color:#a6adc8;font-size:12px;border-bottom:1px solid #313244;}
#wheels-debugbar .wdb-kv dd{padding:6px 12px;font-size:12px;border-bottom:1px solid #313244;word-break:break-word;}
#wheels-debugbar .wdb-kv dd code{font-family:'SF Mono',SFMono-Regular,Menlo,Consolas,monospace;background:#313244;padding:1px 5px;border-radius:3px;font-size:11px;}
#wheels-debugbar .wdb-timing-row{display:flex;align-items:center;gap:8px;margin-bottom:6px;}
#wheels-debugbar .wdb-timing-label{width:100px;font-size:12px;color:#a6adc8;text-align:right;}
#wheels-debugbar .wdb-timing-bar-bg{flex:1;height:20px;background:#313244;border-radius:3px;overflow:hidden;position:relative;}
#wheels-debugbar .wdb-timing-bar{height:100%;border-radius:3px;display:flex;align-items:center;padding-left:6px;font-size:10px;font-weight:600;color:#1e1e2e;min-width:30px;}
#wheels-debugbar .wdb-close-btn{background:none;border:none;color:#a6adc8;cursor:pointer;font-size:18px;padding:4px 8px;border-radius:4px;line-height:1;}
#wheels-debugbar .wdb-close-btn:hover{background:#45475a;color:#cdd6f4;}
#wheels-debugbar .wdb-link-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px;padding:4px 0;}
#wheels-debugbar .wdb-link-card{display:flex;align-items:center;gap:8px;padding:10px 12px;background:#313244;border-radius:6px;color:#cdd6f4;text-decoration:none;font-size:12px;font-weight:500;transition:background .15s;}
#wheels-debugbar .wdb-link-card:hover{background:#45475a;}
#wheels-debugbar .wdb-link-card svg{width:16px;height:16px;fill:#89b4fa;flex-shrink:0;}
#wheels-debugbar .wdb-env-dot{width:8px;height:8px;border-radius:50%;display:inline-block;}
#wheels-debugbar .wdb-section{margin-bottom:16px;}
#wheels-debugbar .wdb-section-title{font-size:11px;font-weight:700;color:#89b4fa;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid #313244;}
Loading
Loading