Skip to content
Merged
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
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.schemaColumnCache` 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
34 changes: 34 additions & 0 deletions vendor/wheels/databaseAdapters/Base.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,32 @@ 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.wheels.schemaColumnCache. 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. The cache lives
// OUTSIDE application.wheels.cache.* on purpose: those categories hold
// {value, expiresAt} envelopes walked by the time-based cull, which would
// throw on a raw query. The read mirrors $getFromCache (try/catch +
// Duplicate) so a concurrent struct read can't surface a partial value and
// a caller can't mutate the cached query in place. (perf)
local.cacheSchema = $get("cacheDatabaseSchema");
if (local.cacheSchema) {
local.cacheKey = Hash(variables.dataSource & Chr(31) & arguments.tableName);
try {
if (StructKeyExists(application.wheels.schemaColumnCache, local.cacheKey)) {
return Duplicate(application.wheels.schemaColumnCache[local.cacheKey]);
}
} catch (any e) {
// fall through to a fresh catalog lookup on any concurrent-read hiccup
}
}

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

if (local.cacheSchema) {
// Store an isolated copy so the cached entry can never be mutated via a
// reference handed to an earlier caller.
lock name="wheels.schemaColumnCache" type="exclusive" timeout="10" {
application.wheels.schemaColumnCache[local.cacheKey] = Duplicate(local.rv);
}
}
return local.rv;
}

Expand Down
14 changes: 14 additions & 0 deletions vendor/wheels/events/onapplicationstart.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ component {
}
application.$wheels.controllers = {};
application.$wheels.models = {};
// Per-app column-metadata cache (see databaseAdapters/Base.cfc $getColumns).
// Deliberately a SIBLING of `cache`, not a `cache.*` category: it stores raw
// query objects that live for the application lifetime, whereas every
// `cache.*` category holds {value, expiresAt} envelopes that the cull/count
// machinery ($addToCache / $cacheCount) walks and dereferences `.expiresAt`
// on. Putting schema queries under `cache.*` makes the cull throw.
application.$wheels.schemaColumnCache = {};
application.$wheels.helperFileCache = {};
application.$wheels.layoutFileCache = {};
application.$wheels.existingObjectFiles = {};
Expand Down Expand Up @@ -400,6 +407,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 --->
Loading
Loading