PHP 7.4 ships with opcache preloading - https://wiki.php.net/rfc/preload - which allows a framework to specify a preload file which will:
...load a certain set of PHP files into memory - and make their contents “permanently available” to all subsequent requests that will be served by that server.
All the functions and classes defined in these files will be available to requests out of the box, exactly like internal entities (e.g. strlen() or Exception). In this way, we may preload entire or partial frameworks, and even the entire application class library. It will also allow for introducing “built-in” functions that will be written in PHP (similar to HHVM's sytemlib).
Notably, this comes with some drawbacks that may not make preloading advisable/possible in certain environments (e.g. shared servers) but is well suited for more "modern" runtime environments such as containers, which generally map to 1 "server" (container) per codebase.
The traded-in flexibility would include the inability to update these files once the server has been started (updating these files on the filesystem will not do anything; A server restart will be required to apply the changes); And also, this approach will not be compatible with servers that host multiple applications, or multiple versions of applications - that would have different implementations for certain classes with the same name - if such classes are preloaded from the codebase of one app, it will conflict with loading the different class implementation from the other app(s).
Akin to Symfony 4.4's compatibility: https://symfony.com/blog/new-in-symfony-4-4-preloading-symfony-applicati...
The proposal would therefore be to ship a preload file which may be optionally included in the site's php ini configuration set; this would be a default-OFF (since it requires manual intervention in the hosting environment) and opt-in ON situation.
As Drupal has much of its core imported from Symfony and already boasts an extensions API, I would imagine extending Symfony's model would be a starting point.
Comments
Comment #2
mglamanThis would be super interesting. Especially if we could finally dump the compiled container to a PHP file instead of the database or memory cache service like Redis to leverage preloading. I have no idea the full implication there, but it seems like that would be a huge boost.
Comment #3
andypostIt actually could be a 9.0 target as of related issue #3020296: Remove Symfony's classloader as it does not exist in Symfony 4
Also it reminds me about 2 remaining issues in #2218651: [meta] Make Drupal compatible with persistent app servers like ReactPHP, PHP-PM, PHPFastCGI, FrankenPHP, Swoole
Comment #4
bradjones1Interesting this could be a 9.0 target, however I think this is only really related in themes and spirit to the classloader issue; since this is an optional feature (by virtue of the fact it's not something we can automatically enable even if we wanted to) I think this is more about shipping a default implementation to point at in the ini file.
It may, however, be blocked/affected by the changes to autoloading in the linked issues (which I have yet to dive deep on) but I don't think it changes the course of those tickets at all.
Which version it goes into isn't so much an issue as just doing the thing, but it sounds like this is worth looking at regardless. I'll try to take a stab at it.
Comment #5
bradjones1Noodling on this in some free time this week. I think the rough plan for this is:
Identify/agree on files that should be preloaded
Symfony accomplishes this by identifying services, but we must do a little more leg work in so far as we also execute code in
.modulefiles, which are (regrettably?) still alive and well in D9. There are also.incfiles running around on frequently-executed code paths (like preprocessing for specific templates).Notably absent here is compiled Twig, which now happens at runtime; raises some issues/opportunities regarding precompilation. See #2308215: Create a script to compile all twig template files and inlines. There is a Drush command to compile templates, however Drush is not (yet?) in core so we can't depend on that per se. Interestingly, if we precompile twig templates that may help make Drupal 9 more cloud-native, in so far as this would avoid different web-heads having locally-compiled twig templates available on local temporary storage. Currently this can be addressed with
twig_temp, but precompilation would allow us to ensure the same starting point on all web heads and also take advantage of opcode preloading.Determine the method for generating the preload file
Drupal 9 requires symfony/dependency-injection, where the new Symfony framework preload logic lives. We can call
Preloader::preload()to take advantage of their de-duping and error catching code. The preload file generation would be different, however, given we do not track their hot-path model particularly closely and have additional non-class files (see above) to include. It would be ideal to be able to generate this file statically without the database, however we would need access to a list of enabled modules (core and contrib) to include, in addition to potentially including twig compiled assets. Since the ideal place to run this would be during CI/CD pipelines (e.g., prior to creating a Docker image artifact) we definitely don't want a database dependency. Since the config export logic is in core, we could perhaps require the generator script depend on the existence of exported config, which we can parse for a list of enabled modules?Alternatively (or additionally?) the generator could be run on a live site and downloaded through the UI akin to the config export module, but I think the advanced nature of this feature means a script may be sufficient as an MVP.
Test coverage
How to test this? The Symfony implementation doesn't (I don't think?) specifically test the preloading functionality, though I could be missing it.
Profiling
Performance profiling will be huge on this; it is not my forte, so I'm looking for suggestions on how to test the impact?
Next steps
I'm excited to do something around this, given the potential for serious performance benefits. Thoughts on the above outline would be most helpful at the moment, though. Particularly around the Twig question, how to identify our own flavor of "hot paths"/code to include, and testing/profiling.
Comment #6
ndobromirov commentedAdded some related tags.
Comment #7
andypostComment #8
bradjones1Regarding hot-paths and identifying files to include, @catch says this on #1818628: Use Composer's optimized ClassLoader for Core/Component classes:
I'm curious though how this would work in practice; we'd need some way to extract this information out of the cache for generating the preload file, and some sites might not easily be able to generate representative samples?
Comment #9
moshe weitzman commentedAnyone know if CLI requests benefit from the preloading? If so, this is a huge win as they get no opcache at all. Some answers:
Comment #10
moshe weitzman commentedActually it looks like opcache keeps statistics so Drupal doesn't need to do anything. Just include this package and make your own preload file? https://medium.com/swlh/preloading-your-php-7-4-project-in-one-line-9ede...
Comment #11
bradjones1Thanks, Moshe...
I think the answer re: CLI is
no, it doesn't- edit: opt-in, see below; the preloading stuff lives "in" opcache though it is kind of a special case from what I can read. But the RFC for the feature says, "All the following HTTP requests use the representation cached in shared memory." Probably still worth a test but that leads me to believe this doesn't cover the command line. Edit -The approach to profiling and then dumping the preload file using a library like the one you link is interesting, however I am not sure that is the correct answer for the framework to adopt, in so far as it requires a lot more work on the part of the implementing site owner? This is an advanced feature, however I think it is "too much" to ask even most technically-minded people who would want to implement this to do their own file generation based on statistics. If Symfony is any model, they landed on a concept like hot paths to determine preloading; we could probably agree on some similar way to identify code worth preloading as a sensible default, and site owners are of course welcome to generate their own through as complicated a process as they like? What do you think?
Any thoughts on twig precompilation or would that be a candidate for a follow-up maybe?
Comment #12
bradjones1ACTUALLY - you can opt-in to opcache for CLI at https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.enab... - so the answer is yes, but not by default. Perhaps drush/console/whatever could implement a suggestion/warning.
Comment #13
andypostMeantime there's other way to minimize load to class loader - pecl
For example - mostly all (except logger) psr https://pecl.php.net/package/psr
Comment #14
andypostIt reminds me discussion about requirements and feature detection (how to unify) - for example image gd supported types
Comment #15
bradjones1Thanks, @andypost - do you remember if there's a ticket for feature detection or was it an offline convo? Would be good to have a meta ticket for that...
The PECL package is intriguing however I'm not sure if it would be a good fit in this case in so far as I'm guessing we can't require a PECL package as matter of course, and I believe (?) that the dependencies that require those interfaces would include them from a library anyway?
Do you have any thoughts about identifying code to include, beyond profiling (which may not be practical for a lot of sites that want a more default setup?)
Comment #16
xjmComment #17
ronlee commentedHas anyone completed a Drupal 7 php file for uploading drupal 7 core?
Comment #18
mxr576I have just bumped into this: https://www.drupal.org/project/preloader
Comment #21
andypostComment #22
berdir@andypost: Not sure how that's related. preloaded classes don't need to be in the apcu cache, so the more you preload, the smaller that can be.
We're not using https://github.com/Ayesh/Composer-Preload and it seems to be working quite well, but setting it up in a useful way is very tedious trial & error. All the BC layers that are going on in symfony 4 and twig with class aliases, conditional class definitions are really making this quite painful to set up because you can't preload those classes then and you can't preload anything that has a reference to something that you can't preload. So I ended up with a rather long list of inclusions and exclusions and regular expressions to find a balance between loading frequently used classes, not having dozens to hundreds of warning in php logs and not filling up your opcache memory.
I'll try to write a blog post or post my configuration somewhere.
The performance improvements are fairly neat, I got a ~20% speed boost on page cache responses for example but no data on productive sites yet.
Comment #23
catchIf the classes are provided by PECL, PHP will get them from there first, since it doesn't need to autoload them. We could recommend installing the package and don't need to require it - without it'll load from the libraries. At one point someone was working on a PECL package for check_plain() etc. for similar reasons, but don't think it ever got very far. This is worth its own issue for a hook_requirements() info.
Comment #24
martin107 commentedI am just reading along ...
by ~20 speed boost on the ...
do you mean a 20% reduction in page load time ?
Comment #25
andypostYes, ~20%
Comment #28
renrhafHi ! I would be really interested into setting up preloading in my Drupal project.
As said in #22 there seems to be some tweaking to do with the BC layers, I'm still experimenting to get it working.
Anyone having some examples of a basic working configuration ? Thanks !
Comment #29
bbralaHmm, this one is interesting. A colleague of mine is planning to roll this out in coming months, generating precompiled opcache for our drupal sites and getting those into our containers. I'll send him this issue and hopefully there is overlap.
Comment #30
bradjones1AFAIK this is different from preloading and... isn't possible? OpCache is entirely in-memory? But I could just be missing something basic about the problem space.
Preloading should be 100% possible though, at least in non-shared hosting (which is becoming a lot less common in the era of containers, anyway.)
Comment #31
bbralaEh. Did I misread this issue then?
"PHP: opcache_compile_file - Manual" https://www.php.net/manual/en/function.opcache-compile-file.php
Comment #32
berdirThat is the function you use, but you don't get to export that somewhere, it's kept in memory of the regular opcache. The trick is a new config setting that runs when a php process is started that allows you to compile all the files you want: https://wiki.php.net/rfc/preload
We did run into a lot of segfaults and other weird php errors after rolling this out and reverted it.
As mentioned before, class aliases and other BC tricks are making the setup of this quite painful, I might try again after we update to D10 and the current ones are gone (although there might be new ones by then, we'll see). I might also reduce the current attempt to only include bootstrap classes, and slowly expand from there, we'll see.
Didn't manage to write a blog post, but here's our configuration that we worked with for https://github.com/Ayesh/Composer-Preload:
As you can see, there are a ton of excludes, either because they are themself or rely on classes that don't work with preloading or I deemed not important enough. Because another problem was that autoloading all of that without the optimization excludes exceeded the memory limit and blew up on start, which on platform.sh resulted in an endless boot loop.
Then you run composer preload on deploy (build step in case of platform.sh) because the paths it generates are absolute, so needs to be on the same environment and then you add
opcache.preload: 'vendor/preload.php'to php.ini.We didn't use the drupal module because that seemed too tedious to maintain. You need a running drupal site from which you export the preload configuration.
Good luck and you're welcome to share your progress ;)
Comment #33
bradjones1@Berdir this is an excellent report "from the wild." Did you do any profiling on the site (while you had this in place) to determine what kind of performance advantage, if any, you got in practice?
Comment #34
casey commentedI already was following this issue.
What I talked about to @bbrala is https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.file...
Apparently it is possible to warm opcache during docker build and point opcache.file_cache to it:
https://github.com/jderusse/composer-warmup
https://github.com/acmephp/acmephp/blob/master/Dockerfile
Comment #35
bradjones1Interesting. A technical primer on this approach from Nikita Popov:
https://www.npopov.com/2021/10/13/How-opcache-works.html#file-cache
Comment #36
berdirSee #22, got ~20% faster page cache responses, didn't do further profiling/testing.
The file cache stuff looks interesting, but first step is to have a stable regular preload working, that's then the cherry on top. I guess whether or not that is worth the extra complexity is how long the regular preload takes and how often you restart php processes. I would expect it's fairly fast.
Comment #39
andypostIt should help running tests a lot for new CI #3386474: [omnibus] Speed up gitlab ci runs
Comment #40
andypostIn PHP 8.4 new JIT engine coming https://github.com/php/php-src/pull/12079
PS: using
opcache.jit_buffer_size=20Mmostly all Drupal sites for last half year without issues but no preloading in a wild(Comment #41
chi commentedI just created one more generator for preload script.
https://www.drupal.org/project/opc
However, while profiling my project I found another way to optimize opcache.
There are a couple opcache settings which default values are not suitable for production.
A typical Drupal sites loads about 1k - 2k files per request. With above configuration it'll revalidate opcache every 2 seconds.
Turning off
opcache.validate_timestampsgave me almost same speed boost as preloading. I think, unless you edit code directly on production server that setting can be disabled. The invalidation should happen through php-fpm reload on deployment.