Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Symfony 2.5 leaks memory during functional tests #11236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
andreausu opened this issue Jun 26, 2014 · 39 comments
Closed

Symfony 2.5 leaks memory during functional tests #11236

andreausu opened this issue Jun 26, 2014 · 39 comments

Comments

@andreausu
Copy link

Since upgrading to symfony 2.5 I've been experiencing heavy memory leaks during the test suite execution on my application.

We're using the client provided by symfony (WebTestCase) to do functional testing of the application, we have ~300 tests with ~1800 assertions and on symfony 2.4 the whole test suite finished in about 3 minutes while consuming only ~100mb of RAM. After upgrading to symfony 2.5 the test suite goes out of memory very soon, consuming more memory at every test. Using a memory_limit of 512mb it stops at about 20%.

I tried upgrading to symfony dev-master but the issue remains.

I set phpunit's processIsolation flag to true to mitigate the problem, but the test suite is very slow when run with that flag.

I'm not the only one who experienced this problem: https://groups.google.com/forum/#!topic/symfony2/IItwqezE_PY

@thunderer
Copy link
Contributor

Have you tried to isolate the issue by running single tests and / or skipping them?

@apfelbox
Copy link
Contributor

Maybe it's related to #11221?

@stof
Copy link
Member

stof commented Jun 27, 2014

@apfelbox nope, because this CssSelector cyclic object graph is there since 2.3

@apfelbox
Copy link
Contributor

Ah, I didn't see the

Since upgrading to symfony 2.5

Ok, in this case it's probably not (directly) related.

@andreausu
Copy link
Author

I printed out the memory consumption inside the setUp() method:

SF 2.4: https://gist.github.com/anonymous/757028ea2742b6e29cea
SF 2.5: https://gist.github.com/anonymous/bb6b8f6aabfc1bc1d09c

Every test call the function WebTestCase::createClient(); to get a test client, and since the biggest memory gains during test execution happen within tests that instantiate multiple clients I think that the culprit could be that function.
Any ideas on how to debug this further?

Thank you!

@shoomyth
Copy link

Wow, ::testBackofficeTl from 64mb to 130. Maybe it'd be reasonable if you paste this method too.
You can also check xdebug traces and see what's going on on both versions

@andreausu
Copy link
Author

Base class: https://gist.github.com/anonymous/b82c4427829c6301cd9f
Actual test class: https://gist.github.com/anonymous/ce913836da7f7b2d7cf3

I think it's more evident in AclTest because it instantiates many clients opposed to other tests that instantiate only one, but the RAM grows steadily at every test.

I really can't figure out why it started happening in SF 2.5, I tried reading the commit log but I wasn't able to find anything related to this.
Do you have any recommended xdebug settings I can run the trace with?

Thank you!

@arthens
Copy link

arthens commented Jun 30, 2014

We are having the same problem. Memory usage in tests went from 76.75Mb to 145.25Mb after upgrading to Symfony 2.5.

@notrix
Copy link

notrix commented Jun 30, 2014

Our company has 140 functional tests on SF 2.5 and with no process isolation we have reached 3GB ram! With process isolation no memory leaks, but it takes 40 min. just to tun tests and 37 hours to run tests with coverage.

@ghost
Copy link

ghost commented Jun 30, 2014

@andreausu : if you wanna track down where exactly it happened then you could run git bisect to find the regression.

Short tutorial is here if you aren't familiar with it:
http://git-scm.com/book/en/Git-Tools-Debugging-with-Git

@arthens
Copy link

arthens commented Jun 30, 2014

I did something similar, and this is the commit that introduced the memory leak:

bd577b1

Unfortunately it's a quite large change.

@arthens
Copy link

arthens commented Jul 1, 2014

@andreausu can you confirm that if you revert Symfony to 880880b the memory leak doesn't happen?

@andreausu
Copy link
Author

I confirm that the bug was introduced by bd577b1

Thank you @arthens

@stof
Copy link
Member

stof commented Jul 1, 2014

@webmozart could you take a look at it ?

@stof stof added the Validator label Jul 1, 2014
@arthens
Copy link

arthens commented Jul 15, 2014

Any update on this? This bug is holding us back from upgrading to Symfony 2.5

@stof
Copy link
Member

stof commented Jul 15, 2014

