gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
Whenever we make something more performant, we often also make our code more complex. So, was the property-caching trick we just used worth it? Maybe... but I'm going to revert it.
Remove the property caching logic and just return
$this->calculateUserActivityText($user)
:
... lines 1 - 10 | |
class AppExtension extends AbstractExtension | |
{ | |
... lines 13 - 26 | |
public function getUserActivityText(User $user): string | |
{ | |
return $this->calculateUserActivityText($user); | |
} | |
... lines 31 - 49 | |
} |
And... we don't need the $userStatuses
property anymore:
... lines 1 - 10 | |
class AppExtension extends AbstractExtension | |
{ | |
... lines 13 - 14 | |
private $userStatuses = []; | |
... lines 16 - 55 | |
} |
We could stop here and say: this spot is not worth optimizing. Or, we can try a different solution - like using a real caching layer. After all, this label probably won't change very often... and it's probably not critical that the label changes at the exact moment a user adds enough comments to get to the next level. Caching could be an easy win.
Back in AppExtension
, autowire Symfony's cache object by adding an argument
type-hinted with CacheInterface
- the one from Symfony\Contracts\Cache
. I'll
press Alt
+Enter
and select "Initialize fields" to make PhpStorm create a new
property with this name and set it in the constructor:
... lines 1 - 7 | |
use Symfony\Contracts\Cache\CacheInterface; | |
... lines 9 - 12 | |
class AppExtension extends AbstractExtension | |
{ | |
... line 15 | |
private $cache; | |
public function __construct(CommentHelper $commentHelper, CacheInterface $cache) | |
{ | |
... line 20 | |
$this->cache = $cache; | |
} | |
... lines 23 - 59 | |
} |
Down in the method, let's first create a cache key that's specific to each user.
How about: $key = sprintf('user_activity_text_'.
and then $user->getId()
:
... lines 1 - 12 | |
class AppExtension extends AbstractExtension | |
{ | |
... lines 15 - 30 | |
public function getUserActivityText(User $user): string | |
{ | |
$key = sprintf('user_activity_text_'.$user->getId()); | |
... lines 34 - 39 | |
} | |
... lines 41 - 59 | |
} |
Wow, I just realized that my sprintf
here is totally pointless.
Then, return $this->cache->get()
and pass this $key
. If that item exists in
the cache, it will return immediately:
... lines 1 - 12 | |
class AppExtension extends AbstractExtension | |
{ | |
... lines 15 - 30 | |
public function getUserActivityText(User $user): string | |
{ | |
$key = sprintf('user_activity_text_'.$user->getId()); | |
return $this->cache->get($key, function(CacheItemInterface $item) use ($user) { | |
... lines 36 - 38 | |
}); | |
} | |
... lines 41 - 59 | |
} |
Otherwise, it will execute this callback function, pass us a CacheItemInterface
object and our job will be to return the value that should be stored in cache.
Hmm... I need the $user
object inside here. Add use
then $user
to bring it
into scope. Then return $this->calculateUserActivityText($user)
:
... lines 1 - 12 | |
class AppExtension extends AbstractExtension | |
{ | |
... lines 15 - 30 | |
public function getUserActivityText(User $user): string | |
{ | |
$key = sprintf('user_activity_text_'.$user->getId()); | |
return $this->cache->get($key, function(CacheItemInterface $item) use ($user) { | |
... lines 36 - 37 | |
return $this->calculateUserActivityText($user); | |
}); | |
} | |
... lines 41 - 59 | |
} |
I think it's probably safe to cache this value for one hour: that's long enough,
but not so long that we need to worry about adding a system to manually invalidate
the cache. Set the expiration with $item->expiresAfter(3600)
:
... lines 1 - 12 | |
class AppExtension extends AbstractExtension | |
{ | |
... lines 15 - 30 | |
public function getUserActivityText(User $user): string | |
{ | |
$key = sprintf('user_activity_text_'.$user->getId()); | |
return $this->cache->get($key, function(CacheItemInterface $item) use ($user) { | |
$item->expiresAfter(3600); | |
return $this->calculateUserActivityText($user); | |
}); | |
} | |
... lines 41 - 59 | |
} |
So... does this help? Of course it will! More importantly, because we decided we don't need to worry about adding more complexity to invalidate the cache, it's probably a big win! But let's find out for sure.
Move over and refresh. Boo - 500 error. We're in the prod
environment... and I
forgot to rebuild the cache:
php bin/console cache:clear
And:
php bin/console cache:warmup
Refresh again. And... profile! I'll name this one: [Recording] Show page real cache
.
Open up the call graph: https://bit.ly/sf-bf-real-caching.
This time things look way better. But let's not trust it: go compare the original profile - before we even did property caching - to this new one: https://bit.ly/sf-bf-compare-real-cache.
Wow. The changes are significant... and there's basically no downside to the changes we made. Even our memory went down! You can also compare this to the property caching method: https://bit.ly/sf-bf-compare-prop-real-caching. Yea... it's way better
And really, this is no surprise: fully caching things will... of course be faster! The question is how much faster? And if adding caching means that you also need to add a cache invalidation system, is that performance boost worth it? Since we don't need to worry about invalidation in this case, it was totally worth it.
Next: let's find & solve a classic N+1 query problem. The final solution might not be what you traditionally expect.
Hey Kiuega
Those are great questions. I think you can implement that listener and do what you say, it should work but you must be aware that Doctrine's listeners are not triggered when you run a custom query, i.e. when you don't create your query via the QueryBuilder
. Another thing to consider is code clarity. It may not be obvious for other developers on your team how the cache it's cleared out. Perhaps you would prefer to manually clear the cache directly on the remove/add comment endpoints.
Cheers!
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-ctype": "*",
"ext-iconv": "*",
"blackfire/php-sdk": "^1.20", // v1.20.0
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // v1.8.0
"doctrine/doctrine-bundle": "^1.6.10|^2.0", // 1.11.2
"doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // v2.0.0
"doctrine/orm": "^2.5.11", // v2.6.4
"phpdocumentor/reflection-docblock": "^3.0|^4.0", // 4.3.2
"sensio/framework-extra-bundle": "^5.4", // v5.5.1
"symfony/console": "4.3.*", // v4.3.10
"symfony/dotenv": "4.3.*", // v4.3.10
"symfony/flex": "^1.9", // v1.18.7
"symfony/form": "4.3.*", // v4.3.10
"symfony/framework-bundle": "4.3.*", // v4.3.9
"symfony/http-client": "4.3.*", // v4.3.10
"symfony/property-access": "4.3.*", // v4.3.10
"symfony/property-info": "4.3.*", // v4.3.10
"symfony/security-bundle": "4.3.*", // v4.3.10
"symfony/serializer": "4.3.*", // v4.3.10
"symfony/twig-bundle": "4.3.*", // v4.3.10
"symfony/validator": "4.3.*", // v4.3.10
"symfony/webpack-encore-bundle": "^1.6", // v1.7.2
"symfony/yaml": "4.3.*", // v4.3.10
"twig/extensions": "^1.5" // v1.5.4
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.2", // 3.2.2
"easycorp/easy-log-handler": "^1.0.7", // v1.0.9
"fzaninotto/faker": "^1.8", // v1.8.0
"symfony/browser-kit": "4.3.*", // v4.3.10
"symfony/css-selector": "4.3.*", // v4.3.10
"symfony/debug-bundle": "4.3.*", // v4.3.10
"symfony/maker-bundle": "^1.13", // v1.14.3
"symfony/monolog-bundle": "^3.0", // v3.5.0
"symfony/phpunit-bridge": "^5.0", // v5.0.3
"symfony/stopwatch": "4.3.*", // v4.3.10
"symfony/var-dumper": "4.3.*", // v4.3.10
"symfony/web-profiler-bundle": "4.3.*" // v4.3.10
}
}
Hi team! I love this way of caching items! In the video you set up a cache on the user_activity_text, referenced by the user ID. You invalidate this cache after 3600 seconds.
Couldn't we instead create a Doctrine listener, which, when a user adds / deletes a comment, then we invalidate the 'user_activity_text' cache referenced by that user?
In general, would managing the timeout manually using Doctrine listeners be a good solution?
I also appreciate the fact that you put forward the fact of creating a cache key per user, I was wondering if this was relevant or if we should have a cache for all users!