Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Timeline: Finding a Hidden Surprise

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

One of the big spots on the timeline is the RequestEvent. It's purple because this is an event: the first event that Symfony dispatches. It happens before the controller is called... which is pretty obvious in this view.

Let's zoom in: by double-clicking the square. Beautiful! What happens inside this event? Apparently... the routing layer happens! That's RouterListener. You can also see Firewall: this is where authentication takes place. Security is a complex system... so being able to see a bit about what happens inside of it is pretty cool. At some point... it calls a method on EntityRepository and we can see the query for the User object that we're logged in as. Pretty cool.

The Hidden Slow Listener

There's one more big chunk under RequestEvent: something called AgreeToTermsSubscriber... which is taking 30 milliseconds. Let's open that class and see what it does: src/EventSubscriber/AgreeToTermsSubscriber.php:

... lines 1 - 2
namespace App\EventSubscriber;
use App\Entity\User;
use App\Form\AgreeToUpdatedTermsFormType;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Security;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;
use Twig\Environment;
class AgreeToTermsSubscriber implements EventSubscriberInterface
{
private $security;
private $formFactory;
private $twig;
private $entrypointLookup;
public function __construct(Security $security, FormFactoryInterface $formFactory, Environment $twig, EntrypointLookupInterface $entrypointLookup)
{
$this->security = $security;
$this->formFactory = $formFactory;
$this->twig = $twig;
$this->entrypointLookup = $entrypointLookup;
}
public function onRequestEvent(RequestEvent $event)
{
$user = $this->security->getUser();
// only need this for authenticated users
if (!$user instanceof User) {
return;
}
// in reality, you would hardcode the most recent "terms" date
// change so you can see if the user needs to "re-agree". I've
// set it dynamically to 1 year ago to avoid anyone hitting
// this - as it's just example code...
//$latestTermsDate = new \DateTimeImmutable('2019-10-15');
$latestTermsDate = new \DateTimeImmutable('-1 year');
$form = $this->formFactory->create(AgreeToUpdatedTermsFormType::class);
$html = $this->twig->render('main/agreeUpdatedTerms.html.twig', [
'form' => $form->createView()
]);
// resets Encore assets so they render correctly later
// only technically needed here because we should really
// "exit" this function before rendering the template if
// we know the user doesn't need to see the form!
$this->entrypointLookup->reset();
// user is up-to-date!
if ($user->getAgreedToTermsAt() >= $latestTermsDate) {
return;
}
$response = new Response($html);
$event->setResponse($response);
}
public static function getSubscribedEvents()
{
return [
RequestEvent::class => 'onRequestEvent',
];
}
}

Ah yes. Every now and then, we update the "terms of service" on our site. When we do that, our lovely lawyers have told us that we need to require people to agree to the updated terms. This class handles that: it gets the authenticated user and, if they're not logged in, it does nothing:

... lines 1 - 14
class AgreeToTermsSubscriber implements EventSubscriberInterface
{
... lines 17 - 29
public function onRequestEvent(RequestEvent $event)
{
$user = $this->security->getUser();
// only need this for authenticated users
if (!$user instanceof User) {
return;
}
... lines 38 - 63
}
... lines 65 - 71
}

But if they are logged in, then it renders a twig template with an "agree to the terms" form:

... lines 1 - 14
class AgreeToTermsSubscriber implements EventSubscriberInterface
{
... lines 17 - 29
public function onRequestEvent(RequestEvent $event)
{
... lines 32 - 38
// in reality, you would hardcode the most recent "terms" date
// change so you can see if the user needs to "re-agree". I've
// set it dynamically to 1 year ago to avoid anyone hitting
// this - as it's just example code...
//$latestTermsDate = new \DateTimeImmutable('2019-10-15');
$latestTermsDate = new \DateTimeImmutable('-1 year');
$form = $this->formFactory->create(AgreeToUpdatedTermsFormType::class);
$html = $this->twig->render('main/agreeUpdatedTerms.html.twig', [
'form' => $form->createView()
]);
// resets Encore assets so they render correctly later
// only technically needed here because we should really
// "exit" this function before rendering the template if
// we know the user doesn't need to see the form!
$this->entrypointLookup->reset();
// user is up-to-date!
if ($user->getAgreedToTermsAt() >= $latestTermsDate) {
return;
}
... lines 61 - 63
}
... lines 65 - 71
}

Eventually, if the terms have been updated since the last time this User agreed to them, it sets that form as the response instead of rendering the real page.

We haven't seen this form yet... and... it's not really that important. Because we rarely update our terms, 99.99% of the requests to the site will not display the form.

So... the fact that this is taking 30 milliseconds... even though it will almost never do anything... is kind of a lot!

Blue Memory Footprint

Oh, and see this blue background? I love this: it's the memory footprint. If we trace over this call - this is about when the AgreeToTermsSubscriber happens - the memory starts at 3.44 megabytes... and finishes around 4.46. That's 1 megabyte of memory - kinda high for such a rarely-used function.

The point is: this method doesn't take that long to run. And so, it may not have shown up as a performance critical path on the call graph. But thanks to the timeline, this invisible layer jumped out at us. And... I think it is taking a bit too long.

Fixing the Slow Code

Back in the code, the mistake I made is pretty embarrassing. I'm using some pretend logic to see whether or not we need to render the form. But... I put the check too late!

... lines 1 - 14
class AgreeToTermsSubscriber implements EventSubscriberInterface
{
... lines 17 - 29
public function onRequestEvent(RequestEvent $event)
{
... lines 32 - 56
// user is up-to-date!
if ($user->getAgreedToTermsAt() >= $latestTermsDate) {
return;
}
... lines 61 - 63
}
... lines 65 - 71
}

We're doing all the work of rendering the form... even if we don't use it.

Let's move that code all the way to the top. Ah, too far - it needs to be after the fake $latestTermsDate variable:

... lines 1 - 14
class AgreeToTermsSubscriber implements EventSubscriberInterface
{
... lines 17 - 29
public function onRequestEvent(RequestEvent $event)
{
... lines 32 - 38
// in reality, you would hardcode the most recent "terms" date
// change so you can see if the user needs to "re-agree". I've
// set it dynamically to 1 year ago to avoid anyone hitting
// this - as it's just example code...
//$latestTermsDate = new \DateTimeImmutable('2019-10-15');
$latestTermsDate = new \DateTimeImmutable('-1 year');
// user is up-to-date!
if ($user->getAgreedToTermsAt() >= $latestTermsDate) {
return;
}
... lines 50 - 63
}
... lines 65 - 71
}

That looks better. Let's try it! I'll refresh the page. Profile again and call it [Recording] Homepage authenticated fixed subscriber: http://bit.ly/sf-bf-timeline-fix

Let's jump straight to view the Timeline... double-click RequestEvent and this time... AgreeToTermsSubscriber is gone! We can see RouterListener and Firewall... but not AgreeToTermsSubscriber. That's not because our app isn't calling it anymore: it is. It's because Blackfire hides function calls that take almost no resources. That's great.

Next, we know that we can write code inside a function that is slow. But did you know that sometimes even the instantiation of an object can eat a lot of resources? Let's see how that looks in Blackfire and leverage a Symfony feature - service subscribers - to make instantiation lighter.

Leave a comment!

2
Login or Register to join the conversation
Dang Avatar

Hello, I use the same code (not fixed) and open the blackfire but I don't see the "AgreeToTermsSubscriber:onRequestEvent" under the bar RequestEvent in my timeline. There's only main/agreeUpdatedTerms.html.twig display to recognize that the form is rendered. And in the left timeline metrics, I don't see the symfony.firewall and symfony.routing.router as the video. Is there something changed in the blackfire?

And also, when I submit that form with agree (agreeUpdatedTerms.html.twig), nothing happen and the form will show forever . It seems to me this is a bug because the controller of that agreeUpdatedTerms in MainController was never called. Could you verify that?
Thanks

Reply

Hey @Dang!

Hmm, Blackfire's interface has changed a bit, but let's see what we can figure out :).

I just tried profiling - I'm using the "start" code from the tutorial, so before we fix anything. I also don't see the symfony.firewall , but I do see the symfony.routing.router on the left side. Also, when I profile Symfonycasts on production, I DO see symfony.firewall. My guess is that they're hiding/showing some of these just based on how much time they take - e.g. if they don't take much time, then they're not shown.

I also don't see AgreeToTermsSubscriber:onRequestEvent on the timeline. But again, I think this is because Blackfire is hiding things that aren't taking enough time. Are you logged in when you try this page? The subscriber really only takes a lot of time if the user is currently logged in - so that may be why it's running so quickly and not showing up. That was the case for me: I didn't see it at first. But after logging in, it was back on the timeline!

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial can be used to learn how to profile any app - including Symfony 5.

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice