The Game Panel specification

ADR-0020: Resolution data is not memoised per parameter

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:

Harder:

Constrained:

Sources