Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork9.7k
[Serializer] Cache the normalizer to use when possible#27049
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Conversation
| $cacheable =true; | ||
| foreach ($this->normalizersas$normalizer) { | ||
| if ($cacheable) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
think this needs to be inside the$normalizer instanceof NormalizerInterface check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
No, if a normalizer in the chain before the matching one doesn't implement this new interface, we cannot use the cache (or it will be a BC break).
bendaviesApr 25, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
but we are only interested inNormalizerInterface normalizers here? some only implementDenormalizerInterface likeArrayDenormalizer? i'm probably wrong...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Ok got it, sorry.
sroze commentedApr 25, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Do you have some nice Blackfire profile to show us the difference? 😃 |
bendavies commentedApr 25, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
For me this only gave a 9% speed up, as |
| privatefunctiongetNormalizer($data,$format,array$context) | ||
| { | ||
| if (\is_object($data) &&isset($this->normalizerCache[\get_class($data)][$format])) { | ||
| return$this->normalizerCache[\get_class($data)][$format]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
There will be some BC break here, let's imagine two normalizers with same class and format where:
- The first one (having a high priority) vary on data so not cacheable
- The second one (having a lower priorirty) vary on class only, so is cacheable
When a data will get the second one, then it will have more priority than the first one (as it will be cached and returned here everytime).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
No because we don't cache anything if one of the normalizer in the chain before the one that marches is not cacheable (even if the normalizer finally used is cacheable).
dunglas commentedApr 25, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@bendavies I added support for primitive types and excluded denormalizers. Can you try again? |
dunglas commentedApr 25, 2018
Here is a Blackfire comparison:https://blackfire.io/profiles/compare/ca1d8a07-335f-4a37-8ea7-369f3983d044...0563674b-fc0e-4ab4-9e6d-1b9a089651ad/graph?settings%5Bdimension%5D=wt&settings%5Bdisplay%5D=landscape&settings%5BtabPane%5D=nodes&selected=&callname=main() Improvement of 9.27%. I used an adapted version of@egeloen'sbenchmark. The gain will probably be bigger on real projects (especially API Platform ones) where a lot of normalizers are registered. Here is the adaptation: $this->serializer = new Serializer( [+ new DataUriNormalizer(),+ new DateTimeNormalizer(),+ new DateIntervalNormalizer(),+ new ConstraintViolationListNormalizer(),+ new JsonSerializableNormalizer(), new GetSetMethodNormalizer($classMetadataFactory) ], [new JsonEncoder()] ); } And the command used: blackfire run bin/benchmark -i100 -sSymfonyGetSetNormalizer --iteration 100 --vertical-complexity 4 --horizontal-complexity 4 |
dunglas commentedApr 25, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Here is a profile from of a real project:https://blackfire.io/profiles/compare/a5c802f8-5c45-4d06-ae6f-ed63b4f23b8a/graph (provided by@bendavies) 36% for the whole app! |
| */ | ||
| privatefunctiongetNormalizer($data,$format,array$context) | ||
| { | ||
| $type =\is_object($data) ?\get_class($data) :\gettype($data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This could lead to buggy behavior sincegettype can return strings that are valid class names (https://3v4l.org/bAJiO).
(this would be a really hard to get edge-case but still possible).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Ok i'll add a prefix for classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
done
| * | ||
| * @author Kévin Dunglas <dunglas@gmail.com> | ||
| */ | ||
| interface NormalizerWithCacheableSupportsMethod |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
What would you think about renamingNormalizerWithCacheableSupportsMethod toCacheableNormalizer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
It's thesupports method that is cacheable not the normalizer itself if I'm not mistaking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
CacheableNormalizer is misleading (the result of the normalization isn't cacheable itself, it's only the call to thesupports method).
But the performance improvement is so huge that I think caching should be the default behavior, and disabling the cache should be opt-out. Having a normalizer with asupports* method result that isn't cacheable is a edge case.
I would like to make the cache opt-out instead of opt-in, but it will be a BC break... On the other hand it improves DX and performance for everybody.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
MissingInterface suffix.
What aboutCacheableSupportsInterface
I'd also add the supports method in the interface if possible.
Also, what about using constants for possible values?
VARY_BY_TYPE, etc.?
| */ | ||
| publicfunctionvaryBy():array | ||
| { | ||
| returnarray('type'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
shouldn't we use short array notation here, as PHP 7.1 is the min version for 4.x?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Nop, this has been discussed numerous times already and we keep the sort array notation for consistency 🙃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
(s/short/long/)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
It'll be fixed in Symfony 5, right? 😂😂😂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
It'll be reconsidered at last when 3.4 will be EOLed I suppose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
when 2.8 gets EOLed. 3.4 is not an issue. We can migrate it as it supports PHP 5.5+
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Indeed!
| * | ||
| * @author Kévin Dunglas <dunglas@gmail.com> | ||
| */ | ||
| interface NormalizerWithCacheableSupportsMethod |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
MissingInterface suffix.
What aboutCacheableSupportsInterface
I'd also add the supports method in the interface if possible.
Also, what about using constants for possible values?
VARY_BY_TYPE, etc.?
| */ | ||
| privatefunctiongetNormalizer($data,$format,array$context) | ||
| { | ||
| $type =\is_object($data) ?'c-'.\get_class($data) :\gettype($data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
does it make any sense to cache by internal type?
I feel like only classes should allow caching but I might be wrong
if the answer is yes, I'd suggest using the prefix on the gettype side, would be better for performance.
bendaviesApr 26, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
what it's doing is caching the fact that no normalizers supported a primitive, which allows a shortcut to escape testing again.
| $type =\is_object($data) ?'c-'.\get_class($data) :\gettype($data); | ||
| if ( | ||
| isset($this->normalizerCache[$type][$format]) || | ||
| (isset($this->normalizerCache[$type]) &&\array_key_exists($format,$this->normalizerCache[$type])) |
nicolas-grekasApr 26, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
if (isset($this->normalizerCache[$type][[$format])) looks enough to me here, provided the case for primitive types is done this way:
$this->normalizerCache[$type][$format] = false;return $this->normalizerCache[$type][$format] ?: null;
nicolas-grekas commentedApr 26, 2018
Some additional thought: |
jvasseur commentedApr 26, 2018
@nicolas-grekas I like your idea but instead of the marker interface I would use interface with a |
nicolas-grekas commentedApr 27, 2018
*AndFormat - But not sure about that because the (de)normalizer interfaces ask about supportsNormalization. It'd be strange to require implementing two similar methods, especially when the new one would basically just call the current on with a dummy$data as there is no other possible implementation. |
stof commentedApr 27, 2018
The implementation does not seem to care about the varying though. It always varies the cache by both type and format. |
dunglas commentedApr 27, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
New implementation proposal:
The gain is higher now, 16% instead of 9%:https://blackfire.io/profiles/compare/ca1d8a07-335f-4a37-8ea7-369f3983d044...b6a8383e-e4e9-4d9c-b514-2e31a47fbff1/graph?settings%5Bdimension%5D=wt&settings%5Bdisplay%5D=landscape&settings%5BtabPane%5D=nodes&selected=&callname=main() We should also update the documentation to encourage to always use this new interface when possible (= almost always). @nicolas-grekas the local cache still has a benefit if (and only if) at least 1 normalizer in the chain doesn't implement this new interface (in some case, there is a real use case: the |
dunglas commentedApr 27, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
It's not possible, most normalizers use |
bbb4d4e toe89c236Comparenicolas-grekas commentedApr 27, 2018
074b9b7 to11ecc0aCompare11ecc0a to16f8a13Comparedunglas commentedApr 27, 2018
Nice improvements@nicolas-grekas 👍 |
dunglas commentedApr 27, 2018
@bendavies can you try the last version before we merge it? |
bendavies commentedApr 27, 2018 via email• edited by nicolas-grekas
Loading Uh oh!
There was an error while loading.Please reload this page.
edited by nicolas-grekas
Uh oh!
There was an error while loading.Please reload this page.
Yes. Will later today. |
| foreach ($this->normalizerCache[$format][$type]as$k =>$cached) { | ||
| $normalizer =$this->normalizers[$k]; | ||
| if ($cached ||$normalizer->supportsNormalization($data,$format,$context)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Just to clarify maybe you can replace$cached by$isCacheableAndSupportsNormalization?
nicolas-grekasApr 27, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
bof, this is a local variable, and the concern is separated by 5 lines...
bendavies commentedApr 27, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Real world ApiPlatform app comparison (tldr: 36% improvement): |
nicolas-grekas commentedApr 29, 2018
Thank you@dunglas. |
… (dunglas, nicolas-grekas)This PR was merged into the 4.1-dev branch.Discussion----------[Serializer] Cache the normalizer to use when possible| Q | A| ------------- | ---| Branch? | master| Bug fix? | no| New feature? | yes <!-- don't forget to update src/**/CHANGELOG.md files -->| BC breaks? | no <!-- seehttps://symfony.com/bc -->| Deprecations? | no <!-- don't forget to update UPGRADE-*.md and src/**/CHANGELOG.md files -->| Tests pass? | yes <!-- please add some, will be required by reviewers -->| Fixed tickets |#24436 <!-- #-prefixed issue number(s), if any -->| License | MIT| Doc PR | todoStill a WIP:* [x] Support `supportsDenormalization`As this will dramatically improve the performance of the Serializer component, can we consider introducing this change in 4.1 @symfony/deciders?ping@bendaviesCommits-------16f8a13 [Serializer] Generalize caching support a bitc1e850f [Serializer] Cache the normalizer to use when possible
emodric commentedApr 30, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@dunglas@nicolas-grekas It seems that this breaks usage of my custom normalizers. They do not implement |
nicolas-grekas commentedApr 30, 2018
dunglas commentedMay 1, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@emodric can you provide a failing test case so we can iterate? |
…heableSupportsMethodInterface (nicolas-grekas)This PR was merged into the 4.1-dev branch.Discussion----------[Serializer] Add ->hasCacheableSupportsMethod() to CacheableSupportsMethodInterface| Q | A| ------------- | ---| Branch? | master| Bug fix? | yes| New feature? | no| BC breaks? | no| Deprecations? | no| Tests pass? | yes| Fixed tickets | -| License | MIT| Doc PR | -Enhances the interface introduced in#27049 to make it possible to dynamically define if "supports" methods are cacheable.Commits-------04b3692 [Serializer] Add ->hasCacheableSupportsMethod() to CacheableSupportsMethodInterface
| */ | ||
| protected$normalizers =array(); | ||
| private$cachedNormalizers; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
@nicolas-grekas what is this property for? it it set below but never used again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
ah,$this->normalizers is protected so could be edited by extensions. in which case you'd want to reset the cache?
nicolas-grekasJul 9, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
yes, that's the purpose, allowing efficient caching without breaking BC of the protected property (which is now internal so will be removed in 5.0
…dunglas, javiereguiluz)This PR was merged into the 4.1 branch.Discussion----------[Serializer] Cache the normalizer to use when possiblesymfony/symfony#27049Commits-------3e2e30f Minor reword8d6ca70 RST8e09eeb RSTccdf894 [Serializer] Cache the normalizer to use when possible

Uh oh!
There was an error while loading.Please reload this page.
Still a WIP:
supportsDenormalizationAs this will dramatically improve the performance of the Serializer component, can we consider introducing this change in 4.1 @symfony/deciders?
ping@bendavies