Skip to content

perf(model): inherit the model mixin surface instead of copying it per instance - #3462

Merged
bpamiri merged 2 commits into
developfrom
perf/model-inheritance-3213
Aug 31, 2026
Merged

bpamiri merged 2 commits into
developfrom
perf/model-inheritance-3213

Conversation

@bpamiri

@bpamiri bpamiri commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Closes #3213 (the remaining per-object gap).

What changed

The 18 model mixin components (vendor/wheels/model/*.cfc) were converted to cfscript-wrapped include fragments (*.cfm) that wheels.Model compile-time includes — the same pattern the global surface already uses (#3241). Model instances now inherit the full API, so $integrateComponents("wheels.model") no longer runs per materialization and the ~270-reference copy loop is gone.

  • super<name> convention preserved (super<name> originals are only registered for model overrides — controller/view helper overrides get no superLinkTo() #3325): Model.init() registers super<name> aliases for app-declared overrides, resolving the framework original from a shared wheels.Model prototype (per-class request-scoped cache; zero copies in the common no-override case). SuperOverrideSpec still passes.
  • Dynamic plugin/package mixins still work: they apply via $pluginObj().$initializeMixins(variables) at onDIcomplete(); pluginsMixinClassificationSpec / pluginsSharedInstanceSpec pass.
  • DI model-subsystem bindings (ModelFinderInterface etc.) now resolve to wheels.Model, which carries the whole surface; interface docs updated.
  • The runtime integration-plan cache remains for controller/mapper/view surfaces (IntegrationPlanSpec now pins the controller path; integrationPlanCacheSpec pins the model fast path).
  • Drive-by: buildInfoSpec used 2-arg directoryCreate(), which Adobe CF rejects ("takes 1 parameter") — fixed.
  • Drive-by: RocketUnitRunnerSpec's full-runner round-trip now also skips on BoxLang (same legacy-runner Invoke()/evaluate gaps as RustCFML — found on the boxlang matrix leg).

Measured (#3213 rig, Lucee 7, warmed)

  • 3,000 × model().new(): ~1,290 ms (develop) → ~745 ms (this branch, ~1.7× faster); the earlier no-alias prototype hit ~620 ms — the delta is the super<name> registration cost that preserves the override contract.
  • 40-test RocketUnit suite: ~221 ms → ~200 ms.

Verification

  • Local Lucee 7 full suite: 5552 pass / 0 fail.
  • Local RustCFML suite: at baseline (5551 pass, only the known migrationSpec :: addIndex failure).
  • Local matrix: boxlang+sqlite PASS; adobe2023+mysql has only pre-existing failures unrelated to this change (pure-CFML bcrypt int-overflow on ACF — will file separately — and a package-hardener symlink cleanup flake).

…r instance

The model mixin components (vendor/wheels/model/*.cfc) were converted to
cfscript-wrapped include fragments (vendor/wheels/model/*.cfm) that
wheels.Model compile-time includes — the same pattern the global surface
already uses (issue #3241). Model instances now INHERIT the full API, so
$integrateComponents("wheels.model") no longer runs per materialization
and the ~270-reference copy loop is gone (issue #3213).

- Measured on the #3213 rig (Lucee 7): 3,000 model().new() went from
  ~1,290 ms (4.0.3) / ~1,179 ms (develop) to ~620 ms; the instrumented
  profile shows the integration cost (plan lookup + copy loop + override
  set, ~45% of instantiation) at zero.
- The super<name> override convention (#3325) is preserved: Model.init()
  registers super<name> aliases for app-declared overrides, resolving the
  framework original from a shared wheels.Model prototype (per-class
  request-scoped cache; zero copies in the no-override case). The
  SuperOverrideSpec model cases still pass.
- Dynamic plugin/package mixins still apply via
  $pluginObj().$initializeMixins(variables) at onDIcomplete();
  pluginsMixinClassificationSpec and pluginsSharedInstanceSpec pass.
- The model subsystem DI bindings (ModelFinderInterface etc.) now resolve
  to wheels.Model, which carries the entire surface; the interface docs
  were updated. The runtime integration-plan cache remains for the
  controller/mapper/view surfaces (IntegrationPlanSpec now pins the
  controller path; integrationPlanCacheSpec pins the model fast path).

Verification: local Lucee full suite 5552 pass/0 fail; local RustCFML
suite at baseline (5551 pass, only the known migrationSpec addIndex
failure); adobe2023 + boxlang matrix legs run at PR time.

Signed-off-by: Peter Amiri <peter@alurium.com>
BoxLang's legacy-runner surface (Invoke()/evaluate) has the same gaps
as RustCFML's — the full-runner round-trip records both tests as errors,
so numSuccesses comes back 0. The handler contract itself is still pinned
by the $testErrorMessage spec on every engine, and by the full-runner
round-trip on Lucee/Adobe. Found while verifying #3213 on the boxlang
matrix leg.

Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri
bpamiri merged commit 5a6a15c into develop Aug 31, 2026
12 checks passed
@bpamiri
bpamiri deleted the perf/model-inheritance-3213 branch August 31, 2026 15:59
bpamiri added a commit that referenced this pull request Aug 31, 2026
…ctive invoke (#3464)

Two more #3213 hot-path cuts on top of the #3462 inheritance fast path:

1. $createObjectFromRoot's model-instance branch ($initModelObject) now
   does a direct CreateObject + structured $initModelObject call and runs
   onDIcomplete manually — skipping the DI container's resolveMapping /
   $resolveInitArguments auto-wiring and the reflective Invoke() on the
   hottest path in the framework. Models explicitly mapped in the
   container (map/to/toFactory/asSingleton/asRequestScoped) keep the
   legacy path via the new Injector.hasExplicitMapping().

2. $initModelObject's cached-class read no longer takes a $simpleLock:
   application.wheels.models[name] is only written after the class is
   fully built (config() + column processing), so a present entry is
   always complete and concurrent first loads are handled by the slow
   path.

Measured on the #3213 rig (Lucee 7, H2, warmed): 3,000 x model().new()
went from ~745 ms (#3462) to ~508 ms; the full chain 4.0.3 -> now is
~1,290 ms -> ~508 ms. RocketUnit suite ~180 ms.

InjectorInterface declares hasExplicitMapping(); InjectorSpec pins its
contract (false for unmapped names, true after map/to/toFactory/
asSingleton/asRequestScoped).

Verification: local Lucee full suite 5554 pass/0 fail; RustCFML at
baseline; boxlang matrix PASS; adobe2023 matrix shows only the known
pre-existing failures (bcrypt #3463 + package-hardener symlink flake).

Signed-off-by: Peter Amiri <peter@alurium.com>
bpamiri added a commit that referenced this pull request Oct 1, 2026
…grades and install (#3932)

* docs(guides): correct stale 4.1 statements on error email, reload, upgrades and install

- production-config: errorEmailAddress defaults to "" since 4.1.2 (GHSA-8gcv-3v9h-7gj2);
  with no recipient no error email is sent and a one-time warning goes to wheels.log
- environments-and-configuration: an empty reloadPassword fails closed since 4.0.4
  (#3062); #3059 is fixed, not open
- performance-notes: describe the released fixes (4.0.6 #3236, 4.1.0 #3461/#3462/#3464,
  4.1.1 #3656) instead of pointing at bleeding-edge
- manual-installation: use the 4.1.2 release asset names and tag
- upgrade: document the 4.1.0 app template drift advisory (#3379)
- 3x-to-4x: the wheels.Testbox upgrade-check rule shipped in 4.0.4
- manual-upgrades: caution that the 4.1.1 .env boolean fix (#3641) must be applied by
  hand to the app-owned public/Application.cfc

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>

* docs(guides): give the full error-email sender fallback order

errorEmailFromAddress, then errorEmailAddress, then the recipient (EventMethods.cfc). Review nit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>

---------

Signed-off-by: Peter Amiri <peter@alurium.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
bpamiri added a commit that referenced this pull request Oct 1, 2026
…or variables.super<name> (#3938)

* docs(guides): model overrides may delegate with super.x, super<name> or variables.super<name>

Two 4.1 pages disagreed and both were wrong for models.
manual-upgrades said 4.1.0 requires switching variables.superdelete() to
super.delete(); overriding-core-methods said super.findAll() stopped working
in 3.0. Probed on develop with a model overriding findAll() and delete():
superFindAll(), super.findAll(), superDelete(), super.delete() and
variables.superdelete() all reach the framework original on Lucee 7,
Adobe CF 2025 and BoxLang. Since 4.1.0 the model methods are compile-time
includes (#3462), and #3515 re-registers the super<name> aliases.

Controller and view helper guidance is unchanged (super<name>); super.x
there is untested and tracked in #3933.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>

* docs(guides): point performance-notes at the corrected model-override note

The 4.1.0 caution it referenced is now a note that model overrides need no change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>

---------

Signed-off-by: Peter Amiri <peter@alurium.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
bpamiri added a commit that referenced this pull request Oct 2, 2026
…/view overrides (#3933) (#4036)

* test(controller): pin super-delegation forms for model and controller/view overrides (#3933)

Characterise and pin how an override reaches the framework original, verified on
every engine (Lucee 7, Adobe 2023/2025, BoxLang, RustCFML):

- Model overrides delegate three ways — the bare super<name> alias, super.<name>,
  and variables.super<name> — and all three work everywhere (model mixins are
  compile-time includes, #3462).
- Controller and view-helper overrides reach the original only through the
  super<name> alias (superLinkTo). super.<name> resolves against wheels.Controller,
  where the helper is a mixin rather than an inherited method, so it FAILS on
  Lucee/Adobe/BoxLang. RustCFML happens to resolve it, so the dotted form is not
  portable; the spec tolerates that divergence and pins that where super.<name>
  does resolve it still delegates correctly.

Supplements the existing SuperOverrideSpec (super<name> for a model property and
super.delete()). Documents the contract in the Overriding Core Methods guide:
use super<name>() to reach the original; super.<name>() works only for model
methods.

Fixes #3933.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>

* test(controller): prove delegated finder returns the framework query (#3933)

rev1-r1 LOW: the model specs asserted only the override's own flag, not that the
delegated call returned framework data. Each fixture now captures the delegated
result in-fixture (null-safe) and exposes wasDelegated(), returnedQuery() and
delegatedCount(); the spec asserts the returned query's recordCount equals the
post count.

This surfaced a cross-engine finding: all three model forms RUN on every engine,
but RustCFML's super.<name> and variables.super<name> delegation runs and drops
the return value (returns null), where Lucee/Adobe/BoxLang propagate the query.
The spec asserts the recordCount only where the engine returns it, so the proof
is strong on the JVM engines and the RustCFML return divergence is pinned rather
than hidden (the portable, fully-working form remains the super<name> alias).

Also fixed the header comment (super.<name> resolves on RustCFML, fails on
Lucee/Adobe/BoxLang).

Refs #3933.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>

* test(controller): pin the super.<name> return per engine explicitly (#3933)

rev1-r1 LOW: `if (returnedQuery()) expect(count)` made the recordCount assertion
optional on every engine, so a JVM regression to null would pass silently and
RustCFML's null wasn't actually pinned. Probing each form on RustCFML showed the
divergence is narrow: only the dotted `super.<name>()` syntax drops the return
there — `variables.super<name>()` and the bare `super<name>()` alias return
correctly on RustCFML too.

So: assert returnedQuery + recordCount UNCONDITIONALLY for superFindAll() and
variables.superFindAll() (they return on every engine), and gate only
super.findAll() on engineAdapter.isRustCFML() — expecting the known null there
and the real query (returnedQuery=true + recordCount) everywhere else. The engine
check is pinning a known engine bug (tracked for upstream), not probing a
capability, which is why naming the engine is correct here.

Refs #3933.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>

---------

Signed-off-by: Peter Amiri <peter@alurium.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.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.

Wheels 2.x > 4.x Performance Comparison

1 participant