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-integration-request-cache.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Reduced per-instance model/controller materialization overhead by serving the cached component-integration plan from the request scope after its first use in a request, instead of re-reading the synchronized application scope on every instantiation (#3213).
14 changes: 14 additions & 0 deletions vendor/wheels/global/objects.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@
if (!StructKeyExists(application, "wheels")) {
return $buildComponentIntegrationPlan(arguments.path);
}
// Request-scoped cache: the plan is stable within a request (the only
// invalidation — the rebuild-on-poison validation below — replaces both
// copies), and the application scope is synchronized on access. Serving
// the plan from the request scope turns ~6 application-scope reads per
// materialized object into one request-scope read (issue #3213): on
// Lucee 7 that cut the plan lookup from ~61µs to ~1µs per model
// instantiation in the benchmark rig.
if (StructKeyExists(request, "wheelsIntegrationPlans") && StructKeyExists(request.wheelsIntegrationPlans, arguments.path)) {
return request.wheelsIntegrationPlans[arguments.path];
}
if (!StructKeyExists(application.wheels, "integrationPlans")) {
lock name="wheels.integrationPlans.#application.applicationName#" type="exclusive" timeout="10" {
if (!StructKeyExists(application.wheels, "integrationPlans")) {
Expand Down Expand Up @@ -167,6 +177,10 @@
$warnNullIntegrationPlanRefs(arguments.path);
}
}
if (!StructKeyExists(request, "wheelsIntegrationPlans")) {
request.wheelsIntegrationPlans = {};
}
request.wheelsIntegrationPlans[arguments.path] = local.plan;
return local.plan;
}

Expand Down
22 changes: 21 additions & 1 deletion vendor/wheels/tests/specs/global/IntegrationPlanSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ component extends="wheels.WheelsTest" {
if (StructKeyExists(request.wheelsIntegrationPlanChecks, _checkKey)) {
StructDelete(request.wheelsIntegrationPlanChecks, _checkKey);
}
if (StructKeyExists(request.wheelsIntegrationPlans, _checkKey)) {
StructDelete(request.wheelsIntegrationPlans, _checkKey);
}
});

it("builds a model plan with no null references", function() {
Expand All @@ -47,15 +50,32 @@ component extends="wheels.WheelsTest" {
_originalPlan = application.wheels.integrationPlans[_planPath];
application.wheels.integrationPlans[_planPath] = $shallowCopyPlan(_originalPlan);
StructDelete(application.wheels.integrationPlans[_planPath][1].publicMethods[1], "ref");
// force re-validation within this request
// force re-validation within this request (both the once-per-request
// flag and the request-scope plan cache must be cleared so the
// poisoned application-scope plan is re-read)
if (StructKeyExists(request.wheelsIntegrationPlanChecks, _checkKey)) {
StructDelete(request.wheelsIntegrationPlanChecks, _checkKey);
}
if (StructKeyExists(request.wheelsIntegrationPlans, _checkKey)) {
StructDelete(request.wheelsIntegrationPlans, _checkKey);
}
var plan = g.$componentIntegrationPlan(_planPath);
expect(g.$integrationPlanHasNullRefs(plan)).toBeFalse();
expect(application.wheels.integrationPlans[_planPath]).toBe(plan);
});

it("serves the plan from the request scope after its first use", function() {
if (StructKeyExists(request.wheelsIntegrationPlans, _checkKey)) {
StructDelete(request.wheelsIntegrationPlans, _checkKey);
}
var first = g.$componentIntegrationPlan(_planPath);
expect(request.wheelsIntegrationPlans[_planPath]).toBe(first);
// a second call returns the same request-cached array without
// touching the application scope again
var second = g.$componentIntegrationPlan(_planPath);
expect(second).toBe(first);
});

});

}
Expand Down
Loading