perf(dispatch): construct request-start Plugins only inside the mixins guard - #3160
Conversation
…s guard $runOnRequestStart constructed local.Mixins = new wheels.Plugins() unconditionally at the top of every request, but the instance is only used inside the !StructIsEmpty(application.wheels.mixins) guard. For mixin-free apps that construction was pure waste — and not cheap, since every new wheels.Plugins() also runs the wheels.Global pseudo-constructor, including the $promoteIncludedGlobalsToThis() isCustomFunction scan over ~200 inherited UDFs. Hoist the construction inside the guard. Zero semantic change for apps with mixins; mixin-free requests now skip the throwaway instantiation entirely. Pinned by events/RequestStartPluginsConstructionSpec.cfc, a structural guard (the construction site has no injection seam, so a source scan in the style of BareCfabortGuardSpec is the honest gate); it also pins that $initializeMixins(variables) still runs inside the guard, and is written to keep passing when Stage 3 proper replaces the construction with the shared PluginObj. Refs #2897 (PR A of the staged plan) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
There was a problem hiding this comment.
Wheels Bot — Reviewer
TL;DR: This PR hoists the local.Mixins = new wheels.Plugins() construction in EventMethods.cfc#$runOnRequestStart from the top of the function into the !StructIsEmpty(application.wheels.mixins) guard where the instance is consumed, sparing mixin-free apps a throwaway Plugins + wheels.Global pseudo-constructor on every request. I verified the change is semantically safe, the new structural guard spec follows established prior art, and the changelog fragment and commit are in order. Verdict: comment — one minor, non-blocking nit on the spec; otherwise clean.
Correctness
No findings. What I verified rather than assumed:
local.Mixinsis referenced nowhere else in the function — only atvendor/wheels/events/EventMethods.cfc:237(construction) and:239($initializeMixins(variables)), both inside the guard. Nothing between the old construction site (old line 173) and the guard touched the instance.- The construction now happens after the
$loadPlugins()/$loadPackages()blocks instead of before, but that is behavior-neutral:Plugins.cfc#$initializeMixins(line 879) readsapplication[$wheels.appKey].mixinsat call time and depends on no constructor-time state — the no-argnew wheels.Plugins()never runs$init()(which requirespluginPath), so the instance carries only thewheels.Globalpseudo-constructor state. - The no-arg
new wheels.Plugins().$initializeMixins(variables)idiom is the established pattern at five other call sites (Controller.cfc:445,Dispatch.cfc:1030,Model.cfc:680,Test.cfc:802,events/onapplicationstart.cfc:421), so this guard-scoped construction matches framework convention exactly.
Cross-engine
No findings. The spec avoids the documented traps: all Left() calls use lengths ≥ 1 (Invariant 8), there is no catch/finally scoping (Invariants 11–12), no inline-closure constructor args (Invariant 5), and Replace(rawLine, Chr(13), "", "all") plus ListToArray(content, Chr(10), true) handles CRLF sources. Arrow-function it() bodies match the existing core-suite specs. PR body reports green runs on Lucee 7 and Adobe 2023 with pre-existing failures reproduced on pristine develop.
Tests
One non-blocking nit:
vendor/wheels/tests/specs/events/RequestStartPluginsConstructionSpec.cfc:52— the offender condition(guardLine == 0 || lineNumber < guardLine)only flags constructions before the guard line, but the failure message says "constructed before/outside the guard". A future unconditionalnew wheels.Plugins(added after the guard block (still inside$runOnRequestStart, e.g. in the maintenance section) would reintroduce the same per-request waste without tripping the spec. Also, thelineNumber < guardLineclause is dead code: the scan is forward-only, so any line processed afterguardLineis set necessarily has a larger line number. I read the header's honest design note — line-anchored scanning without brace-tracking is the deliberate prior-art shape (security/BareCfabortGuardSpec.cfc,events/OnAppStartBareHelperGuardSpec.cfc), so I'm not asking for brace-tracking; just consider tightening the failure message to "before the guard" (or dropping the dead clause) so the spec's claim matches what it checks.
The second test (guard + $initializeMixins use-site still present) closes the hole where deleting both the guard and the construction would have passed the first test silently — good design. TDD red-first evidence in the PR body (fail on develop pointing at line 173, pass after) is consistent with the diff.
Docs
No findings. Changelog fragment changelog.d/request-start-plugins-guard-hoist.performance.md uses a valid type and contains the complete bullet line per changelog.d/README.md; no direct CHANGELOG.md edit. Internal perf change — no guides or CLAUDE.md update needed.
Commits
No findings. Single commit perf(dispatch): construct request-start Plugins only inside the mixins guard — valid type, header within 100 chars, subject explains the why.
Resolves the EventMethods.cfc conflict in the mixins-nonempty guard: #3160 (PR A) moved the wheels.Plugins construction inside the guard, and this branch (PR B) replaces the per-request construction with the shared application-cached $pluginObj() instance. Integrated form drops the construction entirely and keeps the cached-instance call (issue #2897). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Peter Amiri <petera@pai.com>
Summary
PR A of the staged plan on #2897 (Stages 2–4 design comment, 2026-06-11): the Stage 3 quick win.
EventMethods.cfc#$runOnRequestStartconstructedlocal.Mixins = new wheels.Plugins()unconditionally at the top of every request, but the instance is only used inside the!StructIsEmpty(application.wheels.mixins)guard. For mixin-free apps the construction was 100% wasted — and not cheap: everynew wheels.Plugins()also pays thewheels.Globalpseudo-constructor, including the$promoteIncludedGlobalsToThis()isCustomFunctionscan over ~200 inherited UDFs.This PR moves the construction inside the guard. Zero semantic change for apps with mixins; mixin-free requests skip the throwaway instantiation entirely.
Changes
vendor/wheels/events/EventMethods.cfc— hoistlocal.Mixins = new wheels.Plugins()from the top of$runOnRequestStart(was line 173) into the!StructIsEmpty(application.wheels.mixins)block where it is consumed, with a comment explaining why the placement matters.vendor/wheels/tests/specs/events/RequestStartPluginsConstructionSpec.cfc— new structural guard spec (TDD red-first; failed on develop pointing at line 173, passes after the hoist):new wheels.Plugins(in$runOnRequestStartoccurs only inside the mixins-nonempty guard;$initializeMixins(variables)still runs inside the guard (the use site can't be silently deleted);PluginObj, PR B) removes the construction from this function entirely.changelog.d/request-start-plugins-guard-hoist.performance.md— changelog fragment.Honest design note on the spec: the construction site has no injection seam (
new wheels.Plugins()is a direct constructor call with no instance counter), and executing$runOnRequestStartinside a spec drags in the full request lifecycle without making the construction observable anyway — so the practical gate is a line-anchored source scan with comment-prefix skipping, same approach assecurity/BareCfabortGuardSpec.cfcandevents/OnAppStartBareHelperGuardSpec.cfc(deliberately not a global comment-strip regex, which hangs Lucee 7 on large sources).Test evidence
TDD red-first, new spec in isolation:
Full core suite (Docker harness,
wheels-test-lucee7:v1.0.0/wheels-test-adobe2023:v1.0.1):internal.testClientSpecharness artifacts — matches baselinemigrator.typedColumnDefaultsSpec,security.RouteTesterHardeningSpec) reproduce on pristine develop with this change stashed — pre-existing, unrelated areasRefs #2897 (PR A of the staged plan; PRs B–D track Stage 3 proper, DC15, and Stage 2).
🤖 Generated with Claude Code