@arthens the core dev who wrote the new validator is @webmozart and he will come back from vacation in the net few days (unless he is already back since this weekend, I don't remember). I expect to see some progress on this once he is back working on Symfony.

@arthens
Copy link

arthens commented Jul 15, 2014

Thanks for the update.

@webmozart
Copy link
Contributor

Thank you for reporting this issue! There seems to be a memory leak in the new API of the Validator component. Before I start digging further, could you please check whether #11412 changes anything?

@andreausu
Copy link
Author

Thank you or looking into this, unfortunately that commit doesn't fix the issue and I don't see any changes in the way it goes out of memory.

@webmozart
Copy link
Contributor

Ok, thanks. If you want to help speeding up the fix, you could create a fork of symfony-standard that reproduces the memory leak. That would help me tremendously.

andreausu pushed a commit to andreausu/symfony-standard that referenced this issue Jul 18, 2014
@andreausu
Copy link
Author

Here it is: https://github.com/andreausu/symfony-standard/tree/2.5-memory-leak

Just clone it and run app/run-tests.sh from the 2.5-memory-leak branch.

My results:

sf 880880b: Time: 7.68 seconds, Memory: 34.75Mb
sf 2.5.2: Time: 9.64 seconds, Memory: 259.25Mb

Thank you!

@webmozart
Copy link
Contributor

Thank you so much! I narrowed the source of the error down to the following diff: 0946dbe...b1badea Still searching.

@webmozart
Copy link
Contributor

Fixed in #11454. Thanks again for your test project, this was a tremendous help!

@andreausu
Copy link
Author

Thank you so much Bernhard!

fabpot added a commit that referenced this issue Jul 23, 2014
This PR was merged into the 2.5 branch.

Discussion
----------

[Validator] Fixed memory leak in ValidatorBuilder

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #11236
| License       | MIT
| Doc PR        | -

In 23534ca, the following code was introduced in `ValidatorBuilder::getValidator()`:

```php
AnnotationRegistry::registerLoader(function ($class) {
    if (0 === strpos($class, __NAMESPACE__.'\\Constraints\\')) {
        $file = str_replace(__NAMESPACE__.'\\Constraints\\', __DIR__.'/Constraints/', $class).'.php';

        if (is_file($file)) {
            require_once $file;

            return true;
        }
    }

    return false;
});
```

`AnnotationRegistry::registerLoader()` stores all loaders in a global array. Every time that `getValidator()` is called, a new closure is put onto the loader stack, referencing `$this` (i.e. the ValidatorBuilder) and consequently preventing the ValidatorBuilder (and its references) from being garbage-collected.

The call to `registerLoader()` did not exist in 2.4 and I can't find a reason why it should be there. All tests are green without that code. I suppose I used it for debugging and then forgot to remove it again, so I'm removing it here.

Commits
-------

283387a [Validator] Fixed memory leak in ValidatorBuilder
@fabpot fabpot closed this as completed Jul 23, 2014
@arodiss
Copy link

arodiss commented Jul 24, 2014

Thank you @webmozart - now I can upgrade to 2.5!
(well, in fact only 2.5@dev for now)

@eduardosoliv
Copy link
Contributor

A new tag would be welcome

@stefreak
Copy link

stefreak commented Aug 5, 2014

+1 for new tag

@webmozart
Copy link
Contributor

The next 2.5 patch release is going to be released this week.

@eduardosoliv
Copy link
Contributor

Great

@arturkarczmarczyk
Copy link

Doctrine might be guilty for so much memory leaks. We have had this problem with our functional tests and what has helped in Symfony 2.7 is disabling logging queries in memory. Here's the code that made the magic for us:

class OurTestCase extends WebTestCase
{
    public function __construct($name = null, array $data = array(), $dataName = '')
    {
        parent::__construct($name, $data, $dataName);

        if (!static::$kernel) {
            static::$kernel = self::createKernel(array(
                'environment' => 'test',
                'debug'       => true
            ));
            static::$kernel->boot();
        }

        $this->container = static::$kernel->getContainer();
        $this->em = $this->container->get('doctrine.orm.entity_manager');

        // disabling logging sqls for memory leakage stop
        $this->em->getConnection()->getConfiguration()->setSQLLogger(null);
        foreach (array('monolog.logger.doctrine', 'logger') as $service) {
            $logger = $this->container->get($service);
            $logger->pushHandler(new \Monolog\Handler\NullHandler());
        }
    }
}

I hope it helps

@stof
Copy link
Member

stof commented Apr 28, 2015

@arturkarczmarczyk a better solution would be to disable the doctrine query logging in the config_test.yml file

@arturkarczmarczyk
Copy link

@stof Thank you for your comment. We've modified the config_test.yml file and it works the same for us. Thanks!

Here's the excerpt from our config_test.yml if someone wanted to give it a try:

doctrine:
    dbal:
        driver:   pdo_sqlite
        path:     %kernel.cache_dir%/test.db
        charset:  UTF8
        logging: false
        profiling: false

@ramunasd
Copy link

ramunasd commented Aug 5, 2015

All solutions are fine but they does not solve circular references across all loaded services. We use full container instance removal from all services using reflection:

/**
     * {@inheritdoc}
     */
    public function shutdown()
    {
        if (false === $this->booted) {
            return;
        }

        if ($this->environment != 'test') {
            return parent::shutdown();
        }
        // do cleanup to release memory in test environment
        gc_disable();
        $this->cleanupContainer();
        parent::shutdown();
        gc_collect_cycles();
        gc_enable();
    }

    /**
     * Remove all container references from all loaded services
     */
    protected function cleanupContainer()
    {
        $container = $this->getContainer();
        $object = new \ReflectionObject($container);
        $property = $object->getProperty('services');
        $property->setAccessible(true);
        foreach ($property->getValue($container) as $id => $service) {
            if ($id == 'kernel') {
                continue;
            }
            $serviceObject = new \ReflectionObject($service);
            foreach ($serviceObject->getProperties() as $prop) {
                $prop->setAccessible(true);
                if (!$prop->getValue($service) instanceof \Symfony\Component\DependencyInjection\Container) {
                    continue;
                }
                try {
                    $prop->setValue($service, null);
                } catch (\ReflectionException $e) {
                }
            }
        }
        $property->setValue($container, null);
    }

This loop allows remove almost all services from memory without any negative impact on tests.

Before out tests consumed over 1G memory - now just 200M.

@sstok
Copy link
Contributor

sstok commented Aug 5, 2015

As of Symfony 2.8 this will be much easier #15185

@stof
Copy link
Member

stof commented Aug 5, 2015

@ramunasd can you check whether #15185 improves things when running your testsuite without your Reflection-based cleanup ?
It performs the breaking of loops between services and the container the other way (much simpler and no need for Reflection to break object boundaries): it removes references hold by the container.

@ramunasd
Copy link

ramunasd commented Aug 5, 2015

@stof I've tried #15185 solution. It does not helps.

Actually is not enough to clean only container attributes. This is because there can be many services with cross references that holds container instance.

@stof
Copy link
Member

stof commented Aug 5, 2015

@ramunasd well, even if the all hold the container instance, this instance will be empty so it won't keep reference to services.
And your own logic removing the container from them does not change this fact once the container is reset. I guess that what helps you is that you also force the garbage collector to run on each shutdown to collect object cycles each time rather than waiting until PHP decides to run it. However, forcing this on each shutdown in Symfony is a bad idea IMO: if your object graphs are not cyclic, the reference counting will already be enough to destruct them now that the container does not keep them anymore, which means that forcing a GC run could actually make the testsuite slower for no real benefit.

On a side note, avoiding circular object graphs whenever possible is a good idea: destructing objects based on the refcount is more efficient than forcing the GC to enter in action to collect them

@ramunasd
Copy link

ramunasd commented Aug 5, 2015

@stof It's not perfect solution, this is only mine experiment :) I tough it could help for others like it helped me.

Btw, in my case tests runs even faster with container reference clean up.

@estahn
Copy link

estahn commented Mar 22, 2016

@stof The solution @ramunasd provided works quite well. In total the memory consumption seems to be reduced, which in our case forced the CI server to throw an out of memory fatal error (PHP Fatal error: Allowed memory size of 1073741824 bytes exhausted).

With the fix we finish all tests at 132Mb.

Btw, we're using a slightly adjusted version:

    /**
     * Remove all container references from all loaded services
     */
    protected function cleanupContainer($container, $exclude = ['kernel'])
    {
        $object = new \ReflectionObject($container);
        $property = $object->getProperty('services');
        $property->setAccessible(true);

        $services = $property->getValue($container) ?: [];
        foreach ($services as $id => $service) {
            if (in_array($id, $exclude, true)) {
                continue;
            }

            $serviceObject = new \ReflectionObject($service);
            foreach ($serviceObject->getProperties() as $prop) {
                $prop->setAccessible(true);

                if ($prop->isStatic()) {
                    continue;
                }

                $prop->setValue($service, null);
            }
        }

        $property->setValue($container, null);
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests