Problem/Motivation
ContextDefinition::isSatisfiedBy() is pure with respect to its two operands — the same definition asked about the same context object cannot answer differently within a request — but it is expensive: it builds constraint objects, may generate sample entities through EntityContextDefinition::getSampleValues(), and runs the validator.
Callers ask it the same question repeatedly. Layout Builder is the extreme case: LayoutBuilderEntityViewDisplay::buildSections() calls SectionStorageManager::findByContext() once per rendered entity, which filters every section storage definition against every available context — and LazyContextRepository hands out the same runtime context objects each time, so the answers are already known.
Steps to reproduce
Measured with XHProf on a Drupal 11.3 site with 274 modules (109 available contexts), on a page rendering 137 Layout Builder entities:
- 137
findByContext()calls produced 76,720isSatisfiedBy()calls - 5,460 ms inclusive, 59.6% of the request's wall time
- 28,496 constraint objects built, 15,207 validator runs
Proposed resolution
Memoize the answer per operand pair.
This is the same treatment #2994550 gave the sibling method filterPluginDefinitionsByContexts(), which memoizes checkRequirements() behind a hash of the serialized context definitions and explicitly does not touch getMatchingContexts().
That key does not transfer here. Serializing the operands of this method breaks three existing ContextHandlerTest cases with "Serialization of 'Closure' is not allowed", so the MR keys on object identity instead, in a WeakMap holding both operands. That is also the stricter guarantee: it assumes nothing about constraint semantics, only that asking the identical pair twice gives the identical answer. Holding both operands as weak references matters — without a reference PHP may reuse the object id of a collected object, and a later pair would read a stale answer.
The WeakMap is created on first use rather than in a constructor, so the class signature stays as it is for subclasses.
Test coverage
Two cases in ContextHandlerTest, both failing without the change:
testGetMatchingContextsMemoizesTheAnswer()— a definition is asked about a context once across repeated calls.testGetMatchingContextsKeepsAnswersApart()— a second definition answers the other way round for both contexts, so an answer shared between definitions or between contexts surfaces as a wrong return value and not only as a call count.
Related issues
- #2994550 established this pattern in the sibling method.
- #2981889 made
isSatisfiedBy()itself cheaper by disabling recursive validation. The measurement above is on a version that already has that fix. - #3043319 reported this call path and was closed as a duplicate of #2981889.
Issue fork drupal-3618682
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
hydra commentedComment #4
hydra commentedPipeline is green now — the functional shard passed on a re-run of the same commit, nothing changed in between.
Recording the two failures that came before it, since both turn out to be known:
- `WorkflowUiTest::testWorkflowCreation` — the race condition in the State API from #3438424, which measured 3-4 failures per 100 runs before it was addressed.
- `SecurityAdvisoryTest::testPsa` — sensitive to the faked time in its setup; #3041885 already noted it failing on CI while passing locally.
Both fit the pattern described in #2829040, and neither reproduced on the other run of the same tree.
Comment #5
hydra commentedSetting to needs review — the MR is green and carries the test.