Skip to content

Cache identifiers for better performance on getIriFromItem#997

Merged
soyuka merged 1 commit intoapi-platform:masterfrom
soyuka:performance
Apr 24, 2017
Merged

Cache identifiers for better performance on getIriFromItem#997
soyuka merged 1 commit intoapi-platform:masterfrom
soyuka:performance

Conversation

@soyuka
Copy link
Member

@soyuka soyuka commented Mar 17, 2017

Q A
Bug fix? no
New feature? no
BC breaks? yes
Deprecations? no
Tests pass? Not yet (still experimenting)
Related tickets #212
License MIT
Doc PR ø

Add getIriFromItem cache

The collection here has ~8k items, no relations. Dev mode active but caching with apcu is forced.

Blackfire before:

  • 2.87s spent in getIriFromItem
  • 2.35 s for getIdentifiersFromItem

Blackfire after:

  • 898 ms spent in getIriFromItem
  • 371 ms for getIdentifiersFromItem

When discussing my performance issue with @Simperfit we noticed that getIdentifiersFromItem was called a lot (previous method is getIriFromItem). This is called for every item in the collection.

Profiler:

Don't look at the overall time, only the time spent serializing :p.

before

serializer3

after

cacheiri

In this PR, we cache the properties that are identifiers for the given class.

@teohhanhui
Copy link
Contributor

teohhanhui commented Mar 20, 2017

I like the idea, but the implementation is wrong. We should decorate the IriConverter: CachedIriConverter.

@soyuka
Copy link
Member Author

soyuka commented Mar 20, 2017

👍 indeed

$identifiers = [];
$resourceClass = $this->getObjectClass($item);

$cacheKey = self::CACHE_KEY_PREFIX.md5(serialize([$resourceClass]));
Copy link
Member

Choose a reason for hiding this comment

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

Why not using $resourceClass directly as it's a string?

Copy link
Member Author

Choose a reason for hiding this comment

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

Indeed, copy paste you know 👯‍♂️

I made this in a hurry just to see if my first assumption was correct! I'll refactor this and decorate the IriConverter as suggested by @teohhanhui. Will be done this week!

@soyuka soyuka added this to the 2.1.0 milestone Mar 22, 2017
@soyuka soyuka force-pushed the performance branch 2 times, most recently from 1fcc90f to 6babac9 Compare March 22, 2017 10:10
@soyuka
Copy link
Member Author

soyuka commented Mar 22, 2017

Okay so I did this with a decorator, but because what's slow is getIdentifiersFromItem (which is a private method) I'm not sure it's ideal. Indeed, I had to copy the code from IriConverter (trying to avoid BC break at the same time), therefore I see no point in decorating :/.

You can check the code to understand what I mean...

A solution would be to have a new service for getIdentifiersFromItem that we can cache without changing the IriConverter. Lmk if you've a better idea.


From my benchmarks, I know that we can still win a lot by caching the identifiers getters instead of calling the PropertyAccessor ($identifiers[$propertyName] = $item->{$accessor}();).

