Spotting Heavy Object Instantiation
I want to show a... more subtle performance problem. To even see it, we need to go back to the prod
environment:
// ... lines 1 - 16 | |
APP_ENV=prod | |
// ... lines 18 - 29 |
Make sure to run cache:clear
:
php bin/console cache:clear
cache:warmup
:
php bin/console cache:warmup
And also:
composer dump-autoload --optimize
Let's create a fresh profile of the homepage. I'll call this one: [Recording] Homepage prod
. Click to view the timeline: http://bit.ly/sf-bf-instantiation
Overall, this request is pretty fast. Click into the "Memory" dimension. The biggest call is Composer\Autoload\includeFile
: that's literally Composer including files that we need... not a lot of memory optimization we can do about that.
But, if we look closer, the memory dimension reveals something else. See this "Container" thing - the 2nd item on the function list? This is related to Symfony's container, which is responsible for instantiating all of our objects. This specific function is interesting: it's highlighting a section of a file that lives in our cache directory. If you looked in that file, you would see that this part of the code is responsible for including some of the main files that our app needs. It's basically another version of the top node: it's code that includes files for classes we're using.
Seeing Object Instantiation
Ok, so the first few aren't really that interesting. Things get much more intriguing down on the 4th function call: some Container{BlahBlah}/getDoctrine_Orm_DefaultEntityManagerService.php
call. What is this? Well, the details of how this is organized are specific to Symfony: but this is evidence of something that every app does: this is showing the amount of resources used to instantiate Doctrine's EntityManager object. I know, we don't often think about how much time or how much memory it takes to instantiate an object, but it can sometimes be a problem. The next function call is for the instantiation of Doctrine's Connection service.
Go down a little bit... I'm looking for something specific... here it is: getLoginFormAuthenticatorService()
. This is responsible for instantiating a LoginFormAuthenticator
object in our app. It's not a particularly problematic function though: it's 10th on the list... only takes 2.56 milliseconds and uses about 500 kilobytes.
Checking the Instantiation of LoginFormAuthenticator
Let's check out the class: src/Security/LoginFormAuthenticator.php
:
// ... lines 1 - 2 | |
namespace App\Security; | |
// ... lines 4 - 21 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
use TargetPathTrait; | |
private $entityManager; | |
private $urlGenerator; | |
private $csrfTokenManager; | |
private $passwordEncoder; | |
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder) | |
{ | |
$this->entityManager = $entityManager; | |
$this->urlGenerator = $urlGenerator; | |
$this->csrfTokenManager = $csrfTokenManager; | |
$this->passwordEncoder = $passwordEncoder; | |
} | |
public function supports(Request $request) | |
{ | |
return 'app_login' === $request->attributes->get('_route') | |
&& $request->isMethod('POST'); | |
} | |
public function getCredentials(Request $request) | |
{ | |
$credentials = [ | |
'email' => $request->request->get('email'), | |
'password' => $request->request->get('password'), | |
'csrf_token' => $request->request->get('_csrf_token'), | |
]; | |
$request->getSession()->set( | |
Security::LAST_USERNAME, | |
$credentials['email'] | |
); | |
return $credentials; | |
} | |
public function getUser($credentials, UserProviderInterface $userProvider) | |
{ | |
$token = new CsrfToken('authenticate', $credentials['csrf_token']); | |
if (!$this->csrfTokenManager->isTokenValid($token)) { | |
throw new InvalidCsrfTokenException(); | |
} | |
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]); | |
if (!$user) { | |
// fail authentication with a custom error | |
throw new CustomUserMessageAuthenticationException('Email could not be found.'); | |
} | |
return $user; | |
} | |
public function checkCredentials($credentials, UserInterface $user) | |
{ | |
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']); | |
} | |
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) | |
{ | |
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) { | |
return new RedirectResponse($targetPath); | |
} | |
return new RedirectResponse($this->urlGenerator->generate('app_homepage')); | |
} | |
protected function getLoginUrl() | |
{ | |
return $this->urlGenerator->generate('app_login'); | |
} | |
} |
As its name suggests, this is responsible for authenticating the user when they submit the login form.
But, there's something special about this class. Due to the way the Symfony security system works, Symfony instantiates this object on every request. It does that so it can then call supports()
to figure out if this service should be "activated" on this request or not:
// ... lines 1 - 21 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 24 - 38 | |
public function supports(Request $request) | |
{ | |
return 'app_login' === $request->attributes->get('_route') | |
&& $request->isMethod('POST'); | |
} | |
// ... lines 44 - 94 | |
} |
For this class, it only needs to its work when the URL is /login
and this is a POST
request. In every other situation, supports()
returns false and no other methods are called on this class.
So let's think about this. Instantiating this class takes about 3 milliseconds and 500 kilobytes... which is not a ton... but since all it needs to do for most requests is check the current URL... then exit... that is kind of heavy.
Why Instantiation is Slow?
The question is: why does it take so many resources to instantiate? Well, 500 kilobytes is not a ton, but this is - according to Blackfire - one of the most expensive objects that is created on this request. Why?
Check out the constructor:
// ... lines 1 - 21 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 24 - 30 | |
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder) | |
{ | |
$this->entityManager = $entityManager; | |
$this->urlGenerator = $urlGenerator; | |
$this->csrfTokenManager = $csrfTokenManager; | |
$this->passwordEncoder = $passwordEncoder; | |
} | |
// ... lines 38 - 94 | |
} |
In order to instantiate this class, Symfony needs to make sure the EntityManager
is instantiated... and the UrlGenerator
.. and the CsrfTokenManager
... and the UserPasswordEncoder
. If any of these services have their own dependencies, even more objects may need to be instantiated. In rare situations, creating a service can be a huge performance problem.
In the case of the EntityManager
and the UrlGenerator
... those are pretty core objects that would probably be needed and thus instantiated by something on this request anyways. But CsrfTokenManager
and UserPasswordEncoder
are not normally needed. In other words, we're forcing Symfony to instantiate both of those services on every request... even though we only need them when the user is submitting the login form.
This is a classic situation where you have an object that is instantiated on every request... but only needs to do real work in rare cases. Certain event subscribers - like our AgreeToTermsSubscriber
- Symfony security voters & Twig extensions are other examples from Symfony. These services might be quick to instantiate... so no problem! But they also might be expensive.
So... how could we make it quicker to instantiate LoginFormAuthenticator
? In Symfony, with a service subscriber.