Problem/Motivation

The proxyBuilder components currently creates proxy classes which look like the following and writes them into the dumped container:


class Drupal_Core_ExampleClass implements Drupal\Core\Example\ExampleInterface {}

While this works perfect for core classes, it doesn't work for interfaces existing in modules.

The problem is that the autoloader for modules is registered AFTER the container is loaded, because it depends on $container->getParameter('container.namespaces').

Proposed resolution

  • Instead of generating the proxy classes on container rebuild time, we use proxies dumped to PHP files, registered to the same namespace as the original class, but with an added 'ProxyClass' prefix in the PSR-4 section, e.g. core/modules/views_ui/src/ViewUIConverter.php becomes core/modules/views_ui/src/ProxyClass/ViewUIConverter.php and hence \Drupal\views_ui\ViewUIConverter.php searches for a proxy class called \Drupal\views_ui\ProxyClass\ViewUiConverter.php.
  • To generate those files, this issue adds a script: generate-proxy-class.php>.
  • This patch already includes generated proxies for all services that are currently flagged lazy in core.
  • A compiler pass now checks for each lazy flagged entry, whether we have a corresponding lazy class available and then overrides the definition
    and moves the existing definition to "drupal.proxy_original_service.$original_service_id"
  • In case that a proxy class is not provided, the container builder throws an Exception (with a @todo to move it to an Assertion once those exist).

This has several advantages:

  • a) the container build time gets faster
  • b) the actual dumped container is smaller, which adds more speed to any request
  • c) It also fixes the main problem of the issue: The requirement to load interfaces, even you don't use them in the first place.
  • d) it allows you to use proxies for modules, which is critical for performance.
  • e) It decouples proxy services from the PHP Container Dumper.
  • f) It removes the dependency of using PHP Storage for proxy services.

Remaining tasks

- Fix tests
- Commit

User interface changes

- None

API changes

- To make a service lazy needs manual work now (run the script for the lazy made class / service).

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Bug, because using proxies should not be restricted to core and not load interfaces of proxied classes at container loading time.
Issue priority Critical, because of performance and scalability.
Prioritized changes The main goal of this issue is performance and scalability.
Disruption Not disruptive for core/contributed and custom modules/themes because lazy services were not available for modules.

Comments

wim leers’s picture

Wow :(

dawehner’s picture

One thing we would do is to also write the interfaces into the container. They don't have to then loaded later anymore.

bforchhammer’s picture

Status: Active » Needs review
Issue tags: +SprintWeekend2015, +SWB2015
StatusFileSize
new3.01 KB

I'm not sure this is supposed to work... oh php is fun :)

Status: Needs review » Needs work

The last submitted patch, 3: proxies_of_module-2408371-3.patch, failed testing.

alexpott’s picture

bforchhammer’s picture

I should note that the patch in #3 actually seems to solve the issue fairly well. The only failing tests (as far as I can see) are the ones which are concerned with the output of the proxy-builder component, and can probably be fixed easily. I have been meaning to do that, but just haven't found the time...

fabianx’s picture

Issue tags: +Performance

#3 looks great to me and will improve performance, too!

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new3.62 KB
new5.58 KB

Fixed the tests.

fabianx’s picture

Issue tags: +needs profiling

We need to ensure an opcode cache is used for the classes still, so need some profiling.

catch’s picture

Pretty sure the opcode for the classes themselves won't be cacheable, at least according to http://marc.info/?l=pear-dev&m=118365656701421&w=2

I'd still expect this to be better than loading all the interfaces, but not sure how well we can measure this.

Status: Needs review » Needs work

The last submitted patch, 8: 2408371.8.patch, failed testing.

alexpott’s picture

Priority: Major » Critical
Status: Needs work » Needs review
StatusFileSize
new6.35 KB
new792 bytes

I'm bumping this to critical since by marking the Update module's services as lazy we can save over 1mb per admin page get and thousands of function calls.

Profiling the user permissions page as user 1...

Run #557ff13e23a93 Run #557ff183384b7 Diff Diff%
Number of Function Calls 301,598 277,618 -23,980 -8.0%
Incl. Wall Time (microsec) 890,760 769,547 -121,213 -13.6%
Incl. MemUse (bytes) 44,881,928 43,249,176 -1,632,752 -3.6%
Incl. PeakMemUse (bytes) 48,886,400 47,464,008 -1,422,392 -2.9%

I think the above performance gains and how this will allow contrib to enjoy the same performance as core makes this critical.

Status: Needs review » Needs work

The last submitted patch, 12: 2408371.12.patch, failed testing.

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new3.78 KB
new9.23 KB

Fixed the ConfigImporter ... SimpletestTest and test inception is proving more difficult.

Repeated profiling from #12... slightly less improvement in function calls... caches must not have been entirely warmed but the memory improvement is still significant.

Run #558004f281d16 Run #558004cb4523d Diff Diff%
Number of Function Calls 277,880 277,618 -262 -0.1%
Incl. Wall Time (microsec) 793,422 776,497 -16,925 -2.1%
Incl. MemUse (bytes) 44,089,568 43,252,088 -837,480 -1.9%
Incl. PeakMemUse (bytes) 48,309,504 47,466,696 -842,808 -1.7%
fabianx’s picture

Overall: +1

I am not sure the function approach gives us op-code cached classes. If it does => Great, but if not we might introduce a performance regression for after-page-cache.

That we would need to test in isolation.

Also:

My opinion is proxies should be:

a) optional
b) autoloaded from \Drupal\[module]\DrupalLazyProxy\[NS]\[ClassName]

