Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/model-create-fast-path.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Model instances are now created with a direct constructor call instead of the DI container's resolve/auto-wire path plus a reflective invoke, and the per-instance class-struct read no longer takes a named lock — roughly 1.5x faster model materialization on top of the previous #3213 work (#3462).
14 changes: 14 additions & 0 deletions vendor/wheels/Injector.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,20 @@ component implements="wheels.interfaces.di.InjectorInterface" {
// Private helpers
// ---------------------------------------------------------------------------

/**
* Whether this name carries an explicit mapping or lifecycle flag in the
* container — the #3213 model fast path in $createObjectFromRoot must fall
* back to the full DI construction path when it does.
*/
public boolean function hasExplicitMapping(required string name) {
return (
StructKeyExists(variables.mappings, arguments.name)
|| StructKeyExists(variables.factories, arguments.name)
|| StructKeyExists(variables.singletonFlags, arguments.name)
|| StructKeyExists(variables.requestScopedFlags, arguments.name)
);
}

/**
* Resolve an alias to its component path, or return the name as-is if no mapping exists.
*/
Expand Down
18 changes: 8 additions & 10 deletions vendor/wheels/Model.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -637,16 +637,14 @@ component output="false" displayName="Model" extends="wheels.Global"{

// Attempt to grab the already‐loaded model object, or force‐load it
if ( structKeyExists( application.wheels.models, arguments.name ) ) {
// Fast path: model is already in the application cache
local.modelObj = application.wheels.models[ arguments.name ];

// At this point, local.modelObj is guaranteed to exist
variables.wheels.class = $simpleLock(
execute = "$classData",
name = local.lockName,
object = local.modelObj,
type = "readOnly"
);
// Fast path: model is already in the application cache. The entry
// is only written after $createModelClass finishes building the
// class (config() + column processing), so a present entry is
// always a complete class — reading the class struct needs no
// lock (#3213: the per-instance $simpleLock was pure overhead on
// the hot path; concurrent first loads are handled by the slow
// path above).
variables.wheels.class = application.wheels.models[arguments.name].$classData();
}
}

Expand Down
18 changes: 18 additions & 0 deletions vendor/wheels/global/objects.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,24 @@
local.argumentCollection = arguments;
if (local.method EQ 'init') {
local.rv = application.wheelsdi.getInstance(name = "#local.component#", initArguments = local.argumentCollection);
} else if (local.method EQ '$initModelObject' && !application.wheelsdi.hasExplicitMapping("#local.component#")) {
// Model-instance fast path (#3213): direct CreateObject + structured
// call skips the DI resolve/auto-wire and the reflective Invoke on
// the hottest path in the framework (every new() and finder row).
// onDIcomplete still runs so plugin/package mixins keep applying;
// models explicitly mapped in the container take the legacy path.
local.instance = CreateObject("component", local.component);
local.rv = local.instance.$initModelObject(
name = local.argumentCollection.name,
properties = local.argumentCollection.properties,
persisted = local.argumentCollection.persisted,
row = local.argumentCollection.row ?: 1,
base = local.argumentCollection.base ?: true,
useFilterLists = local.argumentCollection.useFilterLists ?: true
);
if (StructKeyExists(local.instance, "onDIcomplete")) {
local.instance.onDIcomplete();
}
} else {
local.instance = application.wheelsdi.getInstance(name = "#local.component#");
local.rv = Invoke(local.instance, local.method, local.argumentCollection);
Expand Down
6 changes: 6 additions & 0 deletions vendor/wheels/interfaces/di/InjectorInterface.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ interface {
*/
public boolean function containsInstance(required string name);

/**
* Whether this name carries an explicit mapping or lifecycle flag
* (map().to(), toFactory(), asSingleton(), asRequestScoped()).
*/
public boolean function hasExplicitMapping(required string name);

/**
* Mark the current mapping as a singleton (one instance per app lifecycle).
*
Expand Down
30 changes: 30 additions & 0 deletions vendor/wheels/tests/specs/di/InjectorSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@ component extends="wheels.WheelsTest" {

function run() {

describe("hasExplicitMapping", function() {

it("is false for an unmapped name", function() {
var di = new wheels.Injector(binderPath = "wheels.Bindings");
expect(di.hasExplicitMapping("wheels.tests._assets.di.SimpleService")).toBeFalse();
});

it("is true once a name is mapped or flagged", function() {
var di = new wheels.Injector(binderPath = "wheels.Bindings");
di.map("plain").to("wheels.tests._assets.di.SimpleService");
expect(di.hasExplicitMapping("plain")).toBeTrue();

var di2 = new wheels.Injector(binderPath = "wheels.Bindings");
di2.map("fact").toFactory(function(ctx) {
return "built";
});
expect(di2.hasExplicitMapping("fact")).toBeTrue();

var di3 = new wheels.Injector(binderPath = "wheels.Bindings");
di3.map("single").to("wheels.tests._assets.di.SimpleService").asSingleton();
expect(di3.hasExplicitMapping("single")).toBeTrue();

var di4 = new wheels.Injector(binderPath = "wheels.Bindings");
di4.map("req").to("wheels.tests._assets.di.SimpleService").asRequestScoped();
expect(di4.hasExplicitMapping("req")).toBeTrue();
});

});


describe("Injector", () => {

beforeEach(() => {
Expand Down
Loading