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.
// 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
"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.9.10
"symfony/form": "4.3.*", // v4.3.10
"symfony/framework-bundle": "4.3.*", // v4.3.9
"symfony/http-client": "4.3.*", // v4.3.10
"symfony/orm-pack": "^1.0", // v1.0.7
"symfony/security-bundle": "4.3.*", // v4.3.10
"symfony/serializer-pack": "^1.0", // v1.0.2
"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
"fzaninotto/faker": "^1.8", // v1.8.0
"symfony/debug-pack": "^1.0", // v1.0.7
"symfony/maker-bundle": "^1.13", // v1.14.3
"symfony/test-pack": "^1.0" // v1.0.6
}
}