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

Skip to content

[DI] Generate one file per service factory #23678

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
wants to merge 2 commits into from

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Jul 26, 2017

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? WIP
Fixed tickets -
License MIT
Doc PR -

With this PR, when the new "as_files" option is set on the dumper, it generates an array of files, one per service factory, private or public ones. That's it, nothing else but implementation details.

The benefit should be exactly the same as autoload for class definitions: only the service factories required for the current request are being loaded.

On the technical background, you may wonder if this is relevant with OPcache. If you inspected serious apps with Blackfire, you may have noticed that the container file can take some significant time+memory when the number of services grow. The reason is that even if the class is pre-compiled in shared memory by OPcache, its opcode array still needs to be copied in the memory of the current request. That takes time, and memory.

This PR builds also on static arrays and interned strings to lower the CPU+RAM footprint of the container to the strict minimum.

Yes, doing a require, even an OPcached one, is slower than running an already loaded method. But that number of require/method is low (one per instantiated service, no more). In the end, the dead code elimination provided here wins.

Of course, if anyone can help provide actual numbers, that'd be really awesome.

First bench, front page of the standard edition:

  • profiler shows that memory goes from 4MB to 2MB and no eye-visible diff in wall time
  • ab -n 100 goes from 1.55s to 1.50s
  • blackfire measures -1% in wall time and -20% in memory

Of course, the page is so thin that measuring the effect of the patch is hard. Yet, all measuring tools go in the same direction, Yay! If the benefit is measurable for a simple app, the benefit for real apps is going to be much higher. Looks like the container can now scale with a growing number of services :)

When the "as_files" flag is not set, the same code as before is generated. I've updated the Kernel to set that flag in non-debug envs, so that prod has the optimized code loading strategy, and devs can still open a single file to inspect the factories easily.

}

if (null === $methodName) {
throw new \InvalidArgumentException(sprintf('Missing name of method to call to construct the service "%s".', $id));
}

$factoryCall = sprintf($definition->isPublic() || !method_exists(ContainerBuilder::class, 'addClassResource') ? '$container->%s(false)' : '%s::create($container, false)', $methodName);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

passing the factory class instead of the method name may need to be done another way to preserve BC

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed via #23693

@@ -456,3 +498,20 @@ protected function getDefaultParameters()
);
}
}

final class ProjectServiceContainer_FactorySimpleService extends ProjectServiceContainer
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here is the kind of class that this generates

@nicolas-grekas nicolas-grekas force-pushed the di-split branch 2 times, most recently from a714810 to 736bc22 Compare July 26, 2017 10:16
@javiereguiluz
Copy link
Member

About this:

I'm playing with the idea of splitting the container class in several files.

I can see added complexity in three ways:

  • The logic that generates the compiled container will be more complex.
  • Finding a problem in the compiled container will be more complex, because there's no longer one file to look for but a lot.
  • Performance should be worse. It's just a guess, but having multiple PHP files instead of 1 PHP file should be slower for some things (PHP parsing?, autoloading?)

So, which are the benefits that I'm missing i this proposal? Thanks!

@nicolas-grekas
Copy link
Member Author

Performance should be worse. It's just a guess

It's a guess, so if you're wrong, you have your benefit, there is no other.
As written: "to be proved useful". Only a bench will tell.
We already removed classes.php because having everything in one file proved being slower in fact.
I'd like to try if that applies to the container also. Might benefit large ones more than others.

@robfrawley
Copy link
Contributor

Sounds interesting. Is this PR functional? I have access to a few fairly large (monolithic) applications I'd be happy to test this against.

@nicolas-grekas
Copy link
Member Author

@robfrawley not ready yet, I'll report back :)

@nicolas-grekas nicolas-grekas force-pushed the di-split branch 3 times, most recently from e5c35a7 to 88aea90 Compare July 27, 2017 20:49
fabpot added a commit that referenced this pull request Jul 28, 2017
…umperInterface::getProxyFactoryCode() (nicolas-grekas)

