Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance optimization #387

Merged
merged 28 commits into from
Apr 4, 2025
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3df3fb7
Optimize Container::has() method performance by checking definitions …
samdark Feb 9, 2025
f18c1c6
Optimize Container::get() method performance
samdark Feb 9, 2025
f97bd53
Optimize Container::validateDefinition() method performance
samdark Feb 9, 2025
3c367d3
Optimize Container::build() method performance
samdark Feb 9, 2025
ca8064e
Optimize Container::get() method performance
samdark Feb 9, 2025
bf3fadc
Optimize Container::addDefinition() method performance
samdark Feb 9, 2025
2f99923
Apply fixes from StyleCI
StyleCIBot Feb 9, 2025
72889e7
fix
samdark Feb 12, 2025
c890b27
cleanup
samdark Feb 12, 2025
77b916e
fix
samdark Feb 12, 2025
69a4841
Remove unnecessary check
samdark Feb 12, 2025
316cb53
changelog
samdark Feb 12, 2025
45df64a
Fixes
samdark Mar 4, 2025
c11b58f
Fixes
samdark Mar 4, 2025
0b13705
Fixes
samdark Mar 4, 2025
783df5f
Fix
samdark Mar 4, 2025
b08ac37
fix psalm
vjik Mar 16, 2025
562c7c1
fix
vjik Mar 16, 2025
8b6639d
Merge branch 'master' into performance
samdark Mar 20, 2025
e4e3cd7
Fix
samdark Mar 20, 2025
2bacd57
Simplify state resetter detection
samdark Mar 25, 2025
81abc51
Apply fixes from StyleCI
StyleCIBot Mar 25, 2025
c654800
Keep comments
samdark Mar 25, 2025
696b19f
Remove delegates from has()
samdark Mar 26, 2025
d4356e7
Merge remote-tracking branch 'origin/master' into performance
vjik Apr 4, 2025
723fdd0
`Container` tests
vjik Apr 4, 2025
39b66c4
`CompositeContainer` tests
vjik Apr 4, 2025
ec2b161
Apply fixes from StyleCI
StyleCIBot Apr 4, 2025
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.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- New #380: Add `TagReference::id()` method (@vjik)
- Enh #384: Make `$config` parameter in `Container` constructor optional (@np25071984)
- Enh #324: Make `BuildingException` and `NotFoundException` friendly (@np25071984)
- Enh #387: Improve container performance (@samdark)

## 1.3.0 October 14, 2024

Expand Down
206 changes: 123 additions & 83 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
use Yiisoft\Di\Helpers\DefinitionParser;
use Yiisoft\Di\Reference\TagReference;

use function array_key_exists;
use function array_keys;
use function implode;
use function in_array;
Expand Down Expand Up @@ -69,6 +68,16 @@
private array $resetters = [];
private bool $useResettersFromMeta = true;

/**
* @var array<string, mixed> Normalized definitions cache.
*/
private array $normalizedDefinitions = [];

/**
* @var int Number of normalized definitions before the cache is cleared.
*/
private const MAX_NORMALIZED_DEFINITIONS = 100;

/**
* @param ContainerConfigInterface $config Container configuration.
*
Expand Down Expand Up @@ -103,16 +112,20 @@
*/
public function has(string $id): bool
{
try {
if ($this->definitions->has($id)) {
return true;
}
} catch (CircularReferenceException) {
return true;
}

if (TagReference::isTagAlias($id)) {
$tag = TagReference::extractTagFromAlias($id);
return isset($this->tags[$tag]);
}

try {
return $this->definitions->has($id);
} catch (CircularReferenceException) {
return true;
}
return false;
}

/**
Expand All @@ -136,65 +149,79 @@
*/
public function get(string $id)
{
if (!array_key_exists($id, $this->instances)) {
try {
try {
$this->instances[$id] = $this->build($id);
} catch (NotFoundExceptionInterface $exception) {
if (!$this->delegates->has($id)) {
if ($exception instanceof NotFoundException) {
if ($id !== $exception->getId()) {
$buildStack = $exception->getBuildStack();
array_unshift($buildStack, $id);
throw new NotFoundException($exception->getId(), $buildStack);
}
throw $exception;
}
throw new NotFoundException($id, [$id], previous: $exception);
}
// Fast path: check if instance exists.
if (isset($this->instances[$id])) {
return $id === StateResetter::class ? $this->prepareStateResetter($id) : $this->instances[$id];

Check failure on line 154 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.1-ubuntu-latest

MixedReturnStatement

src/Container.php:154:20: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)

Check failure on line 154 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.2-ubuntu-latest

MixedReturnStatement

src/Container.php:154:20: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)

Check failure on line 154 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.3-ubuntu-latest

MixedReturnStatement

src/Container.php:154:20: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)
}

/** @psalm-suppress MixedReturnStatement */
return $this->delegates->get($id);
}
} catch (Throwable $e) {
if ($e instanceof ContainerExceptionInterface && !$e instanceof InvalidConfigException) {
throw $e;
}
throw new BuildingException($id, $e, $this->definitions->getBuildStack(), $e);
try {
$this->instances[$id] = $this->build($id);
} catch (NotFoundException $exception) {
// Fast path: if the exception ID matches the requested ID, no need to modify stack.
if ($exception->getId() === $id) {
// Try delegates before giving up.
return $this->delegates->has($id) ? $this->delegates->get($id) : throw $exception;

Check failure on line 163 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.1-ubuntu-latest

MixedReturnStatement

src/Container.php:163:24: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)

Check failure on line 163 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.2-ubuntu-latest

MixedReturnStatement

src/Container.php:163:24: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)

Check failure on line 163 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.3-ubuntu-latest

MixedReturnStatement

src/Container.php:163:24: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)
}

