Repository navigation
perf(model): inherit the model mixin surface instead of copying it per instance - #3462
Merged
Merged
Conversation
…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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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) thatwheels.Modelcompile-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 nosuperLinkTo()#3325):Model.init()registerssuper<name>aliases for app-declared overrides, resolving the framework original from a sharedwheels.Modelprototype (per-class request-scoped cache; zero copies in the common no-override case).SuperOverrideSpecstill passes.$pluginObj().$initializeMixins(variables)atonDIcomplete();pluginsMixinClassificationSpec/pluginsSharedInstanceSpecpass.ModelFinderInterfaceetc.) now resolve towheels.Model, which carries the whole surface; interface docs updated.IntegrationPlanSpecnow pins the controller path;integrationPlanCacheSpecpins the model fast path).buildInfoSpecused 2-argdirectoryCreate(), which Adobe CF rejects ("takes 1 parameter") — fixed.RocketUnitRunnerSpec's full-runner round-trip now also skips on BoxLang (same legacy-runnerInvoke()/evaluate gaps as RustCFML — found on the boxlang matrix leg).Measured (#3213 rig, Lucee 7, warmed)
model().new(): ~1,290 ms (develop) → ~745 ms (this branch, ~1.7× faster); the earlier no-alias prototype hit ~620 ms — the delta is thesuper<name>registration cost that preserves the override contract.Verification
migrationSpec :: addIndexfailure).