This PR was merged into the 3.4 branch.

Discussion
----------

[DI][ProxyManager] Pass the factory code to execute to DumperInterface::getProxyFactoryCode()

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

Passing the full code to call the factory is more flexible, as spotted while working on #23678.

Commits
-------

0754617 [DI][ProxyManager] Pass the factory code to execute to DumperInterface::getProxyFactoryCode()
@nicolas-grekas nicolas-grekas force-pushed the di-split branch 6 times, most recently from dc0fc39 to 9628fbe Compare July 29, 2017 14:12
@nicolas-grekas nicolas-grekas changed the title [DI] Generate one factory class per private service [DI] Generate one file per service factory Jul 29, 2017
@nicolas-grekas
Copy link
Member Author

@ogizanagi should be fixed (lazy proxies are not split anymore because that needs more work - another PR maybe.)

@nicolas-grekas nicolas-grekas force-pushed the di-split branch 4 times, most recently from 1b81099 to e91ba87 Compare July 31, 2017 21:09
@ogizanagi
Copy link
Contributor

ogizanagi commented Aug 1, 2017

So I tried in a real-life app, and here are some new blackfire profile comparisons:

No significative diff at all it seems :/

In addition, note the var/cache/prod dir also grew from 2.9M to 8.5M (for ~270 services non-inlined services).

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Aug 1, 2017

3.4 or 4.0? What about testing using ab?

@ogizanagi
Copy link
Contributor

Using Symfony 4.0.

Tests using ab do not show any noteworthy difference either.

@nicolas-grekas
Copy link
Member Author