and statically created for each module and committed to GIT.

If the proxy class cannot be loaded, fall back to the real service, which is possible.

That would make:

a) the container lighter
b) ensure we don't cache information that is purely static usually

--

Allow a contrib author to just do e.g.:

"lazy: true"
drush build-lazy-proxy

or

"lazy: true"
./core/scripts/build-lazy-proxy.sh [directory]

I think that is better than adding more and more information into the container.

Status: Needs review » Needs work

The last submitted patch, 14: 2408371.13.patch, failed testing.

wim leers’s picture

Why are update.module's services even being initialized on admin pages? I don't understand that part yet.

dawehner’s picture

See \update_page_top()
It tries to warn you in case new updates are available.

dawehner’s picture

One goal of the original proxy issue has been to allow any service to be marked as lazy, but you know, it actually failed in doing that.
Part of this should be that you could mark services as lazy as part of your site configuration, as it might be a helpful optimization.

I think dumping the proxy definitions into files is certainly a good idea. The container compiler steps could check whether we want to use the proxy classes, if available,
and then use the compiled static code.
The advantage of using that approach would also to potentially write custom proxies, which could be useful, loggers are one example.

I'm happy to work on this critical

dawehner’s picture

StatusFileSize
new6.43 KB

Just posting some progress,

chx’s picture

Is this going to be command line only? Do we have any other feature requiring command line? If this is a viable why dont we move rebuilding itself to command line? I do not fully understand the repercussions and target users here.

catch’s picture

@chx we discussed checking the proxies into git - so this would be more like a subset of 'module builder' than a rebuild operation.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new68.15 KB
new66.93 KB

Here is a "first" start of writing that command, adapting the proxy output and dump the core lazy services.

TODO: Insert the part discussed with chx this morning.

fabianx’s picture

Looks like a great start.

Status: Needs review » Needs work

The last submitted patch, 23: 2408371-23.patch, failed testing.

fabianx’s picture

Overall looks really really awesome!

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/ProxyServicesPass.php
    @@ -0,0 +1,33 @@
    +        $proxy_class = ProxyBuilder::buildProxyClassName($definition->getClass());
    +        if (class_exists($proxy_class)) {
    +          $definition->setClass($proxy_class);
    +        }
    

    I don't think that will work for factories or such.

    Should this not do:

    - Move the real definition to 'drupal_proxied.[service_name]'

    - Add a new definition with just the class (as that is all the proxy should care about).

    - Have the proxy get drupal_proxied.service_name instead.

    ( or drupal_proxy.real_service.[service_name])

    I was never a fan of re-using the same definition twice as accessing the getX methods is kinda a hidden implementation detail.

    The advantage:

    - Factories, special cases, etc. all work out of the box.

    => Nice isolation.

    => Could give the real service name as an argument to the proxy, so its transparent.

    ( => Fallback theoretically possible if proxy class cannot be found.)

  2. +++ b/core/lib/Drupal/Core/ProxyClass/Extension/ModuleInstaller.php
    @@ -0,0 +1,79 @@
    +            if (!isset($this->service)) {
    +                $method_name = 'get' . \Symfony\Component\DependencyInjection\Container::camelize($this->serviceId) . 'Service';
    +                $this->service = $this->container->$method_name(false);
    +            }
    

    This is an implementation detail of the container and not public API.

    E.g. it would break with my container ... ;)

    Should just use:

    $this->container->get($serviceId);

    instead.

    With moving the proxied class to another service name namespace (see above) this works well and with using that new namespace as an argument to the proxy, the proxies are independent of any implementation. They just know how to get the real service, regardless where it lives.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new84.63 KB
new37.03 KB

Thank you for your feedback fabian!

I was never a fan of re-using the same definition twice as accessing the getX methods is kinda a hidden implementation detail.

I agree, I just tried to do the last amount of changes as part of the patch, but I agree, better to use proper stuff. This also actually simplifies the code

- Factories, special cases, etc. all work out of the box.

To be clear, this will just work in case we have an interface.

Fixed those bits as well, but also added proxies for all the lazy services in modules.

Status: Needs review » Needs work

The last submitted patch, 27: 2408371-27.patch, failed testing.

deepakaryan1988’s picture

Issue tags: -SprintWeekend2015, -SWB2015
dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new84.75 KB
new2.84 KB

Typical sort problem ....

Status: Needs review » Needs work

The last submitted patch, 30: 2408371-30.patch, failed testing.

fabianx’s picture

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/ProxyServicesPass.php
    @@ -20,11 +21,20 @@ class ProxyServicesPass implements CompilerPassInterface {
             $proxy_class = ProxyBuilder::buildProxyClassName($definition->getClass());
             if (class_exists($proxy_class)) {
    

    I think we should move that check up to before creating the proxied service definition?

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/ProxyServicesPass.php
    @@ -20,11 +21,20 @@ class ProxyServicesPass implements CompilerPassInterface {
    +          $proxy_definition = new Definition($proxy_class);
    +          $proxy_definition->setArguments(['service_container', $new_service_id]);
    

    Uh, don't we need to register the proxy definition instead of the real definition?

    Also:

    'service_container' won't fly needs to be:

    new Reference('service_container')

    if my container foo is strong enough today :D.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new84.82 KB
new1.96 KB

Good point :)

This should make it.

Status: Needs review » Needs work

The last submitted patch, 33: 2408371-33.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new88.22 KB
new4.66 KB

Take this!!!

EntityValidationTest is doing evil things, so proxies don't make things easier, to be fair.

dawehner’s picture

Issue summary: View changes
StatusFileSize
new3.85 KB
new92.07 KB

Forgot to add the locally written tests to git.

Updated the issue summary ... and wrote a change notice regarding the use of proxies now.

fabianx’s picture

Overall looks really really great!

Some questions:

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/ProxyServicesPass.php
    @@ -0,0 +1,44 @@
    +        $proxy_class = ProxyBuilder::buildProxyClassName($definition->getClass());
    +        if (class_exists($proxy_class)) {
    

    I wondered a little what is better:

    a) fail silently as done here

    b) throw an Exception with the message to run the script for this file.

    After all you always have a choice to not mark things lazy and because now every module is responsible and hence "lazy: true" is matched to the module itself (that ships the proxy), it might be okay to throw an Exception.

    Not 100% sure though. Thoughts?

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/ProxyServicesPass.php
    @@ -0,0 +1,44 @@
    +          $container->register($service_id, $proxy_class)
    +            ->setArguments([new Reference('service_container'), $new_service_id])
    +            ->setMethodCalls($definition->getMethodCalls());
    

    Oh, what is the use case for the setMethodCalls?

    Would that not lead to the real service being loaded when the 'calls' call a method on the proxy?

dawehner’s picture

StatusFileSize
new91.31 KB
new3.33 KB

Good questions!

a) fail silently as done here

b) throw an Exception with the message to run the script for this file.

I was thinking about for a while and went with the silence, because my idea of a lazy service is built around the idea
that its entirely hidden to the outside world. If we are fair, especially with this change by moving it to PHP files, this is not longer true.

I guess we should better go with the exception.

On top of that I was wondering whether there should be a way to define a custom proxy class, for example when your custom site needs a proxy for a particular
service defined by a contrib module ...

Would that not lead to the real service being loaded when the 'calls' call a method on the proxy?

Well yeah this was a fix I needed before I introduced the proper interface for the clearer, but yeah I agree, all those additional properties are on the other service,
let's remove that again.

Status: Needs review » Needs work

The last submitted patch, 38: 2408371-38.patch, failed testing.

catch’s picture

a) fail silently as done here

b) throw an Exception with the message to run the script for this file.

Looks like a good case for an assertion?

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new93.94 KB
new9.03 KB

Looks like a good case for an assertion?

Good question, given that I'm not really sure exactly about the best place for assertions. Ideally we really want to ensure that we don't break the container, which could happen by a broken contrib module?

fabianx’s picture

#41: Well, no because we ignore the lazy in case that the class does not exist.

I think:

- Exception now (helpful for core as seen) and being explicit
- @todo pointing to Assertions
- Use Assertion once they exist

Overall looks great.

Anything you miss on this, dawehner or can I go to final RTBC review?

dawehner’s picture

StatusFileSize
new94.05 KB
new788 bytes

Anything you miss on this, dawehner or can I go to final RTBC review?

I think it would be great to get the measurements how much this actually improves things, maybe once for a page cache and maybe for normal rendering of let's see /node/1 or /node

Status: Needs review » Needs work

The last submitted patch, 43: 2408371-43.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
Issue tags: +Random test failure
StatusFileSize
new94.05 KB

Please don't retest it, this is an interesting failure

Status: Needs review » Needs work

The last submitted patch, 45: 2408371-43.patch, failed testing.

wim leers’s picture

The IS says:

Instead of generating the proxy classes on container rebuild time we build them using a custom script: generate-proxy-class.php>
and dump them into the file system, for example:

But it seems this patch is committing them? I'm confused now. We need an updated IS.

dawehner’s picture

But it seems this patch is committing them? I'm confused now. We need an updated IS.

Right we provide a script so people can generate their own.

fabianx’s picture

Issue tags: +D8 Accelerate

Yes, we need an IS update.

Anything actionable we can do on the test failure? Is this really random?

dawehner’s picture

Issue summary: View changes
Issue tags: -Random test failure

Does someone mind you to update the IS, I'm a bit blind what you think is cannot be understood.

Anything actionable we can do on the test failure? Is this really random?

No its not random at all. I think we just jumped over the memory limit for that test.

fabianx’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

Added issue summary updates

wim leers’s picture

Thanks, the IS is now 300% more clear.

dawehner’s picture

Thank you fabian!

fabianx’s picture

Issue summary: View changes
dawehner’s picture

Just a quick note: This also appears in case you just run a single test, see https://qa.drupal.org/pifr/test/1078273

Maybe someone has an idea

Crell’s picture

Hm. I am very very nervous about the required pre-compile step. It feels like it's in the same class as forcing theme developers to use Sass and then regenerate CSS.

I don't follow why we can't continue to generate the proxy files at runtime, and put them in PHPStorage. I understand this issue makes that not happen, but I do not see how that's a requirement of fixing the root issue (that modules can't use proxies right now). That seems like a huge step back to me DX-wise to force module developers to generate (and remember to regenerate) their own proxies.

Also, advantage B is not true: The size of the container itself has virtually no impact on performance as long as the file source is small enough to not bust the opcode cache limit for a file; I don't even know what that limit is in PHP 5.5, but APC had a limit. :-) I benchmarked that back when we were debating how to handle injection into controllers and there was no detectable performance difference from the size of the generated container.

chx’s picture

As some of you might remember, I've been a fan of generating things for a very, very long time. And it had tremendous performance promise #35657-15: split mode. We could've utilized it for unit testing. For a somewhat more recent development, see the Automatically generated entity classes per bundle for better IDE integration project -- as an aside, entity classes ought to be abstract and only these generated per bundle classes should be instantiatable.

But I can only agree with Crell in this: generating things on the fly by Drupal is a very different kettle of fish to pregenerating things (the paragraph above doesn't discern between them as it merely wanted to show I've been in this particular arena for a decade). I wrote phpstorage to make generating feasible. I feel we should utilize it a lot more (like entity classes per bundle should be core and everything else we can / want to crank performance) and not enforce command line scripts.

There are not that many lazy classes. Are we sure we can't do this on-the-fly?

dawehner’s picture

I don't follow why we can't continue to generate the proxy files at runtime, and put them in PHPStorage. I understand this issue makes that not happen, but I do not see how that's a requirement of fixing the root issue (that modules can't use proxies right now). That seems like a huge step back to me DX-wise to force module developers to generate (and remember to regenerate) their own proxies.

Do I saw chx and crell agreeing :)

So, the current still existing bootstrap flow:

  1. find the matching multisite
  2. load the matching container
  3. use container.modules in order to fill up the autoloader for modules.

In head the dumped lazy services are part of the container file, which then forces us to have the interfaces of the modules available, when we load that file.
Yes, we could probably write the individual files manually and then extend the autoloader to include the proxy classes. Is it worth, I don't know.

BUT, there are actual other reasons, why dumping things into proper files is a good thing. One is understandability. There is one less level of magic involved. Building things on runtime,
makes it hard to follow.

fabianx’s picture

Assigned: Unassigned » fabianx

Proxy services are a pretty special case.

The amount of effort is way less to add "lazy: true" and then run a script, than to e.g. write a block plugin from scratch. Therefore the effort vs. gain is clearly not a DX hinderance. You get a helpful exception or assertion if your proxy class is missing and we could add a check to load the proxy class (when assertions are in) to assert that the interface still matches (loading the class should be enough for that), or we could put a quick checksum on the interface.

Overall: No one forces you to use proxy services. It is mainly useful for core and core modules.

#57: I like PHP Storage, but we have long decided on many fronts that explicit is better than implicit, so why should we do work at run-time that can be done at compile time? Especially the MTimeProtectedFastFileStorage makes autoloading impossible atm.

--

Gonna take a look at the test failure today.

catch’s picture

Yes we have #2497143: Proxy services in the container force interfaces to be loaded tracking the performance issue of proxies in the container. When the proxy class itself isn't going to be used at all on the request (especially internal page cache, but likely more and more things once SmartCache is enabled), then you get a performance hit of loading interfaces that will never be used. It could easily be dozens of classes if there's a lot of services marked lazy. It's currently 12-15 which is about 1/5 of the classes loaded on page cache hits.

So we need them in separate PHP files either way.

However as alexpott has pointed out, when the proxy class is instantiated, we have to load that code which we currently get nearly for 'free' since it's already in the container. PHPStorage has a runtime performance hit and we'll get that for every interface.

This is an entirely optional API, so it's nothing like SASS, it's more like minification of JS files. So I really lean towards pre-generation here.

One thing we could do is skip the exception and use trigger_error() instead when building the container at least until we have asset(). Then when developing, you get nagged each container rebuild that you need to build the proxies, but you can mark something lazy:true and only generate the class at the last minute (or potentially we can get d.o packaging to do that for contrib modules).

catch’s picture

Issue tags: +D8 critical triage deferred

And...

We discussed this on the critical triage call.

This should not be a hard blocker to #2497243: Replace Symfony container with a Drupal one, stored in cache since it would be possible to commit that patch, have no functioning proxies for a while, then bring them back with this patch.

However that would mean introducing a performance regression. So if that patch is ready first, we should profile and see what the regression looks like without this one applied.

If that regression was not bad at all, we could even consider downgrading this to major and adding it back in a patch-level release (with the warning on missing proxies added in the next minor release), but need to bear in mind that as well as whatever the performance regression is, it also makes things harder profiling since you need to take the lack of proxies into account everywhere. Either way tagging for deferred triage and this looks close to ready anyway.

catch’s picture

Another reason not to use PhpStorage here - this came up in discussing with dawehner in irc.

Currently I do rm -rf sites/default/files/php/* quite regularly, since things get rebuilt afterwards. You can also delete any individual file or directory within that folder and things still work.

If we write the proxy classes there on container rebuilds, then:

1. With current state of HEAD, removing the proxy class files but not the container files would mean a fatal error.

2. With the container not compiled to PHP, rm -rf sites/default/files/* will get you in the same position since that won't invalidate the container either.

Twig is not like this - operates more like a cache, as does the compiled container.

catch’s picture

And last thing.

Adding the checksum seems optional and a major-follow-up to me. If we hash MyInterface.php then just changing a code comment will mean a new checksum - not what we want.

dawehner suggested it might be possible to hash the reflection object, but I think just expecting people to update the classes if they change the interface is reasonable. The whole point of interfaces is that they change very rarely - and you can just not mark a service lazy until your interface is finalized while iterating something in the first place.

dawehner’s picture

Catch had an idea how to debug the failures ... it turns out, with opcache disabled on PHP 5.5, this test requires 281809560 = 268MB
so I could imagine that you can get to the 335544320 bytes potentially quite easy.

fabianx’s picture

Assigned: fabianx » Unassigned

Unassigning for a moment as dawehner and catch check the test failure, too.

dawehner’s picture

Alright I executed all test functions of that test class, note that this is the peak memory usage, at the end of each test method.

Value '86769664:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testSiteInformationTranslationUi' is FALSE.
Value '104857600:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testSourceValueDuplicateSave' is FALSE.	
Value '121110528:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testContactConfigEntityTranslation' is FALSE.	
Value '136052736:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testNodeTypeTranslation' is FALSE.	
Value '150732800:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testDateFormatTranslation' is FALSE
Value '169869312:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testAccountSettingsConfigurationTranslation' is FALSE.	
Value '184287232:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testSourceAndTargetLanguage' is FALSE.	
Value '198443008:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testViewsTranslationUI' is FALSE.	
Value '212860928:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testLocaleDBStorage' is FALSE.	
Value '227016704:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testSingleLanguageUI' is FALSE.	
Value '241172480:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testAlterInfo' is FALSE.	
Value '263716864:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testSequenceTranslation' is FALSE.	

Here a comparison with HEAD

Value '78381056:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testSiteInformationTranslationUi' is FALSE.	
Value '83361792:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testSourceValueDuplicateSave' is FALSE.	
Value '84410368:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testContactConfigEntityTranslation' is FALSE.	
Value '85458944:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testNodeTypeTranslation' is FALSE.	
Value '87293952:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testDateFormatTranslation' is FALSE.	
Value '88604672:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testAccountSettingsConfigurationTranslation' is FALSE.	
Value '89391104:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testSourceAndTargetLanguage' is FALSE.	
Value '90439680:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testViewsTranslationUI' is FALSE.	
Value '91226112:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testLocaleDBStorage' is FALSE.	
Value '92274688:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testSingleLanguageUI' is FALSE.	
Value '93061120:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testAlterInfo' is FALSE.	
Value '93847552:Drupal\\config_translation\\Tests\\ConfigTranslationUiTest::testSequenceTranslation' is FALSE.	
catch’s picture

Put some extra debug into setUp() and it's WebTestCase::setUp() that has the memory leak. Especially running the installer and installing static $modules, so this suggests a memory leak in module install somewhere. Not really further than that though.

dawehner’s picture

StatusFileSize
new39.87 KB

I also found that the module installation processes is pretty much the problem in general.
Commented the modules used by this test and the effect got less, so it maybe just scales with the amount of lazy services or modules ...

In case we can't find the memory leak, here is an alternative solution with most of it part of the interdiff, it just doesn't work.

  • Don't register an extra service
  • Use the same mechansim and call out to the internal container method
  • This would drop support for the container from fabian again ..., which is kinda sad, to be honest, but that container could add explicit lazyness support.
  • Whether we want to go down that route, I don't know ... it at least theoretically works.
fabianx’s picture

Assigned: Unassigned » fabianx

Needing to use the dumper is unfortunate. That memory leak should be fixed though - regardless of the approach we take here.

fabianx’s picture

I found the first memory leak:

MimeTypeGuesser::registerWithSymfonyGuesser($this->container);

does

$singleton = SymfonyMimeTypeGuesser::getInstance();
$singleton->register($container->get('file.mime_type.guesser'));

BUT that singleton never forgets (but it is a protected value (!) so we could add a reset value) and now it gets worse:

file.mime_type.guesser is a proxy, therefore we store all container (builder) instances ever created during a test run ...

However not even marking that non-lazy would help, because it also stores all other proxies internally, so then again the container is stored indefinitely ...

Besides that found:

- We always call addPsr4 instead of setPsr4, so we add more and more namespaces to the autoloader.
- registered stream wrappers leak very tiny amounts of memory (10k) that is not gained back by unregistering
- https://bugs.php.net/bug.php?id=67111 also leaks a little memory in Yaml::parseInline

dawehner’s picture

But does explain the quite high value of 20MB per executed test?
I mean there really needs to be something which makes things behave in a different way with this patch.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new95.6 KB

Just patched Symfony for now, we can cleanly subclass though and do a reset by unsetting the protected property and calling __construct again or copying construct ...

This hopefully passes on test bot ...

fabianx’s picture

StatusFileSize
new1.75 KB

Interdiff for #72

Status: Needs review » Needs work

The last submitted patch, 72: proxies_of_module-2408371-72.patch, failed testing.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new2.89 KB
new96.58 KB

Here we go:

Fixes the memory leak by using ReflectionProperty.

Justification:

08:01 < Fabianx-screen> If they (Symfony) use private, I use Reflection ...
08:01 < Fabianx-screen> easy deal ;)
08:01 < Fabianx-screen> Or rather: if they use private + singleton + static that _cannot be reset_.

We can clean this up once Symfony has added a reset() method.

Hopefully should be green.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Awesome!!

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. This is looking good. I'm a bit concerned about the code generation. The interface change the need to rebuild is going to cause problems.
  2. Any reasons why the the block repository service from the original issue is not being marked as lazy?
  3. +++ b/core/lib/Drupal/Component/ProxyBuilder/ProxyBuilder.php
    @@ -34,19 +57,32 @@ public static function buildProxyClassName($class_name) {
    -  public function build($class_name) {
    +  public function build($class_name, $proxy_class_name = '') {
    

    Missing @param for new argument

  4. +++ b/core/lib/Drupal/Core/Command/GenerateProxyClassCommand.php
    @@ -0,0 +1,94 @@
    +    $this->setName('generate-proxy-class')
    +      ->setDefinition([
    +        new InputArgument('class_name', InputArgument::REQUIRED, 'The class to be proxied'),
    +        new InputArgument('namespace_root', InputArgument::REQUIRED, 'The filepath to the root of the namespace.'),
    +      ])
    +      ->setDescription('Dumps a generated proxy class into its appropriate namespace.');
    

    Can you use the ->addUsage() method to add some examples of how to use this for core/contrib/custom

  5. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/ProxyServicesPass.php
    @@ -0,0 +1,49 @@
    +          // @todo Replace this with an assertion once
    +          //   https://www.drupal.org/node/2408013 is in.
    +          throw new InvalidArgumentException(sprintf('Missing proxy class %s for service %s', $proxy_class, $service_id));
    

    This could build the command to run I think?

  6. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -1180,7 +1179,6 @@ protected function dumpDrupalContainer(ContainerBuilder $container, $baseClass)
    -    $dumper->setProxyDumper(new ProxyDumper(new ProxyBuilder()));
    

    No need to use Drupal\Core\ProxyBuilder\ProxyBuilder anymore then.

  7. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -1305,7 +1303,7 @@ protected function classLoaderAddMultiplePsr4(array $namespaces = array()) {
    -      $this->classLoader->addPsr4($prefix . '\\', $paths);
    +      $this->classLoader->setPsr4($prefix . '\\', $paths);
    

    Should we test this somewhere?

  8. +++ b/core/lib/Drupal/Core/File/MimeType/MimeTypeGuesser.php
    @@ -101,6 +101,20 @@ protected function sortGuessers() {
         $singleton = SymfonyMimeTypeGuesser::getInstance();
    +
    +    // @todo Remove once Symfony adds a reset() method.
    +    $property = new \ReflectionProperty(get_class($singleton), 'guessers');
    +    $property->setAccessible(TRUE);
    +
    +    if (isset($singleton->_beforeDrupalRegistration)) {
    +      // Reset state, else we store more and more services during test runs.
    +      $property->setValue($singleton, $singleton->_beforeDrupalRegistration);
    +    } else {
    +      // Store original state before we register our services.
    +      $singleton->_beforeDrupalRegistration = $property->getValue($singleton);
    +    }
    +
    +    //$singleton->reset();
    

    ouch... btw SafeMarkup is also not reset in the parent site between tests.

  9. +++ b/core/lib/Drupal/Core/Installer/InstallerServiceProvider.php
    @@ -58,7 +58,10 @@ public function register(ContainerBuilder $container) {
    +    $definition->setClass('Drupal\Core\Installer\InstallerRouteBuilder')
    +      // The core router builder, but there is no reason here to be lazy, so
    +      // we don't need to ship with a custom proxy class.
    +      ->setLazy(FALSE);
    

    Maybe it would be best to just not do the lazy compiler pass...

  10. +++ b/core/lib/Drupal/Core/ProxyClass/Batch/BatchStorage.php
    @@ -0,0 +1,83 @@
    +        /**
    +         * The service container.
    +         *
    +         * @var \Symfony\Component\DependencyInjection\ContainerInterface
    +         */
    +        protected $container;
    ...
    +
    +        protected function lazyLoadItself()
    

    Why do we bother with property docs and not function docs? Imo we should just add documentation to everything.

  11. +++ b/core/lib/Drupal/Core/ProxyClass/File/MimeType/MimeTypeGuesser.php
    @@ -0,0 +1,73 @@
    +        /**
    +         * @var string
    +         */
    +        protected $serviceId;
    

    This clashes with the $_serviceId added to all objects in the container. When you look at these in services in a debugger and see serviceId and _serviceId something looks very wrong.

fabianx’s picture

#77:

1. Opened #2513336: Consider checking checksum of interfaces during container building for proxy services, but catch said it is out of scope here.

2. No, reason, but lets do that in a follow-up to measure the impact. Opened #2513340: Make block.repository a lazy service for that.

3. @todo Will be fixed

4. Done, also opened #2513338: Make it easier to create proxies.

5. Yes, however the PSR-4 namespace is not easy to detect. I'll try how difficult it is.

6. @todo Will be fixed

7. @todo This change is actually out of scope here, opened #2513344: Use setPSR4() instead of addPSR4() to avoid making the PSR-4 list bigger and bigger in the autoloader during test runs for it and will revert here.

8. yeah :/

9. Yes, but that is not that easy to do.

10. @todo Will be fixed.

11. @todo Will be fixed. Yes, indeed. I tend to use drupalProxyOriginalServiceId as name, because that matches the container definition and is explicit enough.

dawehner’s picture

Thank you fabian for creating all the followups!

fabianx’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new8 KB
new80.54 KB
new121.44 KB

Fixed all issues:

- interdiff.txt - all fixes
- interdiff-proxies.txt - all resulting proxy class changes, I used the new error message to re-generate the proxies. Worked like a charm :).

Plus full patch.

Changes here:

- Error messages now look like:

<em class="placeholder">User warning</em>: 
Missing proxy class 'Drupal\filter\ProxyClass\FilterUninstallValidator' for lazy service 'filter.uninstall_validator'.
Use the following command to generate the proxy class:
  php core/scripts/generate-proxy-class.php 'Drupal\filter\FilterUninstallValidator' "core/modules/filter/src"

 in <em class="placeholder">Drupal\Core\DependencyInjection\Compiler\ProxyServicesPass-&gt;process()</em> (line <em class="placeholder">68</em> of <em class="placeholder">/var/www/html/core/lib/Drupal/Core/DependencyInjection/Compiler/ProxyServicesPass.php</em>).

- Added doxygen to all proxy methods.

- Fixed command to include real command how the proxy was generated

- Added addUsage() with examples for core, core modules and contrib

- Reverted out-of-scope addPsr4() change for now

Back to RTBC as only docs and error message changes

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 80: proxies_of_module-2408371-79.patch, failed testing.

fabianx’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new123.11 KB
new6.43 KB

Test fixes

  • catch committed 9f521c8 on 8.0.x
    Issue #2408371 by dawehner, Fabianx, alexpott, bforchhammer: Proxies of...
catch’s picture

Status: Reviewed & tested by the community » Fixed

OK I've been following this closely and I think this is in a good state. We have the follow-up to add the interface hashing and to make proxy generation easier, change notice is enough for now though to resolve the critical. And allows us to mark #2497143: Proxy services in the container force interfaces to be loaded as duplicate.

Committed/pushed to 8.0.x, thanks!

alexpott’s picture

Can we get a followup to add some document about lazy services to core.api.php - specifically the section on defining services in @defgroup container Services and Dependency Injection Container. Thanks

dawehner’s picture

dawehner’s picture

Symfony committed https://github.com/symfony/symfony/pull/15110 so someone could setup a method in Drupal

fabianx’s picture

Opened #2526412: Remove Singleton hack in registerWithSymfonyGuesser to remove the hack. Thanks, dawehner!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

wim leers’s picture

Status: Closed (fixed) » Active

Reopening for a question: is it intentional that the generate-proxy-class script that was added here only works for modules that are enabled?

I'd just been trying for 20 minutes to figure out what's wrong for a new module that was therefore not yet installed. Turns out this is what's wrong: I had not yet enabled the module… that I was trying to develop.

xjm’s picture

Status: Active » Closed (fixed)

Re: #90 that sounds non-critical because it has a workaround. Let's open a non-critical followup.

xjm’s picture

Issue tags: +Needs followup