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

[PropertyAccess] Add PSR-6 cache #16838

Closed
wants to merge 2 commits into from

Conversation

dunglas
Copy link
Member

@dunglas dunglas commented Dec 4, 2015

Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets n/a
License MIT
Doc PR n/a

Follow #16294

@@ -401,7 +410,7 @@ private function getReadAccessInfo($object, $property)

if (isset($this->readPropertyCache[$key])) {
$access = $this->readPropertyCache[$key];
} else {
} elseif (!$this->cache || !$access = $this->cache->fetch(self::CACHE_PREFIX_READ.$key)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm afraid that it wouldn't work for ACCESS_TYPE_METHOD case, because constant has value 0

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch it should be fixed now.

@dunglas dunglas force-pushed the propertypath_perf branch 2 times, most recently from 8f972f9 to 7108cc1 Compare December 4, 2015 22:32
@@ -504,6 +504,7 @@ private function addPropertyAccessSection(ArrayNodeDefinition $rootNode)
->children()
->booleanNode('magic_call')->defaultFalse()->end()
->booleanNode('throw_exception_on_invalid_index')->defaultFalse()->end()
->scalarNode('cache')->end()
Copy link
Contributor

Choose a reason for hiding this comment

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

IMHO it would be fine provide more information, like ->info('Service name used for cache')->example('property_accessor.cache.apc')

Copy link
Member

Choose a reason for hiding this comment

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

the example should not be included IMO as the examplified service doesn't exist. but info is indeed a good idea

@Tobion
Copy link
Member

Tobion commented Dec 4, 2015

IMO when a doctrine cache is injected, the internal array cache should not be used.

If I already inject an in-memory cache, I probably don't want to have it cached twice in memory. Also doctrine already has an ArrayCache. For multi-caching, there is ChainCache.
The internal cache would thus only be useful when I don't have the doctrine cache library.

@Koc
Copy link
Contributor

Koc commented Dec 4, 2015

@Tobion class property could save 2 methods call: Cache::fetch and apc_fetch (and maybe Cache::doFetch and maybe other)

@dunglas
Copy link
Member Author

dunglas commented Dec 4, 2015

@Tobion if we go that way, we can just remove totally the local cache array and rely only on Doctrine Cache to enable this performance optimization.
It will require the installation of an extra library (that can be preconfigured in the standard edition) but simplifies the code base and maintenance.

@Tobion
Copy link
Member

Tobion commented Dec 5, 2015

Yes relying fully on doctrine cache is also possible and the most flexible and the most explicit.

The problem with an internal array cache like now is that it is magic from the outside. And it doesn't really work well together with another cache layer like doctrine.

@Koc
Copy link
Contributor

Koc commented Dec 5, 2015

Guys, please measure performance before and after removing internal array cache, maybe using blackfire profile. There is enough funnction calls to retrieve values from cache: fetch, doFetch, getNamespacedId, getNamespaceVersion etc. https://github.com/doctrine/cache/blob/master/lib/Doctrine/Common/Cache/CacheProvider.php

@dunglas
Copy link
Member Author

dunglas commented Dec 5, 2015

Here is a benchmark (tldr use the internal cache, not Doctrine ArrayCache).

The comparison: https://blackfire.io/profiles/compare/3a1b8a29-125e-4f19-b5ba-ff0785c082fe/graph

It's more than 30% faster and consume less memory to use the internal cache than relying on the DoctrineChain. I remove this commit.

With Doctrine Cache only (internal array removed and chained ArrayCache and ApcCache):

<?php

require 'vendor/autoload.php';
require 'Foo.php';

use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\ApcCache;
use Doctrine\Common\Cache\ChainCache;
use Symfony\Component\PropertyAccess\PropertyAccess;

$cache = new ChainCache([new ArrayCache(), new ApcCache()]);
$accessor = new PropertyAccessor(false, false, $cache);

echo 'Property Access Benchmark'.PHP_EOL;

$start = microtime(true);

for ($i = 0; $i < 10000; ++$i) {
    $foo = new Foo();
    $accessor->setValue($foo, 'bar', 'Lorem');
    $accessor->setValue($foo, 'baz', 'Ipsum');
    $accessor->getValue($foo, 'bar');
    $accessor->getValue($foo, 'baz');
}

echo 'Time: '.(microtime(true) - $start).PHP_EOL;

With internal array and Doctrine ApcCache:

<?php

require 'vendor/autoload.php';
require 'Foo.php';

use Doctrine\Common\Cache\ApcCache;
use Symfony\Component\PropertyAccess\PropertyAccess;

$cache = new ApcCache();
$accessor = new PropertyAccessor(false, false, $cache);

echo 'Property Access Benchmark'.PHP_EOL;

$start = microtime(true);

for ($i = 0; $i < 10000; ++$i) {
    $foo = new Foo();
    $accessor->setValue($foo, 'bar', 'Lorem');
    $accessor->setValue($foo, 'baz', 'Ipsum');
    $accessor->getValue($foo, 'bar');
    $accessor->getValue($foo, 'baz');
}

echo 'Time: '.(microtime(true) - $start).PHP_EOL;

@dunglas
Copy link
Member Author

dunglas commented Dec 5, 2015

I've just added support for PropertyPath caching (it was the last bottleneck) and it's impressive:

35% of performance improvement just for this specific optim: https://blackfire.io/profiles/compare/4e5545b5-7537-4502-851b-baf65ad2f2e4/graph
And if we compare to Symfony 2.7.6 (before I start to optimize PropertyAccess), the Component is now 82% faster (after the first run, as the cache must be warmed): https://blackfire.io/profiles/compare/c4142593-9195-4426-806c-9a055670fada/graph

@stof you pointed out on the previous PR that PropertyPath is used outside of this component. I've looked to add factories to share the cache between components but it introduces a lof of complexity: bunch of new classes and services, complex dependency graph because of Doctrine cache... Everything done here is private, so I think we can start like that and refactor it to share the cache between components if someone need/want to work on it.

@Tobion
Copy link
Member

Tobion commented Dec 5, 2015

How is invalidation of the cache handled? Does the user have to clear his cache manually each time an object in the property path is changed as this can change the access strategy?

@dunglas
Copy link
Member Author

dunglas commented Dec 5, 2015

I'm not sure I understand your point. It caches the structure of classes and properties so the only time cache must be flushed it's when the code is modified (and it's almost always the time).

@Koc
Copy link
Contributor

Koc commented Dec 5, 2015

User should use same invalidation method like for apc class loader or validator metadata cache: clear cache after deploy.

@dunglas nice results 👍

@Tobion
Copy link
Member

Tobion commented Dec 5, 2015

So the recommended way is to only use caching on the property accessor in production mode I assume? Would be worth to mention that in the phpdoc I think.

@dunglas
Copy link
Member Author

dunglas commented Dec 5, 2015

@Tobion yes it's only for production but it's already what we suggest for other similar caches. Using the standard edition, you just need to install APC and to uncomment these lines https://github.com/symfony/symfony-standard/blob/master/app/config/config_prod.yml#L4-L14 and you get good performance for free. It's also why I think we need to keep APC services (it's user friendly).

@dunglas
Copy link
Member Author

dunglas commented Dec 5, 2015

Travis failures look unrelated.

@dunglas
Copy link
Member Author

dunglas commented May 5, 2016

There is a "drawback": it's not possible to disable the ApcAdapter if apcu is installed and enabled. Is it acceptable?
But also a benefit: event using the standalone component, the class will be as fast as possible without having to create services manually.

@mvrhov
Copy link

mvrhov commented May 5, 2016

@dunglas: I'm not comfortable with apc being default. The cache cannot be cleared from the console this means that after each deploy the php-fpm/apache should be restarted, ...

@Koc
Copy link
Contributor

Koc commented May 10, 2016

@dunglas can you try temporary disable psr6-caching inside getPropertyPath method and leave only class property cache? Also you can create instance of the property accessor outside foreach - for cases when we read/write many data inside one request

fabpot added a commit that referenced this pull request May 13, 2016
…in (nicolas-grekas)

This PR was merged into the 3.1-dev branch.

Discussion
----------

[FrameworkBundle] Default to Apcu+Filesystem cache chain

| Q             | A
| ------------- | ---
| Branch?       | 3.1
| Bug fix?      | yes (perf)
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

In #16838, @dunglas benched that the filesystem cache is not the fastest one (not a surprise).
Yet, it is the default for now. I propose to replace this default by a dynamically created one that uses APCu when available (of course, we cannot do the check at container-build time), chained with the filesystem.

The benefit is double: APCu is automatically used when available without any configuration, and cache warming up is seeded by the filesystem cache so that the apcu cache can benefit from it.

Commits
-------

b9b57f9 [FrameworkBundle] Default to Apcu+Filesystem cache chain
@nicolas-grekas
Copy link
Member

👍, please squash my commit :-)
Could be interesting to see the perf with PhpFilesAdapter (in another PR)

@dunglas
Copy link
Member Author

dunglas commented Jun 8, 2016

@symfony/deciders can I merge this PR please (one +1 missing)?

public static function createCache($namespace, $defaultLifetime, $version, LoggerInterface $logger = null)
{
if (!class_exists('Symfony\Component\Cache\Adapter\ApcuAdapter')) {
throw new \RuntimeException(sprintf('The Cache Component must be installed to use the "%s::%s" factory.', __CLASS__, __METHOD__));
Copy link
Member

Choose a reason for hiding this comment

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

sprintf('The Symfony Cache component must be installed to use %s().', __METHOD__)

@fabpot
Copy link
Member

fabpot commented Jun 8, 2016

👍

@dunglas
Copy link
Member Author

dunglas commented Jun 8, 2016

Comments from @nicolas-grekas @fabpot fixed. Can be merged.

@nicolas-grekas
Copy link
Member

Thank you @dunglas.

nicolas-grekas added a commit that referenced this pull request Jun 8, 2016
This PR was squashed before being merged into the 3.2-dev branch (closes #16838).

Discussion
----------

[PropertyAccess] Add PSR-6 cache

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | n/a

Follow #16294

Commits
-------

4ccabcd [PropertyAccess] Add PSR-6 cache
@fabpot fabpot mentioned this pull request Oct 27, 2016
@dunglas dunglas deleted the propertypath_perf branch October 17, 2017 07:42
franmomu added a commit to franmomu/SonataAdminBundle that referenced this pull request Nov 27, 2020
PropertyAccesor was added to Pool in order to reuse it for
performance reasons in sonata-project#3488

In symfony/symfony#16838 cache was added
to the component, so there is no need to reuse it thought the Pool
service and it can be injected into the services.
franmomu added a commit to franmomu/SonataAdminBundle that referenced this pull request Nov 27, 2020
PropertyAccesor was added to Pool in order to reuse it for
performance reasons in sonata-project#3488

In symfony/symfony#16838 cache was added
to the component, so there is no need to reuse it thought the Pool
service and it can be injected into the services.
franmomu added a commit to franmomu/SonataAdminBundle that referenced this pull request Nov 27, 2020
PropertyAccesor was added to Pool in order to reuse it for
performance reasons in sonata-project#3488

In symfony/symfony#16838 cache was added
to the component, so there is no need to reuse it thought the Pool
service and it can be injected into the services.
franmomu added a commit to franmomu/SonataAdminBundle that referenced this pull request Nov 29, 2020
PropertyAccesor was added to Pool in order to reuse it for
performance reasons in sonata-project#3488

In symfony/symfony#16838 cache was added
to the component, so there is no need to reuse it thought the Pool
service and it can be injected into the services.
franmomu added a commit to franmomu/SonataAdminBundle that referenced this pull request Nov 29, 2020
PropertyAccesor was added to Pool in order to reuse it for
performance reasons in sonata-project#3488

In symfony/symfony#16838 cache was added
to the component, so there is no need to reuse it thought the Pool
service and it can be injected into the services.
VincentLanglet pushed a commit to sonata-project/SonataAdminBundle that referenced this pull request Nov 29, 2020
PropertyAccesor was added to Pool in order to reuse it for
performance reasons in #3488

In symfony/symfony#16838 cache was added
to the component, so there is no need to reuse it thought the Pool
service and it can be injected into the services.
@derrabus derrabus added this to the 3.2 milestone Sep 10, 2023
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.

None yet