// Add current ID to build stack for better error reporting.
$buildStack = $exception->getBuildStack();
array_unshift($buildStack, $id);
throw new NotFoundException($exception->getId(), $buildStack);
} catch (NotFoundExceptionInterface $exception) {
// Try delegates before giving up
return $this->delegates->has($id) ? $this->delegates->get($id) : throw new NotFoundException($id, [$id], previous: $exception);

Check failure on line 172 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.1-ubuntu-latest

MixedReturnStatement

src/Container.php:172:20: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)

Check failure on line 172 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.2-ubuntu-latest

MixedReturnStatement

src/Container.php:172:20: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)

Check failure on line 172 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.3-ubuntu-latest

MixedReturnStatement

src/Container.php:172:20: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)
} catch (ContainerExceptionInterface $e) {
if (!$e instanceof InvalidConfigException) {
throw $e;
}
throw new BuildingException($id, $e, $this->definitions->getBuildStack(), $e);
} catch (Throwable $e) {
throw new BuildingException($id, $e, $this->definitions->getBuildStack(), $e);
}

// Handle StateResetter for newly built instances.
if ($id === StateResetter::class) {
$delegatesResetter = null;
if ($this->delegates->has(StateResetter::class)) {
$delegatesResetter = $this->delegates->get(StateResetter::class);
}
return $this->prepareStateResetter($id);

Check failure on line 184 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.1-ubuntu-latest

MixedReturnStatement

src/Container.php:184:20: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)

Check failure on line 184 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.2-ubuntu-latest

MixedReturnStatement

src/Container.php:184:20: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)

Check failure on line 184 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.3-ubuntu-latest

MixedReturnStatement

src/Container.php:184:20: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)
}

/** @var StateResetter $mainResetter */
$mainResetter = $this->instances[$id];
/** @psalm-suppress MixedReturnStatement */
return $this->instances[$id];
}

if ($this->useResettersFromMeta) {
/** @var StateResetter[] $resetters */
$resetters = [];
foreach ($this->resetters as $serviceId => $callback) {
if (isset($this->instances[$serviceId])) {
$resetters[$serviceId] = $callback;
}
}
if ($delegatesResetter !== null) {
$resetters[] = $delegatesResetter;
}
$mainResetter->setResetters($resetters);
} elseif ($delegatesResetter !== null) {
$resetter = new StateResetter($this->get(ContainerInterface::class));
$resetter->setResetters([$mainResetter, $delegatesResetter]);
/**
* @param string $id
* @return mixed
*/
private function prepareStateResetter(string $id)
{
$delegatesResetter = null;
if ($this->delegates->has(StateResetter::class)) {
$delegatesResetter = $this->delegates->get(StateResetter::class);
}

/** @var StateResetter $mainResetter */
$mainResetter = $this->instances[$id];

return $resetter;
if ($this->useResettersFromMeta) {
/** @var StateResetter[] $resetters */
$resetters = [];
foreach ($this->resetters as $serviceId => $callback) {
if (isset($this->instances[$serviceId])) {
$resetters[$serviceId] = $callback;
}
}
if ($delegatesResetter !== null) {
$resetters[] = $delegatesResetter;
}
$mainResetter->setResetters($resetters);
} elseif ($delegatesResetter !== null) {
$resetter = new StateResetter($this->get(ContainerInterface::class));
$resetter->setResetters([$mainResetter, $delegatesResetter]);

return $resetter;
}

/** @psalm-suppress MixedReturnStatement */
return $this->instances[$id];
return $mainResetter;
}

/**
Expand All @@ -212,12 +239,16 @@
[$definition, $meta] = DefinitionParser::parse($definition);
if ($this->validate) {
$this->validateDefinition($definition, $id);
$this->validateMeta($meta);
// Only validate meta if it's not empty.
if ($meta !== []) {
$this->validateMeta($meta);
}
}
/**
* @psalm-var array{reset?:Closure,tags?:string[]} $meta
*/

// Process meta only if it has tags or reset callback.
if (isset($meta[self::META_TAGS])) {
$this->setDefinitionTags($id, $meta[self::META_TAGS]);
}
Expand All @@ -226,6 +257,7 @@
}

unset($this->instances[$id]);

$this->addDefinitionToStorage($id, $definition);
}

