Resolution data is not memoised per parameter
- Created
- Decided
- Versions
- History
Context
The container supplies a constructor's or method's parameters by reflecting on each one: its type, whether it is
optional, its default, and its attributes. Each parameter becomes a Dependency, which holds the parameter's
ReflectionType and the Named, qualifier and resolvable attribute instances found on it. Handling parameter-level
attributes is the hot path of resolution, at about 3 µs per constructor parameter, 64% of it spent reading the
attributes.
The panel runs as a long-lived worker process, per ADR-0009, so anything the container caches is held for the life of the worker.
Attribute memoisation already exists at class level, per
ADR-0014, where every attribute that matters is a
marker tested only for presence. Parameter-level attributes are not all like that. Named and resolvable attributes
carry values that are read when the dependency is resolved, so they are needed as instances.
Decision
Resolution data is not memoised per parameter. The container keeps nothing about a parameter beyond the resolution
that reflected on it: no Dependency, no attribute instance, and no record of which type of attribute each attribute
class is. It keeps no resolution plan either, whether held in memory or compiled to a file at build time. Each
parameter is reflected on when it is supplied, and attribute memoisation stays at class level.
Alternatives
Memoising Dependency per parameter. It holds one entry for every class, method and parameter a worker ever
resolves, for the life of the worker: an unbounded, permanent memory cost for a bounded, one-off saving in reflection.
Each entry keeps attribute instances, with their values, and a ReflectionType alive.
Caching the resolution plan in memory, a prebuilt set of dependencies for each class and method. It measures about
fifteen times cheaper than preparing the parameters again, cutting roughly 20% from a representative resolution. It
carries the same unbounded, permanent cost, with every entry keeping a ReflectionType alive, and every entry would
depend on the module scope the resolution runs under, so a cached plan could not be shared between scopes.
Compiling the resolution plan to a file at build time. Only external modules need compiled files, because of how they are installed. The rest of the panel runs as one long-lived process, so a compiled plan adds more complexity than it saves.
Caching which type of attribute each attribute class is, so that a parameter's attributes are read in one pass
and matched by name. Named and resolvable attributes need instances, so each parameter's attributes are still read
through reflection, a marker needs no more than a boolean, and a qualifier only its class. Little reflection is
therefore removed, and what remains is a small saving in reflection calls with no problem behind it to solve.
The text this option was weighed in measured it differently, at 5.85 µs of a 9.19 µs call, and preferred it. That measurement is not disputed here. The reasoning that prevailed is that instances of some attributes are needed either way.
Consequences
Easier:
- The container's memory will not grow with the number of classes, methods and parameters a worker resolves over its life.
- Nothing cached about a parameter will need invalidating, so context known only when a resolution runs, such as the
module scope it runs under, can be stamped onto a
Dependencyat no cost.
Harder:
- Parameter-level attribute handling will remain the hot path of resolution, at about 3 µs per constructor parameter, paid every time a constructor or method has its parameters supplied.
Constrained:
- Attribute memoisation will stay at class level, limited to markers tested only for presence.
Sources
- Planning session, , following a code review, not publicly available and first written down on
: the decision, which is both
createdanddecided, and the reason that memoising per parameter in a long-lived worker is an unbounded, permanent memory cost for a bounded, one-off saving in reflection. - Issue #43, Engine - Container - Memoise class-level attribute lookups, : parameter-level attribute handling as the hot path, its measurements, and caching the whole resolution plan suggested as a follow-up.
- Issue #70, Engine - Container - Module scopes, , the first public record of the decision. Its original
text, replaced three minutes after it was opened and kept in its edit history, weighed caching the resolution plan,
each entry pinning a
ReflectionTypeand depending on the module scope, against caching which type of attribute each attribute class is, which it measured at 5.85 µs of a 9.19 µs call and preferred. Its revised text records thatDependencyis not memoised, so stamping a scope on it costs nothing, taken from what followed rather than anticipated. - Compiling the resolution plan to a file at build time being ruled out, and why: first written down on , with no record of when it was weighed.
- Caching which type of attribute each attribute class is being rejected, and why, against the measurement in that original text: first written down on .
src/Container/Dependency.phpat 728ad64: aDependencyholding the parameter'sReflectionTypeand itsNamed, qualifier and resolvable attribute instances.
Source adr/0020-resolution-data-is-not-memoised-per-parameter.md · commit 072385a · exported