Skip to content

perf(dispatch): construct request-start Plugins only inside the mixins guard - #3160

Merged
bpamiri merged 1 commit into
developfrom
peter/issue-2897-pra-eventmethods-hoist
Jun 12, 2026
Merged

perf(dispatch): construct request-start Plugins only inside the mixins guard#3160
bpamiri merged 1 commit into
developfrom
peter/issue-2897-pra-eventmethods-hoist

Conversation

@bpamiri

@bpamiri bpamiri commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

PR A of the staged plan on #2897 (Stages 2–4 design comment, 2026-06-11): the Stage 3 quick win.

EventMethods.cfc#$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 the construction was 100% wasted — and not cheap: every new wheels.Plugins() also pays the wheels.Global pseudo-constructor, including the $promoteIncludedGlobalsToThis() isCustomFunction scan 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 — hoist local.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):
    • pins that any new wheels.Plugins( in $runOnRequestStart occurs only inside the mixins-nonempty guard;
    • pins that $initializeMixins(variables) still runs inside the guard (the use site can't be silently deleted);
    • written to keep passing when Stage 3 proper (shared 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 $runOnRequestStart inside 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 as security/BareCfabortGuardSpec.cfc and events/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:

Engine Before fix After fix
Lucee 7 (SQLite) 1 pass / 1 fail (offender at EventMethods.cfc:173) 2 pass / 0 fail
Adobe 2023 (SQLite) 2 pass / 0 fail

Full core suite (Docker harness, wheels-test-lucee7:v1.0.0 / wheels-test-adobe2023:v1.0.1):

Engine Result Notes
Lucee 7 (SQLite) 4501 pass / 12 fail / 0 error (4531 specs) the 12 are the tolerated internal.testClientSpec harness artifacts — matches baseline
Adobe 2023 (SQLite) 4511 pass / 1 fail / 1 error (4531 specs) both artifacts (migrator.typedColumnDefaultsSpec, security.RouteTesterHardeningSpec) reproduce on pristine develop with this change stashed — pre-existing, unrelated areas

Refs #2897 (PR A of the staged plan; PRs B–D track Stage 3 proper, DC15, and Stage 2).

🤖 Generated with Claude Code

…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>
@bpamiri
bpamiri enabled auto-merge (squash) June 12, 2026 19:42
@bpamiri
bpamiri merged commit 274952d into develop Jun 12, 2026
10 checks passed
@bpamiri
bpamiri deleted the peter/issue-2897-pra-eventmethods-hoist branch June 12, 2026 19:45

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Mixins is referenced nowhere else in the function — only at vendor/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) reads application[$wheels.appKey].mixins at call time and depends on no constructor-time state — the no-arg new wheels.Plugins() never runs $init() (which requires pluginPath), so the instance carries only the wheels.Global pseudo-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 unconditional new 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, the lineNumber < guardLine clause is dead code: the scan is forward-only, so any line processed after guardLine is 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.

bpamiri pushed a commit that referenced this pull request Jun 13, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant