You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The framework re-derives application-lifetime-constant data on its hottest paths, in four compounding ways. All file:line references verified against origin/develop @ 0688d914b.
1. Full mixin re-integration per model object and per HTTP request
vendor/wheels/Model.cfc:5 — every model object instantiation calls $integrateComponents("wheels.model"). That runs a directoryList over vendor/wheels/model/ plus one createObject per CFC (~18 files) (Model.cfc:545-557), then a getMetaData().functions scan and a ~245-function-reference copy into variables/this (Model.cfc:562-593). findAll(returnAs="objects") over N rows pays all of this N times.
vendor/wheels/Controller.cfc:4-5 — every request does the same twice (wheels.controller, 14 CFCs + wheels.view, 15 CFCs; ~266 functions) via Controller.cfc:148-193.
2. Throwaway wheels.Plugins (+ its 4,038-line wheels.Global parent) constructed per request and per row
vendor/wheels/Controller.cfc:216 and vendor/wheels/Model.cfc:650 — onDIcomplete() runs new wheels.Plugins().$initializeMixins(variables) per controller instance and per materialized model object.
vendor/wheels/events/EventMethods.cfc:156 — $runOnRequestStart constructs local.Mixins = new wheels.Plugins() unconditionally at the top of every request, but only uses it inside the !StructIsEmpty(application.wheels.mixins) guard (EventMethods.cfc:219-222) — 100% wasted for mixin-free apps. Each construction also pays the Global pseudo-constructor work, including the $promoteIncludedGlobalsToThis() variables-scope scan (vendor/wheels/Global.cfc:4017, 4026-4037).
Further call sites: vendor/wheels/Dispatch.cfc:809, vendor/wheels/Test.cfc:802.
A cached instance already exists — application[appKey].PluginObj (vendor/wheels/Global.cfc:3072) — but it cannot be shared safely today: $initializeMixins (vendor/wheels/Plugins.cfc:792-840) makes unscoped $wheels.* scratch writes into the component's own variables scope (a data race once the instance is shared), and its trailing StructDelete(variablesScope, "$wheels") is a no-op under the current call pattern.
3. Warm model()/controller() lookups pay reflective dispatch on every call
vendor/wheels/Global.cfc:1169-1181 (model()) and Global.cfc:1144-1163 (controller()) route every warm-cache hit through $doubleCheckedLock (Global.cfc:3) → $invoke (Global.cfc:289) → cfinvoke — two reflective dispatches just to evaluate the one-line $cachedModelClassExists (Global.cfc:917) / $cachedControllerClassExists (Global.cfc:928). model() is the framework's hottest function: called per association, per validation, per row.
4. $resolveInitArguments metadata scan per transient resolution — and it never matches inherited init()
vendor/wheels/Injector.cfc:290-320 (called from Injector.cfc:177, reached per model row via $createObjectFromRoot) runs getMetaData() on every resolution and inspects only the top-level functions array. Inherited init() lives under extends, so for every app controller/model it returns {} — guaranteed-no-op metadata churn, and the advertised init auto-wiring silently never applies to inherited init().
None of these inputs change during the application lifetime (short of ?reload=true).
Impact
This is the dominant avoidable overhead in the framework. Per materialized row: 1 directory listing, ~19 createObject, ~19 getMetaData, a ~245-function copy, a fresh Plugins+Global instantiation, and a $resolveInitArguments metadata scan. Per request: the ~29-CFC controller+view integration plus another throwaway Plugins. Per warm model() call: two reflective cfinvoke dispatches where a StructKeyExists would do.
Suggested approach (staged — each stage independently shippable, ordered by risk)
Stage 1 — lock-free warm fast path in model()/controller() (low risk; candidate first PR). Before calling $doubleCheckedLock, add a guarded direct lookup, e.g. for model():
(controllers struct for controller(), preserving the params branch that calls $createControllerObject.) Semantically identical to the existing unlocked first check inside $doubleCheckedLock; removes the reflective dispatches per warm call. The full StructKeyExists chain is required for early-bootstrap/reload windows where application.wheels.models may not exist yet, and the change must not bypass $clearModelInitializationCache semantics.
Stage 2 — memoize the integration product per base path. At first use, under a named lock, run one directoryList + createObject + getMetaData pass per base path (wheels.model, wheels.controller, wheels.view) and store a functionName → function-reference map in application scope (same precedent as application.wheels.mixins, which already stores function refs). Rewrite Model.cfc / Controller.cfc$integrateComponents to copy from the cached map into variables/this, preserving the existing collision rules exactly — note they differ per class:
Model.cfc:562-593 assigns super<name> into variables/this when the method already exists (else-branch), and super-prefixes under $willBeOverriddenByMixin.
Controller.cfc:165-193 silently skips on collision and only super-prefixes under $willBeOverriddenByMixin (Controller.cfc:195-213).
Apply the identical public-only access filter (cross-engine invariant 7). Must prove per engine that a function reference harvested from instance A and assigned into instance B's variables/this binds to B's variables at call time — the current code already relies on this (refs come from a fresh throwaway component), so behavior should be identical, but verify on Lucee 5/6/7, Adobe 2018/2021/2023/2025, BoxLang.
Stage 3 — reuse the cached application[appKey].PluginObj (Global.cfc:3072) instead of new wheels.Plugins() at EventMethods.cfc:156, Controller.cfc:216, Model.cfc:650, Dispatch.cfc:809, Test.cfc:802. Prerequisites: convert the unscoped $wheels.* scratch writes in $initializeMixins (Plugins.cfc:792-840) to local.-scoped so a shared instance is thread-safe; remove the now-no-op StructDelete(variablesScope, "$wheels"); hoist the EventMethods construction inside the mixins-nonempty guard. Keep the IsDefined("application") guards for Test.cfc/CLI call sites where application may be undefined.
Stage 4 — memoize Injector.cfc$resolveInitArguments (Injector.cfc:290) per component dot-path (init metadata is static per class); invalidate on reload. Whether to also fix the inherited-init() blind spot (walk extends) is a separate behavioral decision — it makes auto-wiring start applying where it silently never did (see review finding DI13).
Benchmark harness (for the issue thread, not specs): time findAll(returnAs="objects") over 500 rows and a 200-request warm controller loop, before/after, on Lucee 7 + Adobe 2023 via tools/test-matrix.sh containers.
Why this needs a design pass (not a drive-by PR)
This is the framework's hottest path with HIGH cross-engine regression surface:
Invariant 7 — only public$-prefixed methods integrate; the memoized map must apply the identical access filter or BoxLang will pass while Lucee/Adobe break (precedent: 8f35045a0 made $subscribe* public for exactly this).
Function-reference rebinding — copying refs from a cached map into a new instance's scopes must bind to the receiving instance's variables at call time on every engine. Same mechanism the current code uses, but it must be proven, not assumed — especially on BoxLang and Adobe 2021/2023.
Invariant 2 — Adobe CF rejects calling function members on application scope. The cached map may only be a storage vehicle (copy-then-call), never called in place; application.wheels.mixins is the existing precedent.
Shared PluginObj = shared variables scope — the unscoped $wheels.* writes are a data race once shared; they must be local-scoped first.
Collision semantics must not drift — Model's per-instance super-prefix-on-collision differs from Controller's $willBeOverriddenByMixin gate; memoization must preserve each verbatim or plugin override chains (superFoo) break silently.
Bootstrap/reload windows — the Stage 1 fast path must guard the full StructKeyExists chain and not bypass $clearModelInitializationCache.
No inline closures as constructor named args anywhere in new code (kills entire TestBox bundles on Adobe); no reserved-scope parameter names.
Stage 1: warm model()/controller() return identical objects as before; cold path, bootstrap, and reload windows unaffected; the params branch of controller() still calls $createControllerObject.
Stage 2: integration product built once per base path under a named lock; per-class collision rules (Model super-prefix-on-collision vs Controller skip + $willBeOverriddenByMixin) preserved verbatim; public-only filter identical to today; map stored as data only (copy-then-call).
Stage 2: function-reference rebinding proven by spec on Lucee 5/6/7, Adobe 2018/2021/2023/2025, BoxLang before merge.
Stage 3: $initializeMixins scratch writes local.-scoped; shared PluginObj reused at all five call sites; EventMethods construction hoisted inside the mixins guard; IsDefined("application") guards retained.
Stage 4: $resolveInitArguments memoized per dot-path; memo invalidated on reload.
?reload=true invalidates every new cache.
Full matrix green: tools/test-matrix.sh --all (and the targeted Adobe 2023 + Lucee 7 MySQL runs during development).
Before/after benchmark numbers (500-row findAll(returnAs="objects"), 200-request warm controller loop) posted on this issue for Lucee 7 and Adobe 2023.
Source
Internal multi-agent framework review, 2026-06-09 — finding T9 (mixin-integration-memoize), i.e. Top 10 item #9 "Per-request / per-row mixin re-integration and reflective dispatch tax", consolidating detail findings M10, C14, DC12, DC14, DI4, and DI13. Triage classified this as needing design/discussion rather than an autonomous PR; Stage 1 is the one low-risk separable piece that could become a small first PR.
Problem
The framework re-derives application-lifetime-constant data on its hottest paths, in four compounding ways. All file:line references verified against
origin/develop@0688d914b.1. Full mixin re-integration per model object and per HTTP request
vendor/wheels/Model.cfc:5— every model object instantiation calls$integrateComponents("wheels.model"). That runs adirectoryListovervendor/wheels/model/plus onecreateObjectper CFC (~18 files) (Model.cfc:545-557), then agetMetaData().functionsscan and a ~245-function-reference copy intovariables/this(Model.cfc:562-593).findAll(returnAs="objects")over N rows pays all of this N times.vendor/wheels/Controller.cfc:4-5— every request does the same twice (wheels.controller, 14 CFCs +wheels.view, 15 CFCs; ~266 functions) viaController.cfc:148-193.2. Throwaway
wheels.Plugins(+ its 4,038-linewheels.Globalparent) constructed per request and per rowvendor/wheels/Controller.cfc:216andvendor/wheels/Model.cfc:650—onDIcomplete()runsnew wheels.Plugins().$initializeMixins(variables)per controller instance and per materialized model object.vendor/wheels/events/EventMethods.cfc:156—$runOnRequestStartconstructslocal.Mixins = new wheels.Plugins()unconditionally at the top of every request, but only uses it inside the!StructIsEmpty(application.wheels.mixins)guard (EventMethods.cfc:219-222) — 100% wasted for mixin-free apps. Each construction also pays theGlobalpseudo-constructor work, including the$promoteIncludedGlobalsToThis()variables-scope scan (vendor/wheels/Global.cfc:4017, 4026-4037).vendor/wheels/Dispatch.cfc:809,vendor/wheels/Test.cfc:802.application[appKey].PluginObj(vendor/wheels/Global.cfc:3072) — but it cannot be shared safely today:$initializeMixins(vendor/wheels/Plugins.cfc:792-840) makes unscoped$wheels.*scratch writes into the component's ownvariablesscope (a data race once the instance is shared), and its trailingStructDelete(variablesScope, "$wheels")is a no-op under the current call pattern.3. Warm
model()/controller()lookups pay reflective dispatch on every callvendor/wheels/Global.cfc:1169-1181(model()) andGlobal.cfc:1144-1163(controller()) route every warm-cache hit through$doubleCheckedLock(Global.cfc:3) →$invoke(Global.cfc:289) →cfinvoke— two reflective dispatches just to evaluate the one-line$cachedModelClassExists(Global.cfc:917) /$cachedControllerClassExists(Global.cfc:928).model()is the framework's hottest function: called per association, per validation, per row.4.
$resolveInitArgumentsmetadata scan per transient resolution — and it never matches inheritedinit()vendor/wheels/Injector.cfc:290-320(called fromInjector.cfc:177, reached per model row via$createObjectFromRoot) runsgetMetaData()on every resolution and inspects only the top-levelfunctionsarray. Inheritedinit()lives underextends, so for every app controller/model it returns{}— guaranteed-no-op metadata churn, and the advertised init auto-wiring silently never applies to inheritedinit().None of these inputs change during the application lifetime (short of
?reload=true).Impact
This is the dominant avoidable overhead in the framework. Per materialized row: 1 directory listing, ~19
createObject, ~19getMetaData, a ~245-function copy, a freshPlugins+Globalinstantiation, and a$resolveInitArgumentsmetadata scan. Per request: the ~29-CFC controller+view integration plus another throwawayPlugins. Per warmmodel()call: two reflectivecfinvokedispatches where aStructKeyExistswould do.Suggested approach (staged — each stage independently shippable, ordered by risk)
Stage 1 — lock-free warm fast path in
model()/controller()(low risk; candidate first PR). Before calling$doubleCheckedLock, add a guarded direct lookup, e.g. formodel():if ( StructKeyExists(application, "wheels") && StructKeyExists(application.wheels, "models") && StructKeyExists(application.wheels.models, arguments.name) ) { return application.wheels.models[arguments.name]; }(
controllersstruct forcontroller(), preserving theparamsbranch that calls$createControllerObject.) Semantically identical to the existing unlocked first check inside$doubleCheckedLock; removes the reflective dispatches per warm call. The fullStructKeyExistschain is required for early-bootstrap/reload windows whereapplication.wheels.modelsmay not exist yet, and the change must not bypass$clearModelInitializationCachesemantics.Stage 2 — memoize the integration product per base path. At first use, under a named lock, run one
directoryList+createObject+getMetaDatapass per base path (wheels.model,wheels.controller,wheels.view) and store a functionName → function-reference map in application scope (same precedent asapplication.wheels.mixins, which already stores function refs). RewriteModel.cfc/Controller.cfc$integrateComponentsto copy from the cached map intovariables/this, preserving the existing collision rules exactly — note they differ per class:Model.cfc:562-593assignssuper<name>intovariables/thiswhen the method already exists (else-branch), and super-prefixes under$willBeOverriddenByMixin.Controller.cfc:165-193silently skips on collision and only super-prefixes under$willBeOverriddenByMixin(Controller.cfc:195-213).Apply the identical public-only access filter (cross-engine invariant 7). Must prove per engine that a function reference harvested from instance A and assigned into instance B's
variables/thisbinds to B'svariablesat call time — the current code already relies on this (refs come from a fresh throwaway component), so behavior should be identical, but verify on Lucee 5/6/7, Adobe 2018/2021/2023/2025, BoxLang.Stage 3 — reuse the cached
application[appKey].PluginObj(Global.cfc:3072) instead ofnew wheels.Plugins()atEventMethods.cfc:156,Controller.cfc:216,Model.cfc:650,Dispatch.cfc:809,Test.cfc:802. Prerequisites: convert the unscoped$wheels.*scratch writes in$initializeMixins(Plugins.cfc:792-840) tolocal.-scoped so a shared instance is thread-safe; remove the now-no-opStructDelete(variablesScope, "$wheels"); hoist theEventMethodsconstruction inside the mixins-nonempty guard. Keep theIsDefined("application")guards forTest.cfc/CLI call sites whereapplicationmay be undefined.Stage 4 — memoize
Injector.cfc$resolveInitArguments(Injector.cfc:290) per component dot-path (init metadata is static per class); invalidate on reload. Whether to also fix the inherited-init()blind spot (walkextends) is a separate behavioral decision — it makes auto-wiring start applying where it silently never did (see review finding DI13).Benchmark harness (for the issue thread, not specs): time
findAll(returnAs="objects")over 500 rows and a 200-request warm controller loop, before/after, on Lucee 7 + Adobe 2023 viatools/test-matrix.shcontainers.Why this needs a design pass (not a drive-by PR)
This is the framework's hottest path with HIGH cross-engine regression surface:
public$-prefixed methods integrate; the memoized map must apply the identical access filter or BoxLang will pass while Lucee/Adobe break (precedent:8f35045a0made$subscribe*public for exactly this).variablesat call time on every engine. Same mechanism the current code uses, but it must be proven, not assumed — especially on BoxLang and Adobe 2021/2023.application.wheels.mixinsis the existing precedent.PluginObj= sharedvariablesscope — the unscoped$wheels.*writes are a data race once shared; they must be local-scoped first.$willBeOverriddenByMixingate; memoization must preserve each verbatim or plugin override chains (superFoo) break silently.StructKeyExistschain and not bypass$clearModelInitializationCache.$resolveInitArgumentsmemo). Lesson from feat(migrator): enrich wheels_migrator_versions with name + applied_at (#2780) #2800: never trust a we-did-this-once flag for state that can be reset out-of-band — re-probe.Acceptance criteria
model()/controller()return identical objects as before; cold path, bootstrap, and reload windows unaffected; theparamsbranch ofcontroller()still calls$createControllerObject.$willBeOverriddenByMixin) preserved verbatim; public-only filter identical to today; map stored as data only (copy-then-call).$initializeMixinsscratch writeslocal.-scoped; sharedPluginObjreused at all five call sites;EventMethodsconstruction hoisted inside the mixins guard;IsDefined("application")guards retained.$resolveInitArgumentsmemoized per dot-path; memo invalidated on reload.?reload=trueinvalidates every new cache.tools/test-matrix.sh --all(and the targeted Adobe 2023 + Lucee 7 MySQL runs during development).findAll(returnAs="objects"), 200-request warm controller loop) posted on this issue for Lucee 7 and Adobe 2023.Source
Internal multi-agent framework review, 2026-06-09 — finding T9 (mixin-integration-memoize), i.e. Top 10 item #9 "Per-request / per-row mixin re-integration and reflective dispatch tax", consolidating detail findings M10, C14, DC12, DC14, DI4, and DI13. Triage classified this as needing design/discussion rather than an autonomous PR; Stage 1 is the one low-risk separable piece that could become a small first PR.