Skip to content

Commit

Permalink
feature #49291 [Serializer] Add methods getSupportedTypes to allow …
Browse files Browse the repository at this point in the history
…better performance (tucksaun, nicolas-grekas)

This PR was merged into the 6.3 branch.

Discussion
----------

[Serializer] Add methods `getSupportedTypes` to allow better performance

| Q             | A
| ------------- | ---
| Branch?       | 6.3
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | yes (new method in the interfaces, one interface deprecated)
| License       | MIT
| Doc PR        | to be written

The PRs allows normalizers or denormalizers to expose their supported types and the associated cacheability to the Serializer.
With this info,  even if the `supports*` call is not cacheable, the Serializer can skip a ton of method calls to `supports*` improving performance substaintially in some cases:
![Screenshot 2023-02-02 at 15 46 49](https://user-images.githubusercontent.com/870118/217378926-03aa77e8-d80e-4bdd-b5dc-acc3602b70b3.png)

<details>
 <summary>I found this design while working on a customer project performance (a big app built around API Platform): we reached the point where the slowest part of main application endpoint was `Symfony\Component\Serializer\Serializer::getNormalizer`.</summary>

After some digging, we found out we were experiencing the conjunction of two phenomenons:
- the application is quite complex and returns deep nested and repeating structures, exposing the underlying bottleneck;
- and a lot of custom non-cacheable normalizers.
Because most of the normalizers are not cacheable, the Serializer has to call every normalizer over and over again leading `getNormalizer` to account for 20% of the total wall time:
![Screenshot 2023-02-07 at 16 56 02](https://user-images.githubusercontent.com/870118/217375680-e7d33db2-fd6a-4ef0-b8d0-34d0eac8cf09.png)

We first tried to improve cacheability based on context without much success, then an approach similar to #45779 with some success but still feeling this could be faster.
We then thought that even if the `supportsNormalization` could not be cached (because of the context), maybe we could avoid the calls at the origin by letting the `Normalizers` expose the types they support and came to this PR with pretty good results.
</details>

The perfornance improvement was only measured by adapting Symfony's normalizers as well as the project ones, proper third party normalizers updates should improve performance even more.

This should effectively replaces the `CacheableSupportsMethodInterface` as the cacheability can now be returned by `getSupportedTypes`.

Commits
-------

e5af24a [Serializer] Add wildcard support to getSupportedTypes()
400685a [Serializer] Add methods `getSupportedTypes` to allow better performance
  • Loading branch information
fabpot committed Mar 10, 2023
2 parents a7e0b05 + e5af24a commit 58052e9
Show file tree
Hide file tree
Showing 41 changed files with 619 additions and 78 deletions.
54 changes: 23 additions & 31 deletions .github/expected-missing-return-types.diff
Original file line number Diff line number Diff line change
Expand Up @@ -8130,6 +8130,7 @@ index 1924b1ddb0..62c58c8e8b 100644
$annotatedClasses = [];
diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/ControllerArgumentValueResolverPass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/ControllerArgumentValueResolverPass.php
index dff3e248ae..381db9aa8f 100644
index 6e00840c7e..8e69c81c23 100644
--- a/src/Symfony/Component/HttpKernel/DependencyInjection/ControllerArgumentValueResolverPass.php
+++ b/src/Symfony/Component/HttpKernel/DependencyInjection/ControllerArgumentValueResolverPass.php
@@ -34,5 +34,5 @@ class ControllerArgumentValueResolverPass implements CompilerPassInterface
Expand Down Expand Up @@ -11254,32 +11255,32 @@ index fc6336ebdb..e13a834930 100644
{
if (1 > \func_num_args()) {
diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
index 52e985815b..e7d0493152 100644
index 7d138b0b26..03e28f9d20 100644
--- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
@@ -210,5 +210,5 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
@@ -215,5 +215,5 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
* @throws LogicException if the 'allow_extra_attributes' context variable is false and no class metadata factory is provided
*/
- protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false)
+ protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
{
$allowExtraAttributes = $context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES];
@@ -260,5 +260,5 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
@@ -265,5 +265,5 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
* @return bool
*/
- protected function isAllowedAttribute(object|string $classOrObject, string $attribute, string $format = null, array $context = [])
+ protected function isAllowedAttribute(object|string $classOrObject, string $attribute, string $format = null, array $context = []): bool
{
$ignoredAttributes = $context[self::IGNORED_ATTRIBUTES] ?? $this->defaultContext[self::IGNORED_ATTRIBUTES];
@@ -311,5 +311,5 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
@@ -316,5 +316,5 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
* @throws MissingConstructorArgumentException
*/
- protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, array|bool $allowedAttributes, string $format = null)
+ protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, array|bool $allowedAttributes, string $format = null): object
{
if (null !== $object = $this->extractObjectToPopulate($class, $context, self::OBJECT_TO_POPULATE)) {
diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
index a02a46b941..aedfd67c2e 100644
index 75fe3a5cb1..a28dd40568 100644
--- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
@@ -139,10 +139,10 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
Expand Down Expand Up @@ -11321,46 +11322,36 @@ index a02a46b941..aedfd67c2e 100644
- public function supportsDenormalization(mixed $data, string $type, string $format = null /* , array $context = [] */)
+ public function supportsDenormalization(mixed $data, string $type, string $format = null /* , array $context = [] */): bool
{
return class_exists($type) || (interface_exists($type, false) && $this->classDiscriminatorResolver && null !== $this->classDiscriminatorResolver->getMappingForClass($type));
return class_exists($type) || (interface_exists($type, false) && null !== $this->classDiscriminatorResolver?->getMappingForClass($type));
}

- public function denormalize(mixed $data, string $type, string $format = null, array $context = [])
+ public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
if (!isset($context['cache_key'])) {
diff --git a/src/Symfony/Component/Serializer/Normalizer/DenormalizerAwareTrait.php b/src/Symfony/Component/Serializer/Normalizer/DenormalizerAwareTrait.php
index c5cc86ecf6..c65534fafb 100644
--- a/src/Symfony/Component/Serializer/Normalizer/DenormalizerAwareTrait.php
+++ b/src/Symfony/Component/Serializer/Normalizer/DenormalizerAwareTrait.php
@@ -25,5 +25,5 @@ trait DenormalizerAwareTrait
* @return void
*/
- public function setDenormalizer(DenormalizerInterface $denormalizer)
+ public function setDenormalizer(DenormalizerInterface $denormalizer): void
{
$this->denormalizer = $denormalizer;
diff --git a/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php b/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php
index 1786d6fff1..04a2e62ed2 100644
index 1d83b2da11..1c632f42bf 100644
--- a/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php
+++ b/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php
@@ -45,5 +45,5 @@ interface DenormalizerInterface
@@ -47,5 +47,5 @@ interface DenormalizerInterface
* @throws ExceptionInterface Occurs for all the other cases of errors
*/
- public function denormalize(mixed $data, string $type, string $format = null, array $context = []);
+ public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed;

/**
@@ -57,4 +57,4 @@ interface DenormalizerInterface
@@ -64,5 +64,5 @@ interface DenormalizerInterface
* @return bool
*/
- public function supportsDenormalization(mixed $data, string $type, string $format = null /* , array $context = [] */);
+ public function supportsDenormalization(mixed $data, string $type, string $format = null /* , array $context = [] */): bool;
}

/**
diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php
index e08dd5d9ec..cc282ae4bb 100644
index 2719c8b52c..1112f7f3cc 100644
--- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php
@@ -138,5 +138,5 @@ class GetSetMethodNormalizer extends AbstractObjectNormalizer
@@ -148,5 +148,5 @@ class GetSetMethodNormalizer extends AbstractObjectNormalizer
* @return void
*/
- protected function setAttributeValue(object $object, string $attribute, mixed $value, string $format = null, array $context = [])
Expand All @@ -11379,38 +11370,39 @@ index 40a4fa0e8c..a1e2749aae 100644
{
$this->normalizer = $normalizer;
diff --git a/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php b/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php
index cb43d78cc7..d215ffe997 100644
index d6d0707ff5..9953ad3005 100644
--- a/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php
+++ b/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php
@@ -37,5 +37,5 @@ interface NormalizerInterface
@@ -39,5 +39,5 @@ interface NormalizerInterface
* @throws ExceptionInterface Occurs for all the other cases of errors
*/
- public function normalize(mixed $object, string $format = null, array $context = []);
+ public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null;

/**
@@ -48,4 +48,4 @@ interface NormalizerInterface
@@ -55,5 +55,5 @@ interface NormalizerInterface
* @return bool
*/
- public function supportsNormalization(mixed $data, string $format = null /* , array $context = [] */);
+ public function supportsNormalization(mixed $data, string $format = null /* , array $context = [] */): bool;
}

/**
diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
index 8018cb7a49..aa06b9c50b 100644
index 140e89c6a1..f77348252b 100644
--- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
@@ -133,5 +133,5 @@ class ObjectNormalizer extends AbstractObjectNormalizer
@@ -143,5 +143,5 @@ class ObjectNormalizer extends AbstractObjectNormalizer
* @return void
*/
- protected function setAttributeValue(object $object, string $attribute, mixed $value, string $format = null, array $context = [])
+ protected function setAttributeValue(object $object, string $attribute, mixed $value, string $format = null, array $context = []): void
{
try {
diff --git a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php
index 3dd734055d..cbc0e86d27 100644
index 645ba74290..d960bf4b20 100644
--- a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php
@@ -175,5 +175,5 @@ class PropertyNormalizer extends AbstractObjectNormalizer
@@ -185,5 +185,5 @@ class PropertyNormalizer extends AbstractObjectNormalizer
* @return void
*/
- protected function setAttributeValue(object $object, string $attribute, mixed $value, string $format = null, array $context = [])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ public function normalize(mixed $object, string $format = null, array $context =
return $normalized;
}

public function getSupportedTypes(?string $format): array
{
return [
FlattenException::class => false,
];
}

public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
{
return $data instanceof FlattenException && ($context[Serializer::MESSENGER_SERIALIZATION_CONTEXT] ?? false);
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Component/Serializer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ CHANGELOG

* Add `XmlEncoder::SAVE_OPTIONS` context option
* Add `BackedEnumNormalizer::ALLOW_INVALID_VALUES` context option
* Add method `getSupportedTypes(?string $format)` to `NormalizerInterface` and `DenormalizerInterface`
* Deprecate `MissingConstructorArgumentsException` in favor of `MissingConstructorArgumentException`
* Deprecate `CacheableSupportsMethodInterface` in favor of the new `getSupportedTypes(?string $format)` methods

6.2
---
Expand Down
18 changes: 18 additions & 0 deletions src/Symfony/Component/Serializer/Debug/TraceableNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ public function __construct(
private NormalizerInterface|DenormalizerInterface $normalizer,
private SerializerDataCollector $dataCollector,
) {
if (!method_exists($normalizer, 'getSupportedTypes')) {
trigger_deprecation('symfony/serializer', '6.3', 'Not implementing the "NormalizerInterface::getSupportedTypes()" in "%s" is deprecated.', get_debug_type($normalizer));
}
}

public function getSupportedTypes(?string $format): array
{
// @deprecated remove condition in 7.0
if (!method_exists($this->normalizer, 'getSupportedTypes')) {
return ['*' => $this->normalizer instanceof CacheableSupportsMethodInterface && $this->normalizer->hasCacheableSupportsMethod()];
}

return $this->normalizer->getSupportedTypes($format);
}

public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
Expand Down Expand Up @@ -114,8 +127,13 @@ public function setDenormalizer(DenormalizerInterface $denormalizer): void
$this->normalizer->setDenormalizer($denormalizer);
}

/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', __METHOD__);

return $this->normalizer instanceof CacheableSupportsMethodInterface && $this->normalizer->hasCacheableSupportsMethod();
}

Expand Down
19 changes: 15 additions & 4 deletions src/Symfony/Component/Serializer/Debug/TraceableSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Symfony\Component\Serializer\DataCollector\SerializerDataCollector;
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
Expand All @@ -29,13 +30,13 @@ class TraceableSerializer implements SerializerInterface, NormalizerInterface, D
{
public const DEBUG_TRACE_ID = 'debug_trace_id';

/**
* @param SerializerInterface&NormalizerInterface&DenormalizerInterface&EncoderInterface&DecoderInterface $serializer
*/
public function __construct(
private SerializerInterface $serializer,
private SerializerInterface&NormalizerInterface&DenormalizerInterface&EncoderInterface&DecoderInterface $serializer,
private SerializerDataCollector $dataCollector,
) {
if (!method_exists($serializer, 'getSupportedTypes')) {
trigger_deprecation('symfony/serializer', '6.3', 'Not implementing the "NormalizerInterface::getSupportedTypes()" in "%s" is deprecated.', get_debug_type($serializer));
}
}

public function serialize(mixed $data, string $format, array $context = []): string
Expand Down Expand Up @@ -128,6 +129,16 @@ public function decode(string $data, string $format, array $context = []): mixed
return $result;
}

public function getSupportedTypes(?string $format): array
{
// @deprecated remove condition in 7.0
if (!method_exists($this->serializer, 'getSupportedTypes')) {
return ['*' => $this->serializer instanceof CacheableSupportsMethodInterface && $this->serializer->hasCacheableSupportsMethod()];
}

return $this->serializer->getSupportedTypes($format);
}

public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
{
return $this->serializer->supportsNormalization($data, $format, $context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,13 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory
}
}

/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', __METHOD__);

return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ abstract protected function getAttributeValue(object $object, string $attribute,
*/
public function supportsDenormalization(mixed $data, string $type, string $format = null /* , array $context = [] */)
{
return class_exists($type) || (interface_exists($type, false) && $this->classDiscriminatorResolver && null !== $this->classDiscriminatorResolver->getMappingForClass($type));
return class_exists($type) || (interface_exists($type, false) && null !== $this->classDiscriminatorResolver?->getMappingForClass($type));
}

public function denormalize(mixed $data, string $type, string $format = null, array $context = [])
Expand Down
24 changes: 24 additions & 0 deletions src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@ class ArrayDenormalizer implements ContextAwareDenormalizerInterface, Denormaliz
{
use DenormalizerAwareTrait;

public function setDenormalizer(DenormalizerInterface $denormalizer): void
{
if (!method_exists($denormalizer, 'getSupportedTypes')) {
trigger_deprecation('symfony/serializer', '6.3', 'Not implementing the "DenormalizerInterface::getSupportedTypes()" in "%s" is deprecated.', get_debug_type($denormalizer));
}

$this->denormalizer = $denormalizer;
}

public function getSupportedTypes(?string $format): array
{
// @deprecated remove condition in 7.0
if (!method_exists($this->denormalizer, 'getSupportedTypes')) {
return ['*' => $this->denormalizer instanceof CacheableSupportsMethodInterface && $this->denormalizer->hasCacheableSupportsMethod()];
}

return $this->denormalizer->getSupportedTypes($format);
}

/**
* @throws NotNormalizableValueException
*/
Expand Down Expand Up @@ -69,8 +88,13 @@ public function supportsDenormalization(mixed $data, string $type, string $forma
&& $this->denormalizer->supportsDenormalization($data, substr($type, 0, -2), $format, $context);
}

/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', __METHOD__);

return $this->denormalizer instanceof CacheableSupportsMethodInterface && $this->denormalizer->hasCacheableSupportsMethod();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ final class BackedEnumNormalizer implements NormalizerInterface, DenormalizerInt
*/
public const ALLOW_INVALID_VALUES = 'allow_invalid_values';

public function getSupportedTypes(?string $format): array
{
return [
\BackedEnum::class => true,
];
}

public function normalize(mixed $object, string $format = null, array $context = []): int|string
{
if (!$object instanceof \BackedEnum) {
Expand Down Expand Up @@ -78,8 +85,13 @@ public function supportsDenormalization(mixed $data, string $type, string $forma
return is_subclass_of($type, \BackedEnum::class);
}

/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', __METHOD__);

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
* supports*() methods will be cached by type and format.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @deprecated since Symfony 6.3, implement "getSupportedTypes(?string $format)" instead
*/
interface CacheableSupportsMethodInterface
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ public function __construct(array $defaultContext = [], NameConverterInterface $
$this->nameConverter = $nameConverter;
}

public function getSupportedTypes(?string $format): array
{
return [
ConstraintViolationListInterface::class => __CLASS__ === static::class || $this->hasCacheableSupportsMethod(),
];
}

public function normalize(mixed $object, string $format = null, array $context = []): array
{
if (\array_key_exists(self::PAYLOAD_FIELDS, $context)) {
Expand Down Expand Up @@ -109,8 +116,13 @@ public function supportsNormalization(mixed $data, string $format = null /* , ar
return $data instanceof ConstraintViolationListInterface;
}

/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', __METHOD__);

return __CLASS__ === static::class;
}
}
Loading

0 comments on commit 58052e9

Please sign in to comment.