Profiling Blackfire itself, which has a pretty serious container, I consistently get slightly better results with the patch enabled (using patch in#23741 to be compatible with it.)

A Blackfire profile doesn't notice any conclusive wall time improvement, but on the memory side, the reduction is consistent:
capture du 2017-08-02 13-31-16

Using ab, things are more interesting: I go from 104 req/s. to 108 req/s. Not tremendous, but still nice to have have a +4% perf boost just by upgrading.

@jpauli
Copy link

jpauli commented Aug 3, 2017

OPCache needs to copy from SHM to current process local memory all the classes it parses.
All the classes, means all their constants, static attributes, declared attributes, functions (op_arrays), used traits, and rebind the class (re-traverse the inheritence tree).

This process is highly optimized in OPCache, we even designed an SSE2 SIMD process for part of such a work (https://github.com/php/php-src/blob/PHP-7.1/ext/opcache/zend_accelerator_util_funcs.c#L605)

But it will still eats some little CPU cycles, and above all, will eat "a lot" of memory because a class is really a heavy stuff into PHP in term of memory footprint (much more than an object).

All this process is detailed in an article I once wrote (targeting PHP 5) at http://jpauli.github.io/2015/03/05/opcache.html

Source code parts are available at https://github.com/php/php-src/blob/PHP-7.1/ext/opcache/zend_accelerator_util_funcs.c#L561 and https://github.com/php/php-src/blob/PHP-7.1/ext/opcache/zend_accelerator_util_funcs.c#L341

The current PR should improve performances by only loading the required symfony services, and not load a very very huge class that is the container.
Loading a huge class, just to use a little part of its methods at runtime, is a waste of resources.

fabpot added a commit that referenced this pull request Aug 3, 2017
…tiated services (nicolas-grekas)

This PR was merged into the 2.7 branch.

Discussion
----------

[Bridge\ProxyManager] Dont call __destruct() on non-instantiated services

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

While working on making #23678 green, I discovered that if a lazy service implements `__destruct`, then that service is not lazy anymore: it is created at destruct time.
That behavior is documented at Ocramius/ProxyManager#258 (+related issues).

While I may understand why this behavior is the default for ProxyManager, it does not fit our "lazy-services" use case to me. Typically, nobody wants a database connection to be created to destruct the uninitialized lazy-proxy.

Blocks #23678

Commits
-------

2d79ffa [Bridge\ProxyManager] Dont call __destruct() on non-instantiated services
@nicolas-grekas
Copy link
Member Author

I'm now targeting 3.4, see #23741

@nicolas-grekas nicolas-grekas deleted the di-split branch August 3, 2017 15:51
fabpot added a commit that referenced this pull request Aug 7, 2017
…ekas)

This PR was merged into the 3.4 branch.

Discussion
----------

[DI] Generate one file per service factory

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

See #23678 for background on this proposal.

Commits
-------

4037009 [DI] Generate one file per service factory
@nicolas-grekas
Copy link
Member Author

The most significant improvement of this PR should be on the CLI, where OPcache doesn't work.
See #23601 (comment)

@@ -671,9 +671,29 @@ protected function dumpContainer(ConfigCache $cache, ContainerBuilder $container
$dumper->setProxyDumper(new ProxyDumper(md5($cache->getPath())));
}

$content = $dumper->dump(array('class' => $class, 'base_class' => $baseClass, 'file' => $cache->getPath(), 'debug' => $this->debug));
$content = $dumper->dump(array('class' => $class, 'base_class' => $baseClass, 'file' => $cache->getPath(), 'as_files' => true, 'debug' => $this->debug));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolas-grekas What is the reason to hardcode as_files value?

After upgrading to Symfony 3.4 our tests won't run on the CI platform because they hit the open files limit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because there are many benefits to splitting the container into several files.
ow does this relates to fopen any limit? Service files should be closed by PHP as soon as they are consumed, isn't it?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolas-grekas I meant the limit imposed by /etc/security/limits.conf (which includes all loaded files).

You maintain here that you've "updated the Kernel to set that flag in non-debug envs", but this is not the case.

Is there any way to get around this option apart from overriding Kernel:dumpContainer() (which doesn't seem a good idea)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/etc/security/limits.conf (which includes all loaded files)

I only see nofile, which limits currently open files. Doesn't PHP close them once executed, thus the culprit is elsewhere?

get around this option

not really as this should work for everyone...

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does close them, except if the file is autoloaded, and needs to autoload another file, in such a case, the chain will remain open until last statement

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We run the tests with coverage and thus with phpdbg which may be the curlpit, as you suggested...

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, phpdbg has the max_open_file problem , I dont really know why , I guess it loads PHP files differently from other SAPI , I should have a look at the source code for that.

You should increase the limit by using ulimit -n

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolas-grekas @jpaul
Thank you for this feedback.
Unfortunately ulimits are not available on Codeship.

lcobucci added a commit to lcobucci/di-builder that referenced this pull request May 6, 2018
This option has been added in `symfony/dependency-injection` v4 and the
idea is to reduce the amount of files/classes loaded when the container
is initialised, thus reducing memory usage and speeding up things.

More info: symfony/symfony#23678
lcobucci added a commit to lcobucci/di-builder that referenced this pull request May 6, 2018
This option has been added in `symfony/dependency-injection` v4 and the
idea is to reduce the amount of files/classes loaded when the container
is initialised, thus reducing memory usage and speeding up things.

More info: symfony/symfony#23678
lcobucci added a commit to lcobucci/di-builder that referenced this pull request May 6, 2018
This option has been added in `symfony/dependency-injection` v4 and the
idea is to reduce the amount of files/classes loaded when the container
is initialised, thus reducing memory usage and speeding up things.

More info: symfony/symfony#23678
lcobucci added a commit to lcobucci/di-builder that referenced this pull request May 6, 2018
This option has been added in `symfony/dependency-injection` v4 and the
idea is to reduce the amount of files/classes loaded when the container
is initialised, thus reducing memory usage and speeding up things.

More info: symfony/symfony#23678
lcobucci added a commit to lcobucci/di-builder that referenced this pull request May 6, 2018
This option has been added in `symfony/dependency-injection` v4 and the
idea is to reduce the amount of files/classes loaded when the container
is initialised, thus reducing memory usage and speeding up things.

More info: symfony/symfony#23678
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.