Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Performance Tests

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.

Let's profile the Github API endpoint again. I'll cheat and go directly to /api/github-organization... and click to profile this. I'll call it: [Recording] GitHub Ajax HTTP requests because we're going to look closer at the HTTP requests that our app makes to the GitHub API.

Click to view the call graph: https://bit.ly/sf-bf-http-requests

Oh wow - this request was super slow - 1.83 seconds - a lot slower than we've seen before. We can see that curl_multi_select() is the problem: this is our code making requests to the GitHub API, which is apparently running a bit slow at the moment.

We have a Performance "Bug"

Lucky for us, that's exactly what I wanted to talk about! At the top, Blackfire tells me that this page made two HTTP requests. And HTTP requests are always expensive for performance.

If you studied the data from the two API endpoints that we're using, you would discover that it's possible - by writing some clever code - to get all the info our app needs with just one HTTP request.

What I'm saying is: our page is making one more HTTP request than it truly needs to. If you think about it... that's kind of a performance "bug": we're making 2 HTTP requests and we only need 1.

In an ideal world, when we find a bug, the process for fixing it looks like this. First, write a test for the expected behavior. Second, run that test and watch it fail. And third, fix the bug and make sure the test passes.

Whelp, when it comes to a performance bug... we can do the exact same thing! We can write a functional test that asserts that this endpoint only makes one HTTP request. It's... pretty awesome.

Running the Functional Test

Find your editor and open tests/Controller/MainControllerTest.php. I already set up a functional test that makes a request to /api/github-organization and checks some basic data on the response:

... lines 1 - 2
namespace App\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class MainControllerTest extends WebTestCase
{
public function testGetGitHubOrganization()
{
$client = static::createClient();
$client->request('GET', '/api/github-organization');
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey('organization', $data);
}
}

Let's makes sure this passes. Run PHPUnit and point it directly at this class:

php bin/phpunit tests/Controller/MainControlerTest.php

The first time you run this script, it will probably download PHPUnit in the background. When it finishes... go tests go! All green.

Adding a Performance Assertion

Here's the idea: in addition to asserting that this response contains JSON with an organization key, I also want to assert that it only made one HTTP request. To do that, first add a trait from the SDK: use TestCaseTrait. Next, in the method, add $blackfireConfig = new Configuration() - the one from Blackfire\Profile: the same Configuration class we used earlier when we gave our custom-created profile a title. This time call assert() and pass it a very special string: metrics.http.requests.count == 1:

... lines 1 - 5
use Blackfire\Profile\Configuration;
... lines 7 - 8
class MainControllerTest extends WebTestCase
{
... lines 11 - 12
public function testGetGitHubOrganization()
{
$client = static::createClient();
$blackfireConfig = (new Configuration())
->assert('metrics.http.requests.count == 1');
... lines 19 - 26
}
}

I'll show you where that came from soon. Finally, below this, call $this->assertBlackfire() and pass this $blackfireConfig and a callback function:

... lines 1 - 5
use Blackfire\Profile\Configuration;
... lines 7 - 8
class MainControllerTest extends WebTestCase
{
... lines 11 - 12
public function testGetGitHubOrganization()
{
... lines 15 - 16
$blackfireConfig = (new Configuration())
->assert('metrics.http.requests.count == 1');
$this->assertBlackfire($blackfireConfig, function() use ($client) {
... lines 21 - 25
});
}
}

So... this confused me at first. When we call $this->assertBlackfire() it will execute this callback. Inside, we will do whatever work we want - like making the request. Finally, when the callback finishes, Blackfire will execute this assertion against the code that we ran:

... lines 1 - 5
use Blackfire\Profile\Configuration;
... lines 7 - 8
class MainControllerTest extends WebTestCase
{
... lines 11 - 12
public function testGetGitHubOrganization()
{
... lines 15 - 16
$blackfireConfig = (new Configuration())
->assert('metrics.http.requests.count == 1');
$this->assertBlackfire($blackfireConfig, function() use ($client) {
$client->request('GET', '/api/github-organization');
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey('organization', $data);
});
}
}

To get this to work, we need to use ($client):

... lines 1 - 8
class MainControllerTest extends WebTestCase
{
... lines 11 - 12
public function testGetGitHubOrganization()
{
... lines 15 - 19
$this->assertBlackfire($blackfireConfig, function() use ($client) {
... lines 21 - 25
});
}
}

If this doesn't make sense yet... don't worry: we'll dive a bit deeper soon. But right now... try it! Run the test again:

php bin/phpunit tests/Controller/MainControlerTest.php

And... it fails! Woo! Failed that metrics.http.requests.count == 1!

Performance Tests Create Real Profiles

Behind the scenes, the Blackfire SDK created a real Blackfire profile for the request! You can even copy the profile URL and go check it out! This takes us to an "assertions" tab. We're making 2 requests instead of the expected one. We'll talk a lot more about assertions soon.

Ok, but how did this really work? It's beautifully simple. When you run the test, it does make a real Blackfire profile in the background. However, if you go to your Blackfire homepage, you won't see it.

Why? Hold Cmd or Ctrl and click the assertBlackfire() method. I love it: this method uses the SDK - just like we did! - to create a real profile. When it does that, it also adds a skip_timeline option, which simply tells Blackfire to hide this from our profile page... so it doesn't get cluttered up with all these test profiles. You can totally override that if you wanted... via the Configuration object.

In reality, the Blackfire PHPUnit integration is doing the exact same thing that we just finished doing in our code: manually creating a new profile. This is really nothing new... and I love that!

Except... for this metrics thing. Where did that string come from? And what else can we do here? Let's dive into metrics 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