Repository navigation
perf(controller): apply a per-class cached method integration instead of the per-method loop - #4166
Conversation
… of the per-method loop Controller.init() mixed about 550 controller and view methods into every new instance, one StructKeyExists pair and two writes at a time. Which names it adds and which super<name> aliases it writes depend only on the class, so each class now records that once and later instances apply it with StructAppend: adds without overwriting, super<name> aliases with overwriting, as the loop did. If a name the record treats as an override is missing on an instance, that instance takes the loop and the record is rebuilt. The record lives in application.wheels.controllerIntegration and is cleared on reload, with the controller cache, and when an integration plan is rebuilt for null refs (#3457). Development keeps the loop (cacheControllerIntegration is false there), so methods edited without a reload still apply. Refs #4149 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
|
rev1-r1 review @ 4fed451 (draft, 4.3): APPROVE on code; merge gated on compat-matrix run 37103968629 and on 4.2.0 shipping.
LOW (edge, doc-worthy rather than code): an instance that gains an extra method name at construction, beyond what its class defines (e.g. a pseudo-constructor that conditionally assigns a function to |
Wheels Test Results216 807 tests 212 683 ✅ 1h 6m 52s ⏱️ Results for commit 9d662d6. ♻️ This comment has been updated with latest results. |
…ng guide Adds a "Controller method integration cache" section: what the setting does, that it is on outside development, and when to turn it off (a controller that defines a method conditionally at construction and calls super<name>() for it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
Final: APPROVE @ 9d662d6 (lifted into 4.2; matrix 37261944917, all 32 legs read)Delta since my approval at 4fed451
Matrix
No open findings. |
Fixes #4149
Summary
Every request builds a controller instance, and
Controller.init()mixed the framework's controller and view methods into it one at a time: about 550{name, ref}entries, each with twoStructKeyExistschecks and two writes. On RustCFML that was most of a bare dispatch (#4149).Which names that loop adds, and which
super<name>aliases it writes, depend only on the controller class:So each class now records the outcome once, and every later instance applies it:
StructAppend(variables, result.adds, false); // never overwrite a name the instance has StructAppend(this, result.adds, false); StructAppend(variables, result.supers, true); // super<name> aliases always overwrite, as the loop did StructAppend(this, result.supers, true);Details:
$buildControllerIntegration()makes the loop's exact decisions without writing to the instance. It recordsadds,supersandcollisions(the class's overrides of framework methods). The plan order, thesuper<name>originals are only registered for model overrides — controller/view helper overrides get nosuperLinkTo()#3325 / Pin super<name> / super.<name> delegation for model, controller and view overrides with specs #3933super<name>rule and plugin mixin overrides all match the loop.application.wheels.controllerIntegration, a nested struct of refs in the same shape the plan cache already stores. It's cleared on reload, by$clearControllerInitializationCache(), and when$componentIntegrationPlanrebuilds a plan for null refs (Flaky bare NullPointerException in model instantiation on Lucee 7 (cleared by reload) #3457).cacheControllerIntegration, istrueoutside development. In development, controller methods are edited without a reload, so adding or removing an override there must keep taking effect immediately.cacheControllerIntegrationdoes, its defaults, and when to turn it off (a controller that defines a method conditionally at construction and callssuper<name>()for it).Model.cfc's per-row integration (perf(controller): apply a per-class precomputed method struct instead of the per-instance mixin loop #4149's "may apply") is a separate follow-up.Measurements
controller("test", {controller: "test", action: "index"}), which builds a full instance, timed per call. Each figure is the median of 3 runs of 200 calls after 20 warm-up calls, with the cache off (the loop) and on, in the same process on the same machine (Apple M3 Ultra, SQLite):The RustCFML loop figure matches #4149's 4.47 ms.
Test plan
vendor/wheels/tests/specs/controller/ControllerIntegrationCacheSpec.cfc:linkTo()(superOverride), one that usessuper.linkTo()(superLinkDot) and a plain one (test);superLinkTo()delegation give identical output on and off;super.linkTo()from an override gives the identical outcome (result or error) on and off;linkToas an override withsuperLinkToas an alias;wheels.tests.specs.controller, SQLite): 597 pass / 0 fail / 0 error on Lucee 7, Adobe 2023, Adobe 2025, BoxLang and RustCFML v0.693.0; the new spec is 6/6 everywhere.🤖 Generated with Claude Code