Some links (note that I'm using the normalizer dumper, 8000 items collection):

@soyuka soyuka changed the title [wip] Performance improvements [wip] Performance improvements on getIriFromItem Mar 22, 2017
@teohhanhui
Copy link
Contributor

teohhanhui commented Mar 28, 2017

@soyuka Okay, since what we're caching is a private method, then decoration indeed makes no sense.

A solution would be to have a new service for getIdentifiersFromItem that we can cache without changing the IriConverter.

ItemIdentifiersResolverInterface? ItemIdentifiersExtractorInterface?

@soyuka
Copy link
Member Author

soyuka commented Mar 28, 2017

ItemIdentifiersResolverInterface? Extract is a bit strong no?

Would get${ucfirst($propertyName)} be acceptable to retrieve identifiers instead of using the property accessor?

@teohhanhui
Copy link
Contributor

I think "extract" describes better what we're doing here: extracting identifiers from an item.

Would get${ucfirst($propertyName)} be acceptable to retrieve identifiers instead of using the property accessor?

No, we cannot make such assumptions. Plus, it'd be a BC break.

@dunglas
Copy link
Member

dunglas commented Mar 28, 2017

What we can do however is providing an implementation of the PropertyAccessorInterface doing that. I usually do this for perf in my project. Such class should be included directly in Symfony.

@soyuka
Copy link
Member Author

soyuka commented Mar 28, 2017

@dunglas You mean having a kind of IdentifierPropertyAccessor witch would end up doing something simpler than the original class (still no assumption could be made regarding get{$attribute}) ? We also don't need setValue in this context.

@dunglas
Copy link
Member

dunglas commented Mar 28, 2017

@soyuka yes, that you can register for your project if you stick to a particular convention (like public properties, or getters and setters). It prevents to trigger the introspection system, so it improves performances.

@soyuka
Copy link
Member Author

soyuka commented Mar 28, 2017

Oh I'm doing this already on my project for this particular class :). So, I'll just add a ItemIdentifiersExtractorInterface so that identifiers can be cached here but I won't touch the propertyAccessor thing.

@soyuka soyuka changed the title [wip] Performance improvements on getIriFromItem Cache identifiers for better performance on getIriFromItem Mar 29, 2017
@soyuka soyuka force-pushed the performance branch 2 times, most recently from 09aff4b to 63db69e Compare March 29, 2017 14:10
@soyuka
Copy link
Member Author

soyuka commented Mar 29, 2017

ping @teohhanhui @dunglas

I tried to avoid breaking changes lmk what you think.
Note that Behat tests are green, just waiting for implementation approval to add tests (and #1014 comes first obviously).

<argument type="service" id="api_platform.item_identifiers_extractor.cached" />
</service>

<service id="api_platform.item_identifiers_extractor" class="ApiPlatform\Core\Bridge\Symfony\Routing\ItemIdentifiersExtractor">
Copy link
Member

Choose a reason for hiding this comment

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

Can't those services be private?

use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

class ItemIdentifiersExtractor implements ItemIdentifiersExtractorInterface
Copy link
Member

Choose a reason for hiding this comment

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

Can you make it final and add some PHPDoc? Btw, why do this belong in the Symfony Bridge? It can be used in a more general context.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can be moved under ApiPlatform\Core\Metadata\Property namespace then?

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree

Copy link
Member

Choose a reason for hiding this comment

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

Indeed

private $propertyAccessor;
private $decorated;

public function __construct(CacheItemPoolInterface $cacheItemPool, PropertyAccessorInterface $propertyAccessor = null, ItemIdentifiersExtractorInterface $decorated = null)
Copy link
Contributor

Choose a reason for hiding this comment

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

should $decorated be null ?

use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

class ItemIdentifiersExtractor implements ItemIdentifiersExtractorInterface
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree

@Simperfit
Copy link
Contributor

👍 Great work @soyuka


if ($propertyMetadata->isIdentifier()) {
if (isset($identifiers[$propertyName])) {
throw new RuntimeException(sprintf('Composite identifiers not supported in "%s" through relation "%s" of "%s" used as identifier', $relatedResourceClass, $propertyName, $resourceClass));
Copy link
Member Author

Choose a reason for hiding this comment

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

@teohhanhui I'm having a hard time trying to add test coverage on this one. IMO this is not a valid case, but maybe you can give me some more context/informations about the Exception? Origin commit you authored.

@soyuka
Copy link
Member Author

soyuka commented Apr 12, 2017

I did a great job covering this part as it wasn't tested anywhere (except through IriConverter, but not fully covered).

There's still a point to discuss: the location of the IdentifiersExtractor.

I'm not sure that it should actually be under ApiPlatform\Core\Metadata because it doesn't interfere with metadata, rather uses the metadata to extract identifiers. Therefore, IMHO this class should be in ApiPlatform\Core\Api (so is ResourceClassResolver for example). Let me know if you disagree we can always move it elsewhere :).

@soyuka soyuka requested review from dunglas and teohhanhui April 18, 2017 06:13
@soyuka soyuka force-pushed the performance branch 2 times, most recently from 6a76d54 to 2d8c093 Compare April 20, 2017 15:01
@soyuka
Copy link
Member Author

soyuka commented Apr 20, 2017

All green ping @api-platform/core-team

@Simperfit
Copy link
Contributor

@soyuka could you please update with master ?

@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);
Copy link
Member

Choose a reason for hiding this comment

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

missing blank line

@@ -0,0 +1,101 @@
<?php

declare(strict_types=1);
Copy link
Member

Choose a reason for hiding this comment

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

Missing blank line

private $propertyAccessor;
private $decorated;

public function __construct(CacheItemPoolInterface $cacheItemPool, PropertyAccessorInterface $propertyAccessor = null, IdentifiersExtractorInterface $decorated)
Copy link
Member

Choose a reason for hiding this comment

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

Maybe should you put $propertyAccessor as last parameter (because it is optional)

$cacheItem = $this->cacheItemPool->getItem($cacheKey);
$cacheIsHit = $cacheItem->isHit();

if ($cacheIsHit) {
Copy link
Member

Choose a reason for hiding this comment

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

Can be merged with the previous line

public function __construct(CacheItemPoolInterface $cacheItemPool, PropertyAccessorInterface $propertyAccessor = null, IdentifiersExtractorInterface $decorated)
{
$this->cacheItemPool = $cacheItemPool;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
Copy link
Member

Choose a reason for hiding this comment

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

$propertyAccessor ?? PropertyAccess::createPropertyAccessor();?

interface IdentifiersExtractorInterface
{
/**
* Find identifiers from an Item (Object).
Copy link
Member

Choose a reason for hiding this comment

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

Finds

@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);
Copy link
Member

Choose a reason for hiding this comment

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

Blank line

@@ -0,0 +1,135 @@
<?php

declare(strict_types=1);
Copy link
Member

Choose a reason for hiding this comment

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

Blank line

$relatedCacheItem = $this->cacheItemPool->getItem($relatedCacheKey);

if (!$relatedCacheItem->isHit()) {
throw new CacheException('No relation cache item founded, we need more cache to continue.');
Copy link
Member

Choose a reason for hiding this comment

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

Can't we avoid to use exceptions? Exception handling is slow.

@@ -0,0 +1,111 @@
<?php

declare(strict_types=1);
Copy link
Member

Choose a reason for hiding this comment

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

blank line

@soyuka soyuka force-pushed the performance branch 2 times, most recently from b636643 to 56774b4 Compare April 20, 2017 18:58
@soyuka
Copy link
Member Author

soyuka commented Apr 21, 2017

@dunglas this should be good now!

{
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
$this->propertyMetadataFactory = $propertyMetadataFactory;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
Copy link
Member

Choose a reason for hiding this comment

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

??

$this->itemDataProvider = $itemDataProvider;
$this->routeNameResolver = $routeNameResolver;
$this->router = $router;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
Copy link
Member

Choose a reason for hiding this comment

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

Can you change this one too? ??

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.

👍 Feel free to merge.

@soyuka soyuka force-pushed the performance branch 2 times, most recently from b9cac9f to 5548232 Compare April 21, 2017 13:29
@soyuka soyuka merged commit f73309c into api-platform:master Apr 24, 2017
@soyuka soyuka deleted the performance branch April 24, 2017 07:24
hoangnd25 pushed a commit to hoangnd25/core that referenced this pull request Feb 23, 2018
Cache identifiers for better performance on getIriFromItem
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.

4 participants