Expand Down Expand Up @@ -295,26 +327,24 @@
*/
private function validateDefinition(mixed $definition, ?string $id = null): void
{
if (is_array($definition) && isset($definition[DefinitionParser::IS_PREPARED_ARRAY_DEFINITION_DATA])) {
$class = $definition['class'];
$constructorArguments = $definition['__construct()'];

/**
* @var array $methodsAndProperties Is always array for prepared array definition data.
*
* @see DefinitionParser::parse()
*/
$methodsAndProperties = $definition['methodsAndProperties'];

$definition = array_merge(
$class === null ? [] : [ArrayDefinition::CLASS_NAME => $class],
[ArrayDefinition::CONSTRUCTOR => $constructorArguments],
// extract only value from parsed definition method
array_map(static fn (array $data): mixed => $data[2], $methodsAndProperties),
);
// Skip validation for common simple cases.
if (is_string($definition) || $definition instanceof ContainerInterface || $definition instanceof Closure) {
return;
}

if ($definition instanceof ExtensibleService) {
if (is_array($definition)) {
if (isset($definition[DefinitionParser::IS_PREPARED_ARRAY_DEFINITION_DATA])) {
$class = $definition['class'];
$constructorArguments = $definition['__construct()'];
$methodsAndProperties = $definition['methodsAndProperties'];

$definition = array_merge(
$class === null ? [] : [ArrayDefinition::CLASS_NAME => $class],
[ArrayDefinition::CONSTRUCTOR => $constructorArguments],
array_map(static fn (array $data): mixed => $data[2], $methodsAndProperties),

Check failure on line 344 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.1-ubuntu-latest

MixedArgument

src/Container.php:344:75: MixedArgument: Argument 2 of array_map cannot be mixed, expecting array<array-key, mixed> (see https://psalm.dev/030)

Check failure on line 344 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.2-ubuntu-latest

MixedArgument

src/Container.php:344:75: MixedArgument: Argument 2 of array_map cannot be mixed, expecting array<array-key, mixed> (see https://psalm.dev/030)

Check failure on line 344 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.3-ubuntu-latest

MixedArgument

src/Container.php:344:75: MixedArgument: Argument 2 of array_map cannot be mixed, expecting array<array-key, mixed> (see https://psalm.dev/030)
);
}
} elseif ($definition instanceof ExtensibleService) {
throw new InvalidConfigException(
'Invalid definition. ExtensibleService is only allowed in provider extensions.'
);
Expand Down Expand Up @@ -463,7 +493,7 @@
/**
* Creates new instance by either interface name or alias.
*
* @param string $id The interface or an alias name that was previously registered.
* @param string $id The interface or the alias name that was previously registered.
*
* @throws InvalidConfigException
* @throws NotFoundExceptionInterface
Expand All @@ -475,10 +505,7 @@
*/
private function build(string $id)
{
if (TagReference::isTagAlias($id)) {
return $this->getTaggedServices($id);
}

// Fast path: check for circular reference first as it's the most critical.
if (isset($this->building[$id])) {
if ($id === ContainerInterface::class) {
return $this;
Expand All @@ -492,15 +519,28 @@
);
}

// Less common case: tag alias.
if (TagReference::isTagAlias($id)) {
return $this->getTaggedServices($id);
}

// Check if the definition exists.
if (!$this->definitions->has($id)) {
throw new NotFoundException($id, $this->definitions->getBuildStack());
}

$this->building[$id] = 1;
try {
if (!$this->definitions->has($id)) {
throw new NotFoundException($id, $this->definitions->getBuildStack());
// Use cached normalized definition if available.
if (!isset($this->normalizedDefinitions[$id])) {
// Clear cache if it gets too large to prevent memory issues.
if (count($this->normalizedDefinitions) >= self::MAX_NORMALIZED_DEFINITIONS) {
$this->normalizedDefinitions = [];

Check warning on line 538 in src/Container.php

View check run for this annotation

Codecov / codecov/patch

src/Container.php#L538

Added line #L538 was not covered by tests
}
$this->normalizedDefinitions[$id] = DefinitionNormalizer::normalize($this->definitions->get($id), $id);
}

$definition = DefinitionNormalizer::normalize($this->definitions->get($id), $id);

$object = $definition->resolve($this->get(ContainerInterface::class));
$object = $this->normalizedDefinitions[$id]->resolve($this->get(ContainerInterface::class));

Check failure on line 543 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.1-ubuntu-latest

MixedMethodCall

src/Container.php:543:58: MixedMethodCall: Cannot determine the type of $this->normalizedDefinitions[$id] when calling method resolve (see https://psalm.dev/015)

Check failure on line 543 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.2-ubuntu-latest

MixedMethodCall

src/Container.php:543:58: MixedMethodCall: Cannot determine the type of $this->normalizedDefinitions[$id] when calling method resolve (see https://psalm.dev/015)

Check failure on line 543 in src/Container.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.3-ubuntu-latest

MixedMethodCall

src/Container.php:543:58: MixedMethodCall: Cannot determine the type of $this->normalizedDefinitions[$id] when calling method resolve (see https://psalm.dev/015)
} finally {
unset($this->building[$id]);
}
Expand Down
Loading