Problem/Motivation
This is a follow up for #2296009: Use APC Classloader by default (when available).
Using in-memory cached classloader represents a very nice performance improvement. Although this can be manually configured in settings.php, core added the ability to autodetect the availability of APCu user cache and use symfony's ApcClassLoader by default.
Symfony has also several other optimized class loaders and the original autodetection implementation was only APCu specific.
It would be nice if core supported autodetection of optimized classloaders for other major environments, to make Drupal fast out-of-the-box on as many platforms as possible.
Proposed resolution
Refactor the autodetection mechanism to make it easier to detect the availability of other optimized class loaders, and leverage the fact the symfony's class loader implementations already detect if the required environment setup is available (extensions and functions) instead of doing specific checks for APCu.
Remaining tasks
User interface changes
None.
API changes
None.
Data model changes
None.
Beta phase evaluation
| Issue category | Bug because it is very short minded to only support APC and hardcode it into core without being pluggable - making Drupal barely usable on platforms that do not support APC/APCu. |
|---|---|
| Issue priority | Major because according to the original issue the performance gains are noticeable. |
| Prioritized changes | The main goal of this issue is performance |
| Comment | File | Size | Author |
|---|---|---|---|
| #62 | 2556025-59.patch | 3.96 KB | david_garcia |
| #60 | 2556025-59.patch | 3.96 KB | david_garcia |
| #53 | interdiff.txt | 325 bytes | dawehner |
| #53 | 2556025-53.patch | 3.96 KB | dawehner |
| #50 | interdiff.txt | 779 bytes | dawehner |
Comments
Comment #2
david_garcia commentedHere comes a patch.
Overally I am seeing a tendency towards platform specific code in Drupal core (mainly APC/APCu usage) in such a ways that it is not easy to replace for alternative backends.
Just take a look at rebuild.php, where specific platform user cache clearing is hardcoded and impossible to override/replace.
Considering that most user caches (APC, APCu, Wincache, XCache) work in a very similar fashion some level of abstraction could be introduced here (similar to the database abstraction layer) so that core can interact with in memory user caches in a more generic and pluggable way.
Comment #3
andypostOverall +1
I think hardcoding classnames into core is not a good idea, maybe better to make them configurable via settings somehow
more then 80 chars in line
trailing whitespace
Comment #4
fgmOne thing to keep in mind is the fact that any such optimizations are liable to have problems on multi-web-head configurations. This should not be the case for just the classloader with the rebuild-stamped and shared-filesystem issues being solved, but there's probably not much more which can be done without accounting for multiple heads.
Comment #5
david_garcia commentedFixed coding issues.
I don't like how these classes are hardcoded either, but they were before. Taking into account that in-memory storage is extremely important for performance on a shared nothing architecture and that Drupal should rely more and more on it's availability I believe it should be abstracted in a pluggable way as cache backends are.
Autodetection can be disabled in settings.php anyways.
It does not quite matter that some very frequently used caches get duplicated in multiple heads, such as bootstrap, the classloader, or even other ones... as long as they are not too expensive to generate and store. In memory user caches are way faster than network based shared storage such as memcache, redis or similar - even orders of magnitude faster on some setups.
Comment #6
david_garcia commentedComment #7
joelpittetUnfortunately, this feature didn't make the cut before RC. There may be a case for it to be included as a bug from the beta evaluation but it looks like a feature request and is marked as a task.
Feel free to disagree, I'm just triaging the queue.
Comment #8
david_garcia commentedYou can override this in settings.php so I don't really care.
But I am sure many people will miss that setting, and given the performance gains of using a classloader (+25% more RPS on some scenarios) that leverages caching they will have a painful D8 experience (as if it was not slow enough as it is now...).
I've just always felt that issue priorities a little bit messed up because they do not take into account if the change itself can break things. That's how you end up having hundreds of one-line no brain melter patches lying around in the issue queue dead for months or years, degrading the overal Drupal experience.
I remember at some point having > 100 patches from the issue queue applied to a D7 core fork. How are not those 100 patches all together as critical as one single "critical" issue :)
And I am sure that some patches can be easily evaluated with a 0% probablity of error that they will break nothing.
I don't agree with the postponed status:
Even if moved to 8.1.x-dev, this still needs review (or work).
Comment #9
joelpittetPostponed because the 8.1.x branch is not open and tests won't work on it. Tagging to see what the product manager thinks maybe it can still get in 8.0.x.
Comment #10
fabianx commented#8: However, 80% will have APCu for that already, xcache has gotten much less popular in the last years.
And Drupal on windows, while nicely supported is still a niche and also those people usually know more what they are doing than someone on a VPS or such ...
Comment #11
xjmFor the record, which branch a change can be committed to and when is ultimately a release management decision, not a product management one. :) Product managers are in charge of the user-facing feature set and user experience. Reference: http://cgit.drupalcode.org/governance/plain/drupal-core.html#product-man... versus http://cgit.drupalcode.org/governance/plain/drupal-core.html#release-man....
Since this issue would be adding additional functionality in the form of additional class loader support, I think it it is correct to discuss it as a 8.1.x issue according to https://www.drupal.org/core/d8-allowed-changes#minor. (Not weighing in on whether or how the change should be made, just which branch would be correct if we decide to go forward with it.) Thanks!
Also, testing against 8.1.x should work now, so I'll try adding a test run.
Comment #13
david_garcia commentedComment #14
joelpittetReviewing mostly some minor coding standards fixes but wondering about extensiblity on this:
Should this be configurable in some way to extend this list?
And also a little nit: last item needs a comma and a linebreak.
nit: Period.
nit: Indenting can be moved back 2 spaces and short array syntax on the new hunks could be nice.
Comment #16
david_garcia commentedFixed some of the style issues and the bug that made the tests fail.
These three classloaders are the only ones with platform specific caching that are implemented in symfony, and I believe that by using those three we are covering somewhat 99% of use cases, if not even more.
And as I stated before, you can indeed configure a specific classloader in settings.php, the problem is that the "autodetection" happens before settings.php is processed, so there is actually a measurable edge in adding these class loaders as early as in this autodetection.
And the more Drupal works out of the box - on as many platforms as possible - the better to foster adoption.
Comment #17
david_garcia commentedComment #18
fabianx commentedI agree with this patch, but I disagree with try { catch on every page load - that seems very excessive.
Lets just use a quick and fast function_exists for the loader as well.
apcu_fetch, apc_fetch, wincache_..., etc.
Btw. And thanks David Garcia!
Comment #19
david_garcia commented@Fabianx The use of try/catch is there in order not to reimplement the logic these classloaders already have in their constructors, where they check if what they need to run is available.
I implemented another approach but really, I believe that performance impact of both changes are negligible. Talking about the try{} catch{} "issue":
1. Users with APC enabled: because that is the first element in the loop an exception will never be triggered. Most of the overhead in using try/catch blocks comes when exceptions are thrown (that's why relying on try/catch for logic implementation such as it is being done here is not ideal...)
2. Users with other class loaders: at the most this is a 2 element loop... should still be neglibile.
In the new patch I'm using "extension_loaded" instead of "function_exists" to respect symfony's implementation.
Comment #20
david_garcia commentedSorry wrong patch...
Comment #23
david_garcia commentedPatch did not apply anymore.
Only rerolled the first version.
Comment #24
daffie commentedUsers @andypost and @Fabianx are for this issue and to me this seems to be a good idea. I would like to get this issue fixed, but I have some remarks/questions:
Can we make $classloaders a class property. And can you explain why there are only these 3 classes added and why there are ordered in this way.
Is it necessary to clear all 4 caches? Can you give an estimation how much slower this make drupal core. Is there any way to know if some of these calls are not necessary?
Nitpicks: "Lines containing comments (including docblocks) must wrap as close to 80 characters as possible without going over" according to #1354: [Obsolete] API documentation and comment standards.
Comment #25
david_garcia commentedDone. Explanation is in the comments. Note that these class loaders cannot be "parametrized" (such as being configured in settings file) because this autodetection happens early in the bootstrap.
Later on in bootstraping, if the user specifies a classloader in settings.php, that will be loaded and used.
This will make Drupal core 0.000% slower :) These changes only affect calls to rebuild. This code is not part of the normal request/response workflow. The additional overhead is 3 extra calls (assuming that a deployment will only have one of those backends enabled) to "is_callable". 3 calls to is_callable should not be a concern, specially if only triggered when calling rebuild.php which, by the way, is a slow thing by itself making the change even more negligible.
Fixed.
Comment #26
daffie commentedThese lines are not: "Lines containing comments (including docblocks) must wrap as close to 80 characters as possible without going over". Also 4 lines are ending with a space character.
Comment #27
david_garcia commentedTrue about the whitespaces, yet my editor tells me all these comment lines are below 80 characters long.
Comment #28
daffie commentedYes, the commit lines are below 80 character, but they can be a lot closer to 80 characters without going over.
Edit: see https://www.drupal.org/node/1354.
Comment #29
izaaksom commentedI agree with this patch. It seems like improvements out of the box in Drupal are exclusive only for non-MS installations, even when there's a big enough community of users running Drupal on Windows that would like some of those too. And also seems a little biased to say that all those projects are being run by experts.
I think that David did a good job pointing that it would be nice if core supported autodetection of optimized classloaders for other major environments, this would improve the experience of a lot of people coming to Drupal from other platform backgrounds.
Btw, I tried the patch and runs pretty good, there's not a noticeable overhead caused by the implentation.
Comment #30
fabianx commentedWhat about:
Overall it would be nice to support the different cache systems not just for the class loader, but also for FileCache and the cache backends.
But that can be a follow-up.
Comment #31
david_garcia commented@Fabianx #30 I already proposed something similar in #19 but it turned out to be too complex because the fact that the extension is loaded (at least for wincache) does not mean that the user cache is setup and available. Winache does many things besides providing a user cache, and they can all be disabled individualy.
Each one of the class loaders "hides" the complexity of veryfing if the required backend is available during instantiation.
Again, the overhead of exceptions only matters if you rely on really catching them. For APC users, no exception will be thrown.
http://stackoverflow.com/questions/104329/performance-of-try-catch-in-php
Comment #32
fabianx commentedOkay, if we really must ...
Comment #34
daffie commentedTestbot problem.
Comment #35
catchSo if you don't have any of those extensions available, won't we try and fail to instantiate three different classloaders, each time with them throwing an exception?
Comment #36
-enzo- commentedHi folks
I want to add a change in drupal_rebuild function to remove the type declaration for classloader parameter in the same way the function DrupalKernel::createFromRequest did, to avoid issue with any variation of class loader
I am proposing this change because I am getting this error in my environment using APC loader
Comment #37
david_garcia commentedThrowing and catching 40,000 exceptions:
PHP 5.6.x: 0.08s , that would be 6*10exp-6 seconds for 3 exceptions
PHP 7.0.x: 0.04s, that would be 3*10exp-6 seconds for 3 exceptions
The overhead of the change, when none of the extensions is available, is in the range of a handful of microseconds.
@-enzo-'s change makes sense, Symfony's classloaders do not inherit from the ClassLoader class.
Comment #38
tstoecklerI think we need to profile the difference to an actual Drupal request, or some different ones preferably, for this to be OK.
Generally in core, we have avoided throwing exceptions in the critical path under normal circumstances for performance reasons. Not saying that that always has to stay that way, but this change would have bigger consequences in that it would make it acceptable for exceptions to be thrown generally.
Comment #39
catchHow much of the time is from the first exception being thrown, how much is from the other 39,999?
Comment #40
david_garcia commentedHow much of the time is from the first exception being thrown, how much is from the other 39,999?Initializing the loop and throwing just 1 exception yields something between 1.4*10-5s and 1.6*10-5s, that is, aprox. 16 microseconds (on PHP7). Still, the impact is neglibile, a profiler will be unable to grasp such a difference (these results are with XDEBUG off).
Tried with several loop sizes and it is true that the loop initialization and first exception are more costly than the other exceptions.
Not the best way to measure this, but this is the code I used (it more or less emulates the loop introduced in the patch in the sense that it will instantiate an object and an exception is thrown in the constructor itself):
Comment #41
dawehnerGiven that we already hardcode the 3 different classloader implementation, IMHO we could also just do the if/elseif/elseif call here. It is IMHO not worth to care about the exceptions here.
Comment #42
fabianx commentedI think we might still want to wrap this in a try { catch } block as especially wincache can also have the extension_loaded, but still fail to use it as an user cache as David Garcia stated above.
But that feels good to me and we can assume that 99% of the time it will just work :).
Comment #43
david_garcia commentedNot really, because this code is doing the exact same checks that the classloaders are doing upon being instantiated. The only risk here is those checks changing upstream.
I really don't understand the generalized aversion to exceptions. Having a couple of if/else is more or less the same here.
I tested what happens in this situation (wincache.ucenabled=0) and basically wincache fails transparently, so the worst that will happen is that the Wincache classloader will cache miss all the time.
Comment #44
dawehnerRight, which is why I'm arguing that doing that is enough :)
Mh, that's a fair point.
Comment #45
fabianx commentedRTBC for #41.
Comment #46
andypost+1 to no-try-catch better to know asap that no cache backend available
type probably "object", could be fixed on commit
Comment #47
david_garcia commentedTested #41 manually to make sure it works as expected. +1 RTBC.
Comment #48
alexpottIs this necessary? In order to use the APCClassLoader we're checking if apcu_fetch exists.
Comment #49
dawehnerIf we want to introduce this change, than not in this issue, IMHO.
Comment #50
dawehnerComment #52
fabianx commentedLets just remove this line, we do use a symfony shim that makes apcu available even if only APC (compat) extension is enabled, so we are fine to always use the apcu functions :).
Comment #53
dawehnerSure, let's do that.
Comment #56
david_garcia commentedComment #58
david_garcia commentedComment #60
david_garcia commentedAgain, did not apply due to latest changes. Hope gets in soon so there is no need to roll it again.
Comment #62
david_garcia commentedComment #63
fabianx commentedRTBC - Looks good now
Comment #65
catchCommitted 561580c and pushed to 8.3.x. Thanks!
Seems worth a release notes mention, so tagging.
Comment #66
david_garcia commentedNot strictly due to the changes from this issue, but quite related... if using an optimized classloader:
#1818628: Use Composer's optimized ClassLoader for Core/Component classes
The by putting Apc or Wincache on top of it, we are actually making the autoload process slower.
The original benchmarks that motivated using the Apc classloader are from here #2296009: Use APC Classloader by default (when available), but they where not using optimized composer classloaders dumps.
Considering that D8 has moved into something that needs to be "built" it will become more common that as part of this build process optimized loaders are dumped.
Comment #67
fabianx commented#66: Those sites that have an optimized build process and don't care about the memory used can easily set the class loader explicitly in settings.php, too. In the worst case they need to use a wrapper class to avoid the auto-detection from firing, but that is quite doable with a composer plugin.