⌛ This article is now 4 years and 6 months old, a quite long time during which techniques and tools might have evolved. Please contact us to get a fresh insight of our expertise!
You may have memory leaking from PHP 7 and Symfony tests
Update December 30, 2019: I’m happy to announce this issue has been mitigated in Symfony. So it’s not visible anymore on a Symfony project version >= 4.4.2 or >= 5.0.2.
Last week I spent a couple of hours with my buddy Grégoire on a surprising memory leak while running PHPUnit tests. We tried the best known ways of debugging such issues:
- php-meminfo by Benoit Jacquemont, but our 200 megabytes of lost memory where not visible;
- Blackfire, where we could see the memory usage increase but not really where it was going.
As we were attending the Forum PHP (a great PHP conference in Paris) I reached for help Romain Neutron, technical lead at Blackfire, and Nicolas Grekas, core contributor of Symfony.
Section intitulée what-my-tests-are-doingWhat my tests are doing
I have a simple integration test called SpiderTest
. It takes a page (the homepage basically) and clicks on all the local links to check their HTTP status. So this test uses the Symfony 4.3 KernelBrowser
a lot, and runs ~50 fake HTTP requests.
As my list of links grew, memory became an issue, to the point that tests were running out of it:
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.
Testing REDACTED PROJECT Suite
............................................................... 63 / 167 ( 37%)
...........................PHP Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 45056 bytes) in /home/app/app/vendor/twig/twig/src/Template.php on line 401
PHP Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 53248 bytes) in /home/app/app/vendor/twig/twig/src/Compiler.php on line 129
PHP Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 53248 bytes) in Unknown on line 0
I quickly identified the leaking test to a single one, the Spider, which code is shown below:
/**
* @dataProvider provideAllPages
*/
public function testPage(\DOMElement $link)
{
$path = $link->getAttribute('href');
$client = static::createClient();
$client->followRedirects(true);
$client->request('GET', $path);
$this->assertSame(200, $client->getResponse()->getStatusCode());
}
Here is what we could see in the Blackfire timeline:
Each HttpKernel
Client request call costs memory (the blue background is the memory) where it should just properly garbage collect everything.
There could be a lot of reasons for this memory leak so we tried a number of things:
- disabling all logging and profiling;
- running some parts of our code in loops (like Elastically calls, database queries…) to ensure there is no leak in it;
- calling the garbage collector ourselves…
And none of these solutions worked.
Section intitulée an-issue-in-php-itselfAn issue in PHP itself
When the KernelBrowser
performs a request, the Symfony Kernel is shut down and (re)booted. This is done to simulate the real experience of an HTTP query received by Symfony: you always have a fresh container.
So for every query, a new container has to be loaded. Loading a container is done either by including existing files, or dumping them before requiring them. Either way, there is an include
or a require
happening.
Those files live in the var/cache
directory, and you can have a look at them: it’s full of arrays, anonymous functions, class instantiations…
That’s where Nicolas great knowledge helped us a lot, there is a reported issue in PHP that may be related to our case: https://bugs.php.net/bug.php?id=76982. It says that when you require a file declaring a closure in a loop, the memory usage increases continuously, so it definitely hit hard on Symfony container loading!
Section intitulée the-quick-and-dirty-solutionsThe quick and dirty solutions
Since the bug is in PHP, and I can’t change the way a Symfony container is built, I found two user-land solutions to avoid leaks:
Section intitulée 1-running-insulated-queries1. Running insulated queries
The BrowserKit
Client has a insulated
option that you can use to run your test requests in a new PHP process. As the request is run in a throwaway process, the memory of your main test is not impacted, but it will be slower.
$client = static::createClient();
$client->insulate(true);
Section intitulée 2-disabling-reboot-between-queries2. Disabling reboot between queries
There is another method called disableReboot
allowing to reuse the same Kernel and container on subsequent queries.
$client = static::createClient();
$client->disableReboot();
You have to be careful with this method, especially if your services are not stateless. But you can implement the kernel.reset
tag to “clean” them between requests.
Section intitulée final-wordsFinal words
As suggested by Grégoire, I also tried a new option of Symfony 4.4: a container dumping strategy to write only one file with all the definitions, instead of multiple ones. Sadly that did not work, as the included file is still full of closures.
What I learned from this experience is that if two great tools can’t find the leak, that’s probably because it’s in PHP itself! I should have turned to the PHP bug tracker way earlier!
Thanks again to Nicolas, Romain and Grégoire for the help! The PHP community is a great place to work and I’m glad events like Forum PHP exist: they bring everyone together and allow for greater collaboration and constructives exchanges!
Commentaires et discussions
Ces clients ont profité de notre expertise
LOOK Cycle bénéficie désormais d’une nouvelle plateforme eCommerce disponible sur 70 pays et 5 langues. La base technique modulaire de Sylius permet de répondre aux exigences de LOOK Cycle en terme de catalogue, produits, tunnel d’achat, commandes, expéditions, gestion des clients et des expéditions.
Travailler sur un projet US présente plusieurs défis. En premier lieu : Le décalage horaire. Afin d’atténuer cet obstacle, nous avons planifié les réunions en début d’après-midi en France, ce qui permet de trouver un compromis acceptable pour les deux parties. Cette approche assure une participation optimale des deux côtés et facilite la communication…
À l’occasion de la 12e édition du concours Europan Europe, JoliCode a conçu la plateforme technique du concours. Ce site permet la présentation des différents sites pour lesquels il y a un appel à projets, et encadre le processus de recueil des projets soumis par des milliers d’architectes candidats. L’application gère également toute la partie post-concours…