From 120c7a0a2e9927eff3c1b69d6bcd49ac88d390a1 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 23 Oct 2026 13:43:12 +0200 Subject: [PATCH 1/3] perf(container): remember which initializer can initialize a class Every class without a registered definition was checked against all dynamic initializers, which were rebuilt after every container reset. In worker mode, this amounted to more than 400 `canInitialize()` calls per request (#2309). The container now remembers which initializer, if any, can initialize a class and tag. This is kept across resets, since it only depends on the registered initializers, and is forgotten when initializers are added or removed. Resolved initializers are now also reused until the container is reset, instead of being autowired every time they're used. Co-Authored-By: Claude Opus 5.5 --- docs/1-essentials/05-container.md | 2 + packages/container/src/DynamicInitializer.php | 3 + packages/container/src/GenericContainer.php | 82 +++++++++++++++---- packages/container/tests/ContainerTest.php | 61 ++++++++++++++ .../tests/Fixtures/CountingInitializer.php | 23 ++++++ .../LookupCountingDynamicInitializer.php | 34 ++++++++ 6 files changed, 187 insertions(+), 18 deletions(-) create mode 100644 packages/container/tests/Fixtures/CountingInitializer.php create mode 100644 packages/container/tests/Fixtures/LookupCountingDynamicInitializer.php diff --git a/docs/1-essentials/05-container.md b/docs/1-essentials/05-container.md index 9de0854cc5..3a6f34a896 100644 --- a/docs/1-essentials/05-container.md +++ b/docs/1-essentials/05-container.md @@ -173,6 +173,8 @@ final class RouteBindingInitializer implements DynamicInitializer } ``` +The container remembers the outcome of `canInitialize` for every class and tag, so it should only depend on those two arguments. + ## Autowired dependencies When you need to assign a default implementation to an interface without any specific instantiation steps, creating an initializer class for a single line of code might feel excessive. diff --git a/packages/container/src/DynamicInitializer.php b/packages/container/src/DynamicInitializer.php index 6bfe2aefe6..0a4d857700 100644 --- a/packages/container/src/DynamicInitializer.php +++ b/packages/container/src/DynamicInitializer.php @@ -9,6 +9,9 @@ interface DynamicInitializer { + /** + * Determines whether this initializer can initialize the given class and tag. The outcome is remembered by the container, so it should only depend on the class and tag. + */ public function canInitialize(ClassReflector $class, string|UnitEnum|null $tag): bool; public function initialize(ClassReflector $class, string|UnitEnum|null $tag, Container $container): object; diff --git a/packages/container/src/GenericContainer.php b/packages/container/src/GenericContainer.php index 8901e4900b..22546b778e 100644 --- a/packages/container/src/GenericContainer.php +++ b/packages/container/src/GenericContainer.php @@ -5,6 +5,7 @@ namespace Tempest\Container; use ArrayIterator; +use ArrayObject; use Closure; use Psr\Container\ContainerInterface; use ReflectionFunction; @@ -28,8 +29,16 @@ final class GenericContainer implements Container { use HasInstance; - /** @var array, DynamicInitializer> */ - private array $resolvedDynamicInitializers = []; + /** + * Which initializer can initialize a given class and tag, or `false` when there is none. Unlike resolved + * instances, this is kept when the container is reset, since it only depends on the registered initializers. + * + * @var ArrayObject|false> + */ + private ArrayObject $initializerLookups; + + /** @var ArrayObject */ + private ArrayObject $resolvedInitializers; public function __construct( /** @var ArrayIterator $definitions */ @@ -55,6 +64,10 @@ public function __construct( private(set) ?DependencyChain $chain = null, ) { + // These are objects, so that they are shared with clones of this container. + $this->initializerLookups = new ArrayObject(); + $this->resolvedInitializers = new ArrayObject(); + $this->singleton(Container::class, $this); $this->singleton(ContainerInterface::class, $this); $this->singleton(GenericContainer::class, $this); @@ -77,6 +90,7 @@ public function setSingletons(array $singletons): self public function setInitializers(array $initializers): self { $this->initializers = new ArrayIterator($initializers); + $this->initializerLookups->exchangeArray([]); return $this; } @@ -84,7 +98,7 @@ public function setInitializers(array $initializers): self public function setDynamicInitializers(array $dynamicInitializers): self { $this->dynamicInitializers = new ArrayIterator($dynamicInitializers); - $this->resolvedDynamicInitializers = []; + $this->initializerLookups->exchangeArray([]); return $this; } @@ -313,6 +327,8 @@ public function addInitializer(ClassReflector|string $initializerClass): Contain // First, we check whether this is a DynamicInitializer, // which don't have a one-to-one mapping + $this->initializerLookups->exchangeArray([]); + if ($initializerClass->getType()->matches(DynamicInitializer::class)) { $this->dynamicInitializers[] = $initializerClass->getName(); @@ -348,12 +364,15 @@ public function removeInitializer(ClassReflector|string $initializerClass): Cont ); unset($this->dynamicInitializers[$index]); - unset($this->resolvedDynamicInitializers[$initializerClass->getName()]); + unset($this->resolvedInitializers[$initializerClass->getName()]); + $this->initializerLookups->exchangeArray([]); return $this; } unset($this->initializers[$initializerClass->getName()]); + unset($this->resolvedInitializers[$initializerClass->getName()]); + $this->initializerLookups->exchangeArray([]); return $this; } @@ -368,6 +387,11 @@ public function addDecorator(ClassReflector|string $decoratorClass, ClassReflect return $this; } + /** + * @template TClassName of object + * @param class-string $className + * @return TClassName + */ private function resolve(string $className, string|UnitEnum|null $tag = null, mixed ...$params): object { $instance = $this->resolveDependency($className, $tag, ...$params); @@ -460,13 +484,35 @@ private function resolveDependency(string $className, string|UnitEnum|null $tag private function initializerForBuiltin(TypeReflector $target, string $tag): ?Initializer { if ($initializerClass = $this->initializers[$this->resolveTaggedName($target->getName(), $tag)] ?? null) { - return $this->resolve($initializerClass); + return $this->resolveInitializer($initializerClass); } return null; } private function initializerForClass(ClassReflector $target, string|UnitEnum|null $tag = null): Initializer|DynamicInitializer|null + { + // Most classes don't have an initializer, and finding out requires asking every dynamic initializer, + // so the outcome is remembered for every class and tag. + $lookup = $this->resolveTaggedName($target->getName(), $tag); + + if (! isset($this->initializerLookups[$lookup])) { + $this->initializerLookups[$lookup] = $this->findInitializerClass($target, $tag) ?? false; + } + + $initializerClass = $this->initializerLookups[$lookup]; + + if ($initializerClass === false) { + return null; + } + + return $this->resolveInitializer($initializerClass); + } + + /** + * @return class-string|null + */ + private function findInitializerClass(ClassReflector $target, string|UnitEnum|null $tag = null): ?string { // Initializers themselves can't be initialized, // otherwise you'd end up with infinite loops @@ -475,7 +521,7 @@ private function initializerForClass(ClassReflector $target, string|UnitEnum|nul } if ($initializerClass = $this->initializers[$this->resolveTaggedName($target->getName(), $tag)] ?? null) { - return $this->resolve($initializerClass); + return $initializerClass; } // Check if dynamic tags are allowed @@ -485,29 +531,29 @@ private function initializerForClass(ClassReflector $target, string|UnitEnum|nul $singleton = $class->getAttribute(Singleton::class) ?? $class->getMethod('initialize')->getAttribute(Singleton::class); if ($singleton?->dynamicTags) { - return $this->resolve($class->getName()); + return $class->getName(); } } // Loop through the registered initializers to see if // we have something to handle this class. foreach ($this->dynamicInitializers as $initializerClass) { - if (! isset($this->resolvedDynamicInitializers[$initializerClass])) { - $this->resolvedDynamicInitializers[$initializerClass] = $this->resolve($initializerClass); + if ($this->resolveInitializer($initializerClass)->canInitialize(class: $target, tag: $tag)) { + return $initializerClass; } - - $initializer = $this->resolvedDynamicInitializers[$initializerClass]; - - if (! $initializer->canInitialize(class: $target, tag: $tag)) { - continue; - } - - return $initializer; } return null; } + /** + * @param class-string $initializerClass + */ + private function resolveInitializer(string $initializerClass): Initializer|DynamicInitializer + { + return $this->resolvedInitializers[$initializerClass] ??= $this->resolve($initializerClass); + } + private function autowire(string $className, string|UnitEnum|null $tag, mixed ...$params): object { $classReflector = new ClassReflector($className); @@ -777,7 +823,7 @@ public function addResettable(string|ClassReflector $resettableClass): Container public function reset(): self { $this->resolvedSingletons = new ArrayIterator(); - $this->resolvedDynamicInitializers = []; + $this->resolvedInitializers->exchangeArray([]); foreach ($this->resettables as $resettableClass) { /** @var Resettable $resettable */ diff --git a/packages/container/tests/ContainerTest.php b/packages/container/tests/ContainerTest.php index b37f7e8e6b..2009077300 100644 --- a/packages/container/tests/ContainerTest.php +++ b/packages/container/tests/ContainerTest.php @@ -36,6 +36,7 @@ use Tempest\Container\Tests\Fixtures\ContainerObjectE; use Tempest\Container\Tests\Fixtures\ContainerObjectEInitializer; use Tempest\Container\Tests\Fixtures\CountingDynamicInitializer; +use Tempest\Container\Tests\Fixtures\CountingInitializer; use Tempest\Container\Tests\Fixtures\DecoratedClass; use Tempest\Container\Tests\Fixtures\DecoratedInterface; use Tempest\Container\Tests\Fixtures\DecoratorClass; @@ -58,6 +59,7 @@ use Tempest\Container\Tests\Fixtures\InvokableClass; use Tempest\Container\Tests\Fixtures\InvokableClassWithDependencies; use Tempest\Container\Tests\Fixtures\InvokableClassWithParameters; +use Tempest\Container\Tests\Fixtures\LookupCountingDynamicInitializer; use Tempest\Container\Tests\Fixtures\NonDynamicTaggedDependency; use Tempest\Container\Tests\Fixtures\OptionalTypesClass; use Tempest\Container\Tests\Fixtures\ResettableDependency; @@ -73,6 +75,7 @@ use Tempest\Container\Tests\Fixtures\UnionInterfaceB; use Tempest\Container\Tests\Fixtures\UnionTypesClass; use Tempest\Reflection\ClassReflector; +use Throwable; use function Tempest\Reflection\reflect; @@ -184,6 +187,64 @@ public function dynamic_initializer_instances_are_reused(): void $this->assertSame(1, CountingDynamicInitializer::$instances); } + #[Test] + public function initializer_lookups_are_remembered_across_resets(): void + { + LookupCountingDynamicInitializer::reset(); + + $container = new GenericContainer(); + $container->addInitializer(LookupCountingDynamicInitializer::class); + + $container->get(ContainerObjectA::class); + $container->get(ContainerObjectA::class); + $container->reset(); + $container->get(ContainerObjectA::class); + + $this->assertSame(1, LookupCountingDynamicInitializer::$lookups[ContainerObjectA::class]); + } + + #[Test] + public function initializer_lookups_are_forgotten_when_initializers_change(): void + { + $container = new GenericContainer(); + + // The container now remembers that there is no initializer for ContainerObjectD + try { + $container->get(ContainerObjectD::class); + $this->fail('ContainerObjectD should not be autowireable.'); + } catch (Throwable $throwable) { + $this->assertInstanceOf(DependencyCouldNotBeAutowired::class, $throwable); + } + + $container->addInitializer(ContainerObjectDInitializer::class); + + $this->assertSame('test', $container->get(ContainerObjectD::class)->prop); + + $container->removeInitializer(ContainerObjectDInitializer::class); + $container->addInitializer(CountingInitializer::class); + + $this->assertSame('counted', $container->get(ContainerObjectD::class)->prop); + } + + #[Test] + public function initializer_instances_are_reused_until_reset(): void + { + CountingInitializer::$instances = 0; + + $container = new GenericContainer(); + $container->addInitializer(CountingInitializer::class); + + $container->get(ContainerObjectD::class); + $container->get(ContainerObjectD::class); + + $this->assertSame(1, CountingInitializer::$instances); + + $container->reset(); + $container->get(ContainerObjectD::class); + + $this->assertSame(2, CountingInitializer::$instances); + } + #[Test] public function arrays_are_automatically_created(): void { diff --git a/packages/container/tests/Fixtures/CountingInitializer.php b/packages/container/tests/Fixtures/CountingInitializer.php new file mode 100644 index 0000000000..ea81637943 --- /dev/null +++ b/packages/container/tests/Fixtures/CountingInitializer.php @@ -0,0 +1,23 @@ + */ + public static array $lookups = []; + + public static function reset(): void + { + self::$lookups = []; + } + + public function canInitialize(ClassReflector $class, string|UnitEnum|null $tag): bool + { + self::$lookups[$class->getName()] = (self::$lookups[$class->getName()] ?? 0) + 1; + + return false; + } + + public function initialize(ClassReflector $class, string|UnitEnum|null $tag, Container $container): object + { + throw new LogicException('LookupCountingDynamicInitializer should not initialize objects.'); + } +} From 601bfe480b57c113c6b3b7a1c98c432ec54b596b Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 23 Oct 2026 13:48:41 +0200 Subject: [PATCH 2/3] perf(container): cache reflection data needed for autowiring Autowiring a class reflected on it from scratch every time: its constructor, parameters, properties, and the `#[Tag]`, `#[Proxy]`, `#[Inject]` and `#[Singleton]` attributes were all looked up and instantiated again. Since class definitions can't change at runtime, this data is now cached once and shared between containers. Resolving an already resolved singleton also no longer reflects on its class. Co-Authored-By: Claude Opus 5.5 --- packages/container/src/GenericContainer.php | 177 ++++++++++++++------ 1 file changed, 130 insertions(+), 47 deletions(-) diff --git a/packages/container/src/GenericContainer.php b/packages/container/src/GenericContainer.php index 22546b778e..13a87a4f15 100644 --- a/packages/container/src/GenericContainer.php +++ b/packages/container/src/GenericContainer.php @@ -19,6 +19,7 @@ use Tempest\Reflection\FunctionReflector; use Tempest\Reflection\MethodReflector; use Tempest\Reflection\ParameterReflector; +use Tempest\Reflection\PropertyReflector; use Tempest\Reflection\TypeReflector; use Throwable; use UnitEnum; @@ -40,6 +41,29 @@ final class GenericContainer implements Container /** @var ArrayObject */ private ArrayObject $resolvedInitializers; + /* + * Reflection data only depends on class definitions, which can't change at runtime, + * so it is cached once and shared between all containers. + */ + + /** @var array */ + private static array $classReflectors = []; + + /** @var array */ + private static array $constructors = []; + + /** @var array> */ + private static array $parameters = []; + + /** @var array> */ + private static array $injectedProperties = []; + + /** @var array */ + private static array $autowiredSingletons = []; + + /** @var array */ + private static array $initializerSingletons = []; + public function __construct( /** @var ArrayIterator $definitions */ private(set) ArrayIterator $definitions = new ArrayIterator(), @@ -405,8 +429,6 @@ private function resolve(string $className, string|UnitEnum|null $tag = null, mi private function resolveDependency(string $className, string|UnitEnum|null $tag = null, mixed ...$params): object { - $class = new ClassReflector($className); - $dependencyName = $this->resolveTaggedName($className, $tag); // Check if a resolved singleton is present. A singleton is only registered once it is fully @@ -415,6 +437,8 @@ private function resolveDependency(string $className, string|UnitEnum|null $tag return $instance; } + $class = self::reflect($className); + // Check if the class has been registered as a singleton. if ($singletonDefinition = $this->singletonDefinitions[$dependencyName] ?? null) { $this->resolveChain()->add($class); @@ -443,7 +467,7 @@ private function resolveDependency(string $className, string|UnitEnum|null $tag // Next we check if any of our default initializers can initialize this class. if (($initializer = $this->initializerForClass($class, $tag)) !== null) { - $initializerClass = new ClassReflector($initializer); + $initializerClass = self::reflect($initializer::class); $this->resolveChain()->add($initializerClass); @@ -456,9 +480,7 @@ private function resolveDependency(string $className, string|UnitEnum|null $tag $this->resolveChain()->pop(); } - $singleton = $initializerClass->getAttribute(Singleton::class) ?? $initializerClass->getMethod('initialize')->getAttribute(Singleton::class); - - if ($singleton !== null) { + if (self::initializerSingleton($initializerClass) !== null) { $this->singleton($className, $object, $tag); } @@ -481,7 +503,7 @@ private function resolveDependency(string $className, string|UnitEnum|null $tag return $this->autowire($className, null, ...$params); } - private function initializerForBuiltin(TypeReflector $target, string $tag): ?Initializer + private function initializerForBuiltin(TypeReflector $target, string|UnitEnum $tag): ?Initializer { if ($initializerClass = $this->initializers[$this->resolveTaggedName($target->getName(), $tag)] ?? null) { return $this->resolveInitializer($initializerClass); @@ -556,9 +578,9 @@ private function resolveInitializer(string $initializerClass): Initializer|Dynam private function autowire(string $className, string|UnitEnum|null $tag, mixed ...$params): object { - $classReflector = new ClassReflector($className); + $classReflector = self::reflect($className); - $constructor = $classReflector->getConstructor(); + $constructor = self::constructor($classReflector); if (! $classReflector->isInstantiable()) { throw new DependencyCouldNotBeInstantiated($classReflector, $this->chain); @@ -574,29 +596,25 @@ private function autowire(string $className, string|UnitEnum|null $tag, mixed .. // Otherwise, use our autowireDependencies helper to automagically : $classReflector->newInstanceWithoutConstructor(); - if ( - ! $classReflector->getType()->matches(Initializer::class) - && ! $classReflector->getType()->matches(DynamicInitializer::class) - && $classReflector->hasAttribute(Singleton::class) - ) { + if (self::isAutowiredSingleton($classReflector)) { $this->singleton($className, $instance, $tag); } - foreach ($classReflector->getProperties() as $property) { - $inject = $property->getAttribute(Inject::class); - - if ($inject && ! $property->isInitialized($instance)) { - if ($property->hasAttribute(Proxy::class)) { - $property->set($instance, $property - ->getType() - ->asClass() - ->getReflection() - ->newLazyProxy( - fn () => $this->get($property->getType()->getName(), $inject->tag), - )); - } else { - $property->set($instance, $this->get($property->getType()->getName(), $inject->tag)); - } + foreach (self::injectedProperties($classReflector) as [$property, $inject, $lazy]) { + if ($property->isInitialized($instance)) { + continue; + } + + if ($lazy) { + $property->set($instance, $property + ->getType() + ->asClass() + ->getReflection() + ->newLazyProxy( + fn () => $this->get($property->getType()->getName(), $inject->tag), + )); + } else { + $property->set($instance, $this->get($property->getType()->getName(), $inject->tag)); } } @@ -615,10 +633,11 @@ private function autowireDependencies(MethodReflector|FunctionReflector $method, // Build the class by iterating through its // dependencies and resolving them. try { - foreach ($method->getParameters() as $parameter) { + foreach (self::parameters($method) as [$parameter, $tag, $lazy]) { $dependencies[] = $this->clone()->autowireDependency( parameter: $parameter, - tag: $parameter->getAttribute(Tag::class)?->name, + tag: $tag, + lazy: $lazy, providedValue: $parameters[$parameter->getName()] ?? null, ); } @@ -629,20 +648,18 @@ private function autowireDependencies(MethodReflector|FunctionReflector $method, return $dependencies; } - private function autowireDependency(ParameterReflector $parameter, string|UnitEnum|null $tag, mixed $providedValue = null): mixed + private function autowireDependency(ParameterReflector $parameter, string|UnitEnum|null $tag, bool $lazy, mixed $providedValue = null): mixed { $parameterType = $parameter->getType(); // If the parameter is a built-in type, skip reflection and attempt to provide the value by // tagged initializer, a default value or null value. if ($parameterType->isBuiltin()) { - return $this->autowireBuiltinDependency($parameter, $providedValue); + return $this->autowireBuiltinDependency($parameter, $tag, $providedValue); } - // Support lazy initialization - $lazy = $parameter->hasAttribute(Proxy::class); // Loop through each type until we hit a match. - foreach ($parameter->getType()->split() as $type) { + foreach ($parameterType->split() as $type) { try { return $this->autowireObjectDependency( type: $type, @@ -689,20 +706,15 @@ private function autowireObjectDependency(TypeReflector $type, string|UnitEnum|n return $this->resolve(className: $type->getName(), tag: $tag); } - private function autowireBuiltinDependency(ParameterReflector $parameter, mixed $providedValue): mixed + private function autowireBuiltinDependency(ParameterReflector $parameter, string|UnitEnum|null $tag, mixed $providedValue): mixed { $typeName = $parameter->getType()->getName(); - $tag = $parameter->getAttribute(Tag::class); - - if ($tag !== null && ($initializer = $this->initializerForBuiltin($parameter->getType(), $tag->name))) { - $initializerClass = new ClassReflector($initializer); + if ($tag !== null && ($initializer = $this->initializerForBuiltin($parameter->getType(), $tag))) { $object = $initializer->initialize($this->clone()); - $singleton = $initializerClass->getAttribute(Singleton::class) ?? $initializerClass->getMethod('initialize')->getAttribute(Singleton::class); - - if ($singleton !== null) { - $this->singleton($typeName, $object, $tag->name); + if (self::initializerSingleton(self::reflect($initializer::class)) !== null) { + $this->singleton($typeName, $object, $tag); } return $object; @@ -781,8 +793,7 @@ private function resolveTaggedName(string $className, string|UnitEnum|null $tag) private function resolveDecorator(string $className, mixed $instance, string|UnitEnum|null $tag = null, mixed ...$params): object { foreach ($this->decorators[$className] ?? [] as $decoratorClass) { - $decoratorClassReflector = new ClassReflector($decoratorClass); - $constructor = $decoratorClassReflector->getConstructor(); + $constructor = self::constructor(self::reflect($decoratorClass)); $parameters = $constructor?->getParameters(); // we look for parameter holding decorated instance @@ -809,6 +820,78 @@ private function resolveDecorator(string $className, mixed $instance, string|Uni return $instance; } + private static function reflect(string $className): ClassReflector + { + return self::$classReflectors[$className] ??= new ClassReflector($className); + } + + private static function constructor(ClassReflector $class): ?MethodReflector + { + $className = $class->getName(); + + if (! array_key_exists($className, self::$constructors)) { + self::$constructors[$className] = $class->getConstructor(); + } + + return self::$constructors[$className]; + } + + /** + * @return list Each parameter along with its tag and whether it should be lazy. + */ + private static function parameters(MethodReflector|FunctionReflector $method): array + { + $resolve = static fn (): array => array_map( + static fn (ParameterReflector $parameter): array => [ + $parameter, + $parameter->getAttribute(Tag::class)?->name, + $parameter->hasAttribute(Proxy::class), + ], + iterator_to_array($method->getParameters(), preserve_keys: false), + ); + + // Functions and closures can't be identified by name, so only the parameters of methods are cached. + if (! $method instanceof MethodReflector) { + return $resolve(); + } + + return self::$parameters[$method->getReflection()->class . '::' . $method->getName()] ??= $resolve(); + } + + /** + * @return list Each injected property along with its attribute and whether it should be lazy. + */ + private static function injectedProperties(ClassReflector $class): array + { + return self::$injectedProperties[$class->getName()] ??= array_values(array_filter(array_map( + static function (PropertyReflector $property): ?array { + $inject = $property->getAttribute(Inject::class); + + return $inject === null ? null : [$property, $inject, $property->hasAttribute(Proxy::class)]; + }, + $class->getProperties(), + ))); + } + + private static function isAutowiredSingleton(ClassReflector $class): bool + { + return self::$autowiredSingletons[$class->getName()] ??= + ! $class->getType()->matches(Initializer::class) && ! $class->getType()->matches(DynamicInitializer::class) && $class->hasAttribute(Singleton::class); + } + + private static function initializerSingleton(ClassReflector $initializerClass): ?Singleton + { + $className = $initializerClass->getName(); + + if (! array_key_exists($className, self::$initializerSingletons)) { + self::$initializerSingletons[$className] = $initializerClass->getAttribute(Singleton::class) ?? $initializerClass + ->getMethod('initialize') + ->getAttribute(Singleton::class); + } + + return self::$initializerSingletons[$className]; + } + public function addResettable(string|ClassReflector $resettableClass): Container { if ($resettableClass instanceof ClassReflector) { From 11991117830d4ff64c7d23250a66f3d2d7cf321c Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 23 Oct 2026 13:56:53 +0200 Subject: [PATCH 3/3] perf(container): only create initializer caches once needed Remembering initializer lookups added two objects to every container, which made constructing a container slower and increased memory usage, as shown by the register benchmarks. They are now created once they're first needed, or right before the container is cloned so that clones still share them. Co-Authored-By: Claude Opus 5.5 --- packages/container/src/GenericContainer.php | 39 ++++++++++++--------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/packages/container/src/GenericContainer.php b/packages/container/src/GenericContainer.php index 13a87a4f15..40037bfa2e 100644 --- a/packages/container/src/GenericContainer.php +++ b/packages/container/src/GenericContainer.php @@ -34,12 +34,15 @@ final class GenericContainer implements Container * Which initializer can initialize a given class and tag, or `false` when there is none. Unlike resolved * instances, this is kept when the container is reset, since it only depends on the registered initializers. * - * @var ArrayObject|false> + * These are objects so that they are shared with clones of this container, and are only created once needed, + * to keep constructing a container cheap. + * + * @var ArrayObject|false>|null */ - private ArrayObject $initializerLookups; + private ?ArrayObject $initializerLookups = null; - /** @var ArrayObject */ - private ArrayObject $resolvedInitializers; + /** @var ArrayObject|null */ + private ?ArrayObject $resolvedInitializers = null; /* * Reflection data only depends on class definitions, which can't change at runtime, @@ -88,10 +91,6 @@ public function __construct( private(set) ?DependencyChain $chain = null, ) { - // These are objects, so that they are shared with clones of this container. - $this->initializerLookups = new ArrayObject(); - $this->resolvedInitializers = new ArrayObject(); - $this->singleton(Container::class, $this); $this->singleton(ContainerInterface::class, $this); $this->singleton(GenericContainer::class, $this); @@ -114,7 +113,7 @@ public function setSingletons(array $singletons): self public function setInitializers(array $initializers): self { $this->initializers = new ArrayIterator($initializers); - $this->initializerLookups->exchangeArray([]); + $this->initializerLookups?->exchangeArray([]); return $this; } @@ -122,7 +121,7 @@ public function setInitializers(array $initializers): self public function setDynamicInitializers(array $dynamicInitializers): self { $this->dynamicInitializers = new ArrayIterator($dynamicInitializers); - $this->initializerLookups->exchangeArray([]); + $this->initializerLookups?->exchangeArray([]); return $this; } @@ -351,7 +350,7 @@ public function addInitializer(ClassReflector|string $initializerClass): Contain // First, we check whether this is a DynamicInitializer, // which don't have a one-to-one mapping - $this->initializerLookups->exchangeArray([]); + $this->initializerLookups?->exchangeArray([]); if ($initializerClass->getType()->matches(DynamicInitializer::class)) { $this->dynamicInitializers[] = $initializerClass->getName(); @@ -388,15 +387,15 @@ public function removeInitializer(ClassReflector|string $initializerClass): Cont ); unset($this->dynamicInitializers[$index]); - unset($this->resolvedInitializers[$initializerClass->getName()]); - $this->initializerLookups->exchangeArray([]); + $this->resolvedInitializers?->offsetUnset($initializerClass->getName()); + $this->initializerLookups?->exchangeArray([]); return $this; } unset($this->initializers[$initializerClass->getName()]); - unset($this->resolvedInitializers[$initializerClass->getName()]); - $this->initializerLookups->exchangeArray([]); + $this->resolvedInitializers?->offsetUnset($initializerClass->getName()); + $this->initializerLookups?->exchangeArray([]); return $this; } @@ -518,6 +517,8 @@ private function initializerForClass(ClassReflector $target, string|UnitEnum|nul // so the outcome is remembered for every class and tag. $lookup = $this->resolveTaggedName($target->getName(), $tag); + $this->initializerLookups ??= new ArrayObject(); + if (! isset($this->initializerLookups[$lookup])) { $this->initializerLookups[$lookup] = $this->findInitializerClass($target, $tag) ?? false; } @@ -573,6 +574,8 @@ private function findInitializerClass(ClassReflector $target, string|UnitEnum|nu */ private function resolveInitializer(string $initializerClass): Initializer|DynamicInitializer { + $this->resolvedInitializers ??= new ArrayObject(); + return $this->resolvedInitializers[$initializerClass] ??= $this->resolve($initializerClass); } @@ -752,6 +755,10 @@ private function autowireBuiltinDependency(ParameterReflector $parameter, string private function clone(): self { + // Created before cloning, so that the clone shares them instead of creating its own. + $this->initializerLookups ??= new ArrayObject(); + $this->resolvedInitializers ??= new ArrayObject(); + return clone $this; } @@ -906,7 +913,7 @@ public function addResettable(string|ClassReflector $resettableClass): Container public function reset(): self { $this->resolvedSingletons = new ArrayIterator(); - $this->resolvedInitializers->exchangeArray([]); + $this->resolvedInitializers?->exchangeArray([]); foreach ($this->resettables as $resettableClass) { /** @var Resettable $resettable */