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
Comments
Comment #1
wim leersWow :(
Comment #2
dawehnerOne thing we would do is to also write the interfaces into the container. They don't have to then loaded later anymore.
Comment #3
bforchhammer commentedI'm not sure this is supposed to work... oh php is fun :)
Comment #5
alexpottMaybe exploring #2354475: [meta] Refactor the installer, (multi)site management, and pre-container bootstrap will lead to a solution.
Comment #6
bforchhammer commentedI 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...
Comment #7
fabianx commented#3 looks great to me and will improve performance, too!
Comment #8
alexpottFixed the tests.
Comment #9
fabianx commentedWe need to ensure an opcode cache is used for the classes still, so need some profiling.
Comment #10
catchPretty 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.
Comment #12
alexpottI'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...
I think the above performance gains and how this will allow contrib to enjoy the same performance as core makes this critical.
Comment #14
alexpottFixed 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.
Comment #15
fabianx commentedOverall: +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.
Comment #17
wim leersWhy are
update.module's services even being initialized on admin pages? I don't understand that part yet.Comment #18
dawehnerSee
\update_page_top()It tries to warn you in case new updates are available.
Comment #19
dawehnerOne 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
Comment #20
dawehnerJust posting some progress,
Comment #21
chx commentedIs 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.
Comment #22
catch@chx we discussed checking the proxies into git - so this would be more like a subset of 'module builder' than a rebuild operation.
Comment #23
dawehnerHere 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.
Comment #24
fabianx commentedLooks like a great start.
Comment #26
fabianx commentedOverall looks really really awesome!
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.)
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.
Comment #27
dawehnerThank you for your feedback fabian!
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
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.
Comment #29
deepakaryan1988Comment #30
dawehnerTypical sort problem ....
Comment #32
fabianx commentedI think we should move that check up to before creating the proxied service definition?
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.
Comment #33
dawehnerGood point :)
This should make it.
Comment #35
dawehnerTake this!!!
EntityValidationTest is doing evil things, so proxies don't make things easier, to be fair.
Comment #36
dawehnerForgot to add the locally written tests to git.
Updated the issue summary ... and wrote a change notice regarding the use of proxies now.
Comment #37
fabianx commentedOverall looks really really great!
Some questions:
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?
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?
Comment #38
dawehnerGood questions!
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 ...
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.
Comment #40
catchLooks like a good case for an assertion?
Comment #41
dawehnerGood 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?
Comment #42
fabianx commented#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?
Comment #43
dawehnerI 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/1or/nodeComment #45
dawehnerPlease don't retest it, this is an interesting failure
Comment #47
wim leersThe IS says:
But it seems this patch is committing them? I'm confused now. We need an updated IS.
Comment #48
dawehnerRight we provide a script so people can generate their own.
Comment #49
fabianx commentedYes, we need an IS update.
Anything actionable we can do on the test failure? Is this really random?
Comment #50
dawehnerDoes someone mind you to update the IS, I'm a bit blind what you think is cannot be understood.
No its not random at all. I think we just jumped over the memory limit for that test.
Comment #51
fabianx commentedAdded issue summary updates
Comment #52
wim leersThanks, the IS is now 300% more clear.
Comment #53
dawehnerThank you fabian!
Comment #54
fabianx commentedComment #55
dawehnerJust 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
Comment #56
Crell commentedHm. 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.
Comment #57
chx commentedAs 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?
Comment #58
dawehnerDo I saw chx and crell agreeing :)
So, the current still existing bootstrap flow:
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.
Comment #59
fabianx commentedProxy 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.
Comment #60
catchYes 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).
Comment #61
catchAnd...
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.
Comment #62
catchAnother 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.
Comment #63
catchAnd 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.
Comment #64
dawehnerCatch had an idea how to debug the failures ... it turns out, with opcache disabled on PHP 5.5, this test requires
281809560 = 268MBso I could imagine that you can get to the 335544320 bytes potentially quite easy.
Comment #65
fabianx commentedUnassigning for a moment as dawehner and catch check the test failure, too.
Comment #66
dawehnerAlright I executed all test functions of that test class, note that this is the peak memory usage, at the end of each test method.
Here a comparison with HEAD
Comment #67
catchPut 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.
Comment #68
dawehnerI 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.
Comment #69
fabianx commentedNeeding to use the dumper is unfortunate. That memory leak should be fixed though - regardless of the approach we take here.
Comment #70
fabianx commentedI 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
Comment #71
dawehnerBut 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.
Comment #72
fabianx commentedJust 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 ...
Comment #73
fabianx commentedInterdiff for #72
Comment #75
fabianx commentedHere 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.
Comment #76
dawehnerAwesome!!
Comment #77
alexpottMissing @param for new argument
Can you use the
->addUsage()method to add some examples of how to use this for core/contrib/customThis could build the command to run I think?
No need to use Drupal\Core\ProxyBuilder\ProxyBuilder anymore then.
Should we test this somewhere?
ouch... btw SafeMarkup is also not reset in the parent site between tests.
Maybe it would be best to just not do the lazy compiler pass...
Why do we bother with property docs and not function docs? Imo we should just add documentation to everything.
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.
Comment #78
fabianx commented#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.
Comment #79
dawehnerThank you fabian for creating all the followups!
Comment #80
fabianx commentedFixed 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:
- 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
Comment #82
fabianx commentedTest fixes
Comment #84
catchOK 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!
Comment #85
alexpottCan 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. ThanksComment #86
dawehnerThere we go.
Comment #87
dawehnerSymfony committed https://github.com/symfony/symfony/pull/15110 so someone could setup a method in Drupal
Comment #88
fabianx commentedOpened #2526412: Remove Singleton hack in registerWithSymfonyGuesser to remove the hack. Thanks, dawehner!
Comment #90
wim leersReopening for a question: is it intentional that the
generate-proxy-classscript 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.
Comment #91
xjmRe: #90 that sounds non-critical because it has a workaround. Let's open a non-critical followup.
Comment #92
xjmComment #93
wim leersSorry.
Filed #2539494: Provide a nice error message when trying to run the generate proxy class script on uninstalled/missing modules for that.