Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

SDK: Automatically Create a Profile

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.

Imagine you have a performance "problem" on production. No worries! Except... the issue is only caused in some edge-case situation... and you're having a hard time repeating the exact condition... which means that you can't create a meaningful Blackfire profile by using the browser extension.

For example, imagine we want to profile the AJAX request that loads the GitHub repository info... but we think that the performance problem only happens for certain types of users - maybe users that have many comments. I'm just making this up.

To do that, instead of triggering a new profile by clicking the browser extension button - which maybe is hard because we can't seem to replicate the correct situation - let's trigger a new profile automatically from inside our code. We can do this using the PHP SDK.

Spin over, go back to MainController and scroll down to loadSightingsPartial()... actually to the gitHubOrganizationInfo() method:

... lines 1 - 16
class MainController extends AbstractController
{
... lines 19 - 58
/**
* @Route("/api/github-organization", name="app_github_organization_info")
*/
public function gitHubOrganizationInfo(GitHubApiHelper $apiHelper)
{
$organizationName = 'SymfonyCasts';
$organization = $apiHelper->getOrganizationInfo($organizationName);
$repositories = $apiHelper->getOrganizationRepositories($organizationName);
return $this->json([
'organization' => $organization,
'repositories' => $repositories,
]);
}
... lines 73 - 127
}

This is the controller that returns the content on the right side of the page.

Start by creating a fake variable $shouldProfile = true:

... lines 1 - 17
class MainController extends AbstractController
{
... lines 20 - 55
public function gitHubOrganizationInfo(GitHubApiHelper $apiHelper)
{
// replace with some conditional logic
$shouldProfile = true;
... lines 60 - 73
}
... lines 75 - 129
}

In a real app, you would replace this with logic to determine whether or not this is one of those requests that you think might have a performance problem: maybe you check to see if the user has many comments or something.

Creating & Starting the Profile

Then, if $shouldProfile, it means that we want Blackfire to profile this request. To do that, say $blackfire = new Client() - the one from Blackfire. This is an object that helps communicate with the Blackfire servers. Next, create a probe - basically create a new "profile" - with $probe = $blackfire->createProbe():

... lines 1 - 17
class MainController extends AbstractController
{
... lines 20 - 55
public function gitHubOrganizationInfo(GitHubApiHelper $apiHelper)
{
// replace with some conditional logic
$shouldProfile = true;
if ($shouldProfile) {
$blackfire = new Client();
$probe = $blackfire->createProbe();
}
... lines 65 - 73
}
... lines 75 - 129
}

Earlier, when we used BlackfireProbe::getMainInstance(), we were, kind of asking for a "probe" if there was a profile happening. But this time, we're creating a probe: creating a new profile and telling it to start "instrumenting" - collecting data - right now.

In fact, the second argument to createProbe() is $enabled=true: whether or not we want the probe to immediately start instrumentation or if we will enable it later with $probe->enable().

Now, because this will create a new profile, you need to make sure you do this only rarely on production. Why? Because creating profiles is heavy and this slow request will be felt by whichever user triggered it. So, choose your logic for $shouldProfile carefully.

Anyways, let's try it! Move over and refresh your list of Blackfire profiles. The most recent one is the "Only instrumenting some code" profile. Now refresh the homepage. This triggers the AJAX call... but notice it's slower. And when we refresh Blackfire... boom! We have a brand new profile! Open that up and... let's give it a name: [Recording] First automatic profile: http://bit.ly/f-bf-1st-auto-profile. I'm so proud.

This only Profiles the Controller

You can now create new profiles from your code... whenever you want to. But... there's a small problem: this only profiled a tiny part of our code. And, that makes sense: when our PHP code started executing, the PHP extension didn't yet know that we wanted to profile this request. And so, it couldn't start collecting data until we told it to, which happened in the controller. To make matters worse, as soon as PHP garbage collected the $probe variable... which happened once the variable isn't used anymore... so at the end of the controller, internally, the probe called close() on itself. That means that we just collected data on nothing more than the code in our controller.

How can we fix that? By starting the probe super early and closing it manually as late as we can. Let's do that next.

Leave a comment!

0
Login or Register to join the conversation
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