Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Serializer] Improve performance by exposing supports-never/-always #45779

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

Closed
wants to merge 1 commit into from

Conversation

Jeroeny
Copy link
Contributor

@Jeroeny Jeroeny commented Mar 18, 2022

Q A
Branch? 6.3
Bug fix? no
New feature? yes
Deprecations? Likely yes
License MIT
Doc PR Not yet initiated

Description

In the Serializer component, there is a CacheableSupportsMethodInterface. It can indicate the fact the normalizer's supports* method result can be cached.
Unfortunately, every normalizer that determines this result using $context cannot have its result cached (because it's based on format+type).

However there is some information with that supports call that often can be cached. Which is when a normalizer never supports a format+type, regardless of context.

For example:

public function supportsNormalization(mixed $data, string $format = null /*, array $context = [] */)
{
    return $data instanceof DateTime && ($context['DB_TYPES') ?? false);
}

This normalizer would not be able to have its result cached. Even though we know that if the $data is not an instance of DateTime, it will never be supported, regardless of $context.

Solution(s)

This pull request proposes to introduce the ability to return always/never supports $format+$type, to increase performance.
By introducing or altering a method to return one of 4 values:

  • support never
  • support not
  • support
  • support always

There are multiple ways to achieve this.

  1. The one that is done in this PR for now, is to use the existing supportsNormalization method.
    It would be changed to allow it to return one of the enum values (e.g. using integers -1 to 2, or bitwise), while still allowing bool return for backwards compatibilty.
    This has the advantage of only needing one method for the caching and supports information. And no extra interfaces, so CacheableSupportsMethodInterface can be deprecated.

With this, the example from earlier would become:

public function supportsNormalization(mixed $data, string $format = null /*, array $context = [] */)
{
    if (! $data instanceof DateTime) {
        return SUPPORT_NEVER; // a const, or could use an enum/integer directly
    }

    return ($context['DB_TYPES') ?? false) ? SUPPORT : SUPPORT_NOT;
}
  1. A new method CacheableSupportsInterface::supportsNormalizationCached($data, $format, $context): int. Possibly with a trait that implements the supportsNormalization to prevent duplicate logic (by doing return $this->supportsNormalizationCached(..) >= SUPPORT).

The solutions apply to both normalization and denormalization.

Performance impact example

  • If there are 10 (de)normalizers, that cannot use CacheableSupportsMethodInterface because of $context, but can return SUPPORT_NEVER like the example above.
  • The last of the normalizers supports the DateTime type.

Now you normalize a $dateTime 1000 times:

  • Before: 10.000 calls to supportsNormalization() and iterated over 10.010 $cached normalizers .
  • After: 1.009 calls to supportsNormalization() and iterated over 1.010 $cached normalizers ($this->normalizerCache[$format][DateTime::class] now only contains the one relevant normalizer).

A decrease in method calls of 89.91%.

@carsonbot
Copy link

Hey!

I think @mtarld has recently worked with this code. Maybe they can help review this?

Cheers!

Carsonbot

@Jeroeny
Copy link
Contributor Author

Jeroeny commented Apr 13, 2022

Is release 6.1 feature-locked yet, or could this still be considered for it ?

@Jeroeny
Copy link
Contributor Author

Jeroeny commented Apr 25, 2022

@dunglas Github has autosuggested you as reviewer. Do you think that's correct or would there be a better fit that I could contact for a review? Thanks

Copy link
Member

@dunglas dunglas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some nitpicking, otherwise LGTM.

@chalasr
Copy link
Member

chalasr commented Apr 26, 2022

Can you rebase and add a changelog entry?

@chalasr
Copy link
Member

chalasr commented Apr 27, 2022

Actually I'd prefer to let us more time to review this change as I'm not fond of the proposed API.
Postponing to 6.2 if you don't mind. Thanks

@chalasr chalasr modified the milestones: 6.1, 6.2 Apr 27, 2022
@Jeroeny
Copy link
Contributor Author

Jeroeny commented Apr 28, 2022

@chalasr Alright, do you have any suggestion for the implementation ?

@chalasr
Copy link
Member

chalasr commented Apr 28, 2022

@Jeroeny not yet, we're focus on stabilizing 6.1 right now. But I'm confident giving us some more time will allow us to explore other possibilities, even if we stick to this one in the end.

@Jeroeny
Copy link
Contributor Author

Jeroeny commented Jun 12, 2022

Rebased and targeting 6.2. Added changelog entry (even though it may still change).

@chalasr chalasr self-requested a review June 12, 2022 16:25
return $normalizer;
}
}

return null;
}

private function supportsNormalizationWrapper(NormalizerInterface $normalizer, mixed $data, ?string $format, array $context): CacheableSupport
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can find a more meaningful name for that private method?
Or at least add a comment telling that is a BC layer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment. The name could be better indeed, but haven't thought of anything better yet, am open to suggestions.

$this->assertFalse($serializer->supportsNormalization(new \StdClass(), 'json', ['TEST_CONTEXT' => true]));
$this->assertFalse($serializer->supportsNormalization(new \StdClass(), 'json', ['TEST_CONTEXT' => true]));
$this->assertFalse($serializer->supportsNormalization(new \StdClass(), 'json', ['TEST_CONTEXT' => true]));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can test the SupportNever as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is testing that, by calling supports.. multiple times and ensuring supportsNormalization is only called 3 times instead of 5.

$normalizer
            ->expects($this->exactly(3))
            ->method('supportsNormalization')

Unless you'd say something more explicit could be added?

@Jeroeny Jeroeny requested review from mtarld and removed request for dunglas and chalasr October 13, 2022 08:03
@Jeroeny Jeroeny requested review from dunglas and removed request for mtarld October 13, 2022 08:04
@nicolas-grekas nicolas-grekas modified the milestones: 6.2, 6.3 Nov 5, 2022
@Jeroeny Jeroeny force-pushed the serializer branch 2 times, most recently from 694b252 to 0fb7743 Compare November 30, 2022 12:43
@Jeroeny Jeroeny requested review from mtarld and removed request for dunglas November 30, 2022 12:44
@nicolas-grekas
Copy link
Member

Thanks for giving this a try. I think the approach in #49291 might provide better results.

@nicolas-grekas
Copy link
Member

Closing in favor #49291, thanks again.

fabpot added a commit that referenced this pull request Mar 10, 2023
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants