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

bradjones1 created an issue. See original summary.

mglaman’s picture

This 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.

bradjones1’s picture

Interesting 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.

bradjones1’s picture

Noodling 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 .module files, which are (regrettably?) still alive and well in D9. There are also .inc files 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.

ndobromirov’s picture

Issue tags: +Performance, +scalability, +PHP 7.4

Added some related tags.

bradjones1’s picture

Regarding hot-paths and identifying files to include, @catch says this on #1818628: Use Composer's optimized ClassLoader for Core/Component classes:

fwiw #2704571: Add an APCu classloader with a single entry is still my preference here as well. That could then function as a source for (optional) class preloading in PHP 7.4, since it would be based on real-life site usage. There are tonnes of classes in core/lib which hardly ever get used and some classes from modules will be used constantly.

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?

moshe weitzman’s picture

Anyone know if CLI requests benefit from the preloading? If so, this is a huge win as they get no opcache at all. Some answers:

  1. Technically, it could be any cache backend not just APCu. But realistically there will be so much churn on this item that you wouldnt want to back it with anything else.
  2. Sites without APCu wouldn't get cache preloading, and would be in same place as today. Seems reasonable to me.
  3. Extracting the info would be $cache->get() which leads to apcu_get().
moshe weitzman’s picture

Actually 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...

bradjones1’s picture

Thanks, 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?

bradjones1’s picture

ACTUALLY - 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.

andypost’s picture

Meantime there's other way to minimize load to class loader - pecl
For example - mostly all (except logger) psr https://pecl.php.net/package/psr

andypost’s picture

It reminds me discussion about requirements and feature detection (how to unify) - for example image gd supported types

bradjones1’s picture

Thanks, @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?)

xjm’s picture

Version: 9.0.x-dev » 9.1.x-dev
ronlee’s picture

Has anyone completed a Drupal 7 php file for uploading drupal 7 core?

mxr576’s picture

I have just bumped into this: https://www.drupal.org/project/preloader

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

andypost’s picture

berdir’s picture

@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.

catch’s picture

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?

If 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.

martin107’s picture

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.

I am just reading along ...

by ~20 speed boost on the ...

do you mean a 20% reduction in page load time ?

andypost’s picture

Yes, ~20%

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

renrhaf’s picture

Hi ! 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 !

bbrala’s picture

Hmm, 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.

bradjones1’s picture

generating precompiled opcache for our drupal sites and getting those into our containers

AFAIK 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.)

bbrala’s picture

Eh. Did I misread this issue then?

"PHP: opcache_compile_file - Manual" https://www.php.net/manual/en/function.opcache-compile-file.php

berdir’s picture

That 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:

        "preload": {
            "paths": [
                "vendor/psr",
                "vendor/symfony/yaml",
                "vendor/symfony/service-contracts",
                "vendor/symfony/dependency-injection",
                "vendor/symfony/http-foundation",
                "vendor/symfony/http-kernel",
                "vendor/symfony/mime",
                "vendor/symfony/routing",
                "vendor/twig/twig/src",
                "vendor/typo3",
                "vendor/stack",
                "web/core/lib/",
                "web/core/modules/page_cache",
                "web/core/modules/path_alias",
                "web/modules/contrib/redis/src"
            ],
            "files": [
                "vendor/symfony/validator/Mapping/Factory/MetadataFactoryInterface.php",
                "vendor/symfony/validator/ConstraintViolationInterface.php",
                "vendor/symfony/validator/Validator/ValidatorInterface.php",
                "vendor/symfony/validator/ConstraintValidator.php",
                "vendor/symfony/validator/ConstraintValidatorInterface.php",
                "vendor/symfony/validator/Validator/ContextualValidatorInterface.php",
                "vendor/symfony/validator/Context/ExecutionContextInterface.php",
                "vendor/symfony/validator/Context/ExecutionContextFactoryInterface.php",
                "vendor/symfony/validator/ConstraintViolationListInterface.php",
                "vendor/symfony/validator/ConstraintViolationList.php",
                "vendor/symfony/validator/Mapping/MetadataInterface.php",
                "vendor/symfony-cmf/routing/src/RouteProviderInterface.php",
                "vendor/symfony/psr-http-message-bridge/HttpMessageFactoryInterface.php",
                "vendor/symfony/psr-http-message-bridge/HttpFoundationFactoryInterface.php",
                "vendor/symfony/event-dispatcher/Event.php",
                "vendor/symfony/event-dispatcher/EventSubscriberInterface.php"
            ],
            "exclude": [
                "vendor/symfony/dependency-injection/Config",
                "vendor/symfony/dependency-injection/Loader",
                "vendor/symfony/dependency-injection/Compiler",
                "vendor/symfony/http-kernel/HttpKernelBrowser.php",
                "vendor/symfony/http-kernel/Config",
                "vendor/symfony/http-kernel/DependencyInjection",
                "vendor/symfony/http-kernel/Event",
                "vendor/symfony/http-kernel/EventListener",
                "vendor/symfony/http-kernel/Debug",
                "vendor/symfony/http-kernel/DataCollector",
                "vendor/symfony/http-kernel/Client.php",
                "vendor/symfony/yaml/Command",
                "vendor/symfony/routing/Loader",
                "web/core/lib/Drupal/Component/Assertion",
                "web/core/lib/Drupal/Component/EventDispatcher",
                "web/core/lib/Drupal/Core/Action",
                "web/core/lib/Drupal/Core/Config/Development",
                "web/core/lib/Drupal/Core/Command",
                "web/core/lib/Drupal/Core/Archiver",
                "web/core/lib/Drupal/Core/Entity/Plugin/Validation",
                "web/core/lib/Drupal/Core/Entity/Event",
                "web/core/lib/Drupal/Core/EventSubscriber",
                "web/core/lib/Drupal/Core/FileTransfer",
                "web/core/lib/Drupal/Core/Update",
                "web/core/lib/Drupal/Core/Updater",
                "web/core/lib/Drupal/Core/Installer/Form",
                "web/core/lib/Drupal/Core/Test",
                "web/core/lib/Drupal/Core/Transliteration",
                "web/core/lib/Drupal/Core/Validation",
                "web/core/lib/Drupal/Core/Ajax",
                "web/core/lib/Drupal/Core/Path/Plugin/Validation",
                "web/core/lib/Drupal/Core/Database/Driver/pgsql",
                "web/core/lib/Drupal/Core/Database/Driver/sqlite",
                "web/core/lib/Drupal/Component/Annotation/Doctrine",
                "web/core/lib/Drupal/Component/Annotation/Plugin",
                "web/core/lib/Drupal/Component/Annotation/Reflection",
                "web/core/lib/Drupal/Component/Bridge",
                "web/core/lib/Drupal/Component/ClassFinder",
                "web/core/lib/Drupal/Component/Transliteration",
                "web/core/lib/Drupal/Component/Gettext",
                "web/modules/contrib/redis/src/Queue"
            ],
            "extensions": ["php"],
            "exclude-regex": "/(AnnotatedClassDiscovery\\.php|FieldUpdateActionBase\\.php|Validation\\/ExecutionContext\\.php|ConstraintViolationBuilder\\.php|TypedConfigManager\\.php|TypedDataManager\\.php|ConfigCollectionInfo\\.php|EmailValidator\\.php|http-kernel\\/Client\\.php|Event\\.php|Pass\\.php|HttpKernelBrowser\\.php|ExpressionLanguage|Installer\\.php|Predis|\\/tests?\\/|[A-Za-z0-9_]test\\.php$)/i",
            "no-status-check": false
        }

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 ;)

bradjones1’s picture

@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?

casey’s picture

A colleague of mine ...

I 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

bradjones1’s picture

Interesting. A technical primer on this approach from Nikita Popov:

https://www.npopov.com/2021/10/13/How-opcache-works.html#file-cache

berdir’s picture

See #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.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

andypost’s picture

It should help running tests a lot for new CI #3386474: [omnibus] Speed up gitlab ci runs

andypost’s picture

In PHP 8.4 new JIT engine coming https://github.com/php/php-src/pull/12079

PS: using opcache.jit_buffer_size=20M mostly all Drupal sites for last half year without issues but no preloading in a wild(

chi’s picture

I 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.

opcache.validate_timestamps=On
opcache.revalidate_freq=2

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_timestamps gave 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.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.