Problem/Motivation

See notes in #3478621: Add filecache to OOP hook attribute parsing.

Mostly for YAML discovery, we cache the parsing of files in FileCache, which is a raw APCu cache implementation using filemtime for invalidation. The idea is to persist file parsing across cache clears (because it's not a cache backend), so that multiple module installs or cache clears and re-use parsing of identical files.

APCu isn't shared between cli and web processes, nor is it shared between cli processes or - therefore whenever YAML (or soon attribute) parsing tries to use FileCache on the cli, it won't work. Similarly, it's not shared between multiple web heads either.

Many of the things in FileCache are very expensive to generate at the level of a request (e.g. hundreds of milliseconds), but also over the lifecycle of a site have very low cache hit rates because they are often multiple caching layers on top.

If we can add a persistent, shared version of FileCache for most discovery, it should accomplish two things:

1. Increase the cache hit rate compared to storing in apcu

2. Reduce the amount of data we put into apcu by quite a lot, freeing it up for things accessed more frequently.

Steps to reproduce

Proposed resolution

File cache has the benefit of persisting in-between cache clears (which for some use-cases is almost the only time it runs like router rebuilds). Therefore add a new cache bin cache.file_parsing which is not tagged as a cache bin so therefore won't be affected by drush cr or cache clears via the UI.

On top of this cache bin, add a YamlCacheCollector - this can be used for a (not quite because the APIs are different) drop in replacement of FileCache. Each discovery should use its own instance of the class with its own cache key so we don't dump all yaml files in single cache item.

On top of YamlCacheCollector, add a discovery implementation too for use with routing/plugins.

Remaining tasks

To use this for css and js minification we'll need a non-YAML CacheCollector that does minification instead of YAML parsing - this might be able to use a base class or trait to share some of the logic.

Similarly, we should be able to provide an attribute cache collector similar to the Yaml one, for attribute plugins.

Both of these may end up in their own individual follow-ups depending on scoping decisions.

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

Issue fork drupal-3486503

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

catch created an issue. See original summary.

alexpott’s picture

Eek... but yeah this has always been a downside. This brings me back to a micro/precompiled container that that has essential services. FWIW we do have something similar already with \Drupal\Core\Config\BootstrapConfigStorageFactory...

catch’s picture

Status: Active » Needs work

Pipeline is green.

There is one remaining thing, which is we need a new FileCacheGarbageCollectionInterface, implement it in the new class, call the ::garbageCollection() method in system_cron() if the service implements the interface. That will complete the expires logic added so the database table doesn't have stale and un-removable cache entries in it forever.

catch’s picture

Status: Needs work » Needs review

Pushed a commit to implement #4.

nicxvan’s picture

Status: Needs review » Needs work

Unfortunately lots of failures that seem relevant, have not reviewed any.

catch’s picture

Issue summary: View changes

It's my attempt at garbage collection breaking. Pushed a commit which fixes at least one test.

catch’s picture

Status: Needs work » Needs review
Issue tags: +beta target

MR is green again now. Updated the issue summary with a summary of the implementation.

Tagging beta target because I think this is needed to mitigate the memory usage implications of the OOP hooks bc layer and new attribute parsing in core generally.

nicxvan’s picture

Did a quick review, a few questions where I feel like I'm missing something obvious.

nicxvan’s picture

My questions were answered I'll do a more thorough review later unless someone else gets to it.

nicxvan’s picture

Status: Needs review » Needs work

Just one final comment that I think needs addressing.

catch’s picture

Status: Needs work » Needs review

Addressed the last comment. Did some manual testing of this with #3478621: Add filecache to OOP hook attribute parsing and got what I would expect running the UI installer with the standard profile. Comments over there.

When doing that testing I realised another thing.

FileCache forces use of the apcu_prefix in the cache key. I think we should move that down to the apcu backend so the database doesn't use it - this would enabling persisting file_cache entries across deployments which is potentially a few hundred milliseconds downtime and dozens/hundreds of mb of peak memory usages removed from every deployment. However will start a new issue to explore that and we can either merge it back into here or do it in two issues depending on what it looks like.

Also notable is that I'm only seeing hook implementations go into the cache, nothing from YAML parsing - need to refresh my memory on what we do there but looks like we might need to either fix or add some caching to library/info/config YAML parsing.

alexpott’s picture

I think we want to add the table to the schema only suggestion in \Drupal\Core\Command\DbDumpCommand::configure() - we also need an issue against Drush to update the schema only defaults stuff there for db dumping.

catch’s picture

I think we want to add the table to the schema only suggestion in \Drupal\Core\Command\DbDumpCommand::configure()

This is done.

we also need an issue against Drush to update the schema only defaults stuff there for db dumping.

Happy to open this one once we've finalised the table name.

catch’s picture

Also notable is that I'm only seeing hook implementations go into the cache, nothing from YAML parsing - need to refresh my memory on what we do there but looks like we might need to either fix or add some caching to library/info/config YAML parsing.

This was bad testing on my part - I already had these cached in APCu from a previous installation attempt. Did a ddev restart, emptied the database, reinstalled, and there were 317 rows just from hitting the first page of the UI installer with a pre-filled settings.php.

After install and hitting the front page, there were 1226 rows from various discovery (library, info etc.) - this was without the hook discovery caching MR.

Shows that there are definitely advantages to the APCu cache for situations like repeatedly reinstalling Drupal as well as test pipelines.

catch’s picture

Opened #3488925: Move the FileCache apcu prefix handling into the apcu implementation or allow the APCu prefix to be customised and in the process of writing the issue summary, decided it definitely needs to be its own issue if we do it at all.

nicxvan’s picture

Failures look random, but I cannot rerun.

nicxvan’s picture

Couple of relevant failures, they seem the same type, expecting no tables but db cache exists

catch’s picture

Moving discussion from the MR to here.

@alexpott brought up the test expectation change in InstallerExistingSettingsTest.

We added that in #3103529: Drupal 8.8.1+ and 9 can fail to install in the web browser due to cache pollution to prevent cache pollution when hitting index.php then getting redirected to the installer.

I had a look at what changes would be necessary to prevent the table from being created in the early installer phase (when settings.php is pre-filled), and I wasn't able to find a good way to do this - because of the singleton approach in FileCacheFactory as opposed to it being a service (which is necessary for it being a dependency for container building), finding somewhere that's early enough and also being able to detect the installer state is not really doable. One possible option I didn't try yet (because it seems like a huge hack) would be checking for the install state globals in the implementation. Our entire 'early installer detection' relies on 'has a table in system_schema() been created yet' which itself is a massive hack that needs to be replaced too.

However, the bug we added that test for is for the regular cache backends which don't do mtime invalidation, so I think it's fine for this table to be created when settings.php is filled, the content of the cache doesn't vary by the install state or not, it just varies by the contents of the file - so why not rely on it in that very early install state.

catch’s picture

Tests are back to green.

nicxvan’s picture

One more comment on the mr once that is addressed I think this is ready. I think @alexpott should confirm his concern has been addressed.

catch’s picture

Fixed the one comment on the MR. Also realised we can/should take the database connection out of the constructor - theoretically at least this can be used without needing one, so might as well get the connection as late as possible.

nicxvan’s picture

I can't rerun the failed jobs, but only one looks like it might be relevant.

catch’s picture

Re-ran the three failing jobs - all green now.

nicxvan’s picture

From my perspective this is RTBC, @alexpott mentioned on slack he wants to review it again.

nicxvan’s picture

Status: Needs review » Reviewed & tested by the community

This is ready I think.

There are two open comments by @daffie, I think both have been addressed appropriately.
The check does need to be for default not active.
And the next time we need normalizeCid it should be factored out, but not yet.

Alex's concerns have also been answered.

nicxvan’s picture

Status: Reviewed & tested by the community » Needs work

Based on @alexpott's comment @daffie's comment was not actually resolved.

catch’s picture

I looked a bit more and I'm not really sure about @daffie's comment either.

With ::getConnectionInfo() we ensure the default database connection is configured, and then get a connection if it is.

With ::isActiveConnection() we check if there's an active connection (which will be the default database connection if it's active whenever this would be called), but then if it's not, we won't try to start a new connection.

To me ::getConnectionInfo() seems more robust, because it's OK if we start a new database connection here, we just need to know we have one to start.

In practice this might not actually make any difference to a site.

alexpott’s picture

Status: Needs work » Reviewed & tested by the community

@catch and I discussed #28 and decided to keep getConnectionInfo('default') but then do getConnection('default', 'default') to be super explicit about what connection to use. This is because of how $key and $target work for those two methods, this class can create tables and we have good evidence that file cache is used by run-tests.sh when rendering test results!

alexpott credited daffie.

alexpott’s picture

nicxvan’s picture

Status: Reviewed & tested by the community » Needs work
catch’s picture

Status: Needs work » Reviewed & tested by the community

The fix that just got committed for #3486503: Add a file parsing cache collector to replace some uses of FileCache should mean we get a green run again, restoring the RTBC since no code changes here.

nicxvan’s picture

Status: Reviewed & tested by the community » Needs work

core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php

catch’s picture

This test failure (not in the most recent run) is very strange, and i can reproduce it locally:

https://git.drupalcode.org/project/drupal/-/jobs/3513488


+····2·=>·Binary·String:·0x494e5345525420494e544f202264617461626173655f66696c655f636163686522202822636964222c2022657870697265222c202263726561746564222c202264617461222c202273657269616c697a656422292056414c55455320282264727570616c2e66696c655f63616368652e31312e322d6465762e2e6c3833444a62637057506668645372695a775a657268432d79736b54374139566b72624c736b705053494d3a6174747269627574655f646973636f766572793a44727570616c5f76696577735f4174747269627574655f56696577734163636573733a4e58687363526530343430504670493564537a6e4556676d61754c32354b6f6a443775346539615a774f4d3a44727570616c5f76696577735f416e6e6f746174696f6e5f56696577734163636573733a2f6275696c325836714e3969314c3759675149686247414776365f314e354f6f656468746264586237544e4d54623973222c20313734303530393230332c20313733323733333230332e3638332c2022613a333a7b733a353a226d74696d65223b693a313733323733333136333b733a383a2266696c6570617468223b733a35303a22636f72652f6d6f64756c65732f757365722f7372632f506c7567696e2f76696577732f6163636573732f526f6c652e706870223b733a343a2264617461223b613a323a7b733a323a226964223b733a343a22726f6c65223b733a373a22636f6e74656e74223b733a3534333a22613a393a7b733a353a22636c617373223b733a33363a2244727570616c5c757365725c506c7567696e5c76696577735c6163636573735c526f6c65223b733a383a2270726f7669646572223b733a343a2275736572223b733a323a226964223b733a343a22726f6c65223b733a353a227469746c65223b4f3a34383a2244727570616c5c436f72655c537472696e675472616e736c6174696f6e5c5472616e736c617461626c654d61726b7570223a333a7b733a393a22002a00737472696e67223b733a343a22526f6c65223b733a31323a22002a00617267756d656e7473223b613a303a7b7d733a31303a22002a006f7074696f6e73223b613a303a7b7d7d733a31313a2273686f72745f7469746c65223b4e3b733a343a2268656c70223b4f3a34383a2244727570616c5c436f72655c537472696e675472616e736c6174696f6e5c5472616e736c617461626c654d61726b7570223a333a7b733a393a22002a00737472696e67223b733a36343a224163636573732077696c6c206265206772616e74656420746f207573657273207769746820616e79206f66207468652073706563696669656420726f6c65732e223b733a31323a22002a00617267756d656e7473223b613a303a7b7d733a31303a22002a006f7074696f6e73223b613a303a7b7d7d733a31333a22646973706c61795f7479706573223b4e3b733a343a2262617365223b613a303a7b7d733a353a226e6f5f7569223b623a303b7d223b7d7d222c203129204f4e204455504c4943415445204b455920555044415445202263696422203d2056414c554553282263696422292c202265787069726522203d2056414c554553282265787069726522292c20226372656174656422203d2056414c55455328226372656174656422292c20226461746122203d2056414c55455328226461746122292c202273657269616c697a656422203d2056414c554553282273657269616c697a65642229,

There are multiple queries that just say 'Binary String' with the same long string. I don't know if they are errors, or informational, but they are weird and being picked up by the performance test query logger (apparently only sometimes).

catch’s picture

Realised thinking about that - we can't log these queries in performance tests as database queries, because an item may or may not exist in APCu depending on whether it gets evicted or is ever set etc. So we would need to ignore them. We could also ignore 'Binary string' queries but I'd like to know what those are ideally.

Theoretically it might be possible to add a file cache listener to log get/store separately (like we do for caches), but that's going to require overriding the backend with a custom one and can't just decorate a service.

Discussed this issue a fair bit with @berdir in slack, and he suggested possibly only opting into the database-backed cache for attribute parsing, since YAML parsing may be faster without it (and shouldn't result in unfreeable memory usage). Still need to think more about that, but this is blocked on excluding anything from FileCache in performance tests at least.

catch credited berdir.

catch’s picture

berdir’s picture

Link to the slack thread (it's long): https://drupal.slack.com/archives/C079NQPQUEN/p1732735819875639.

TLDR: This adds around 1700 database queries on my current setup (core + a bunch of contrib modules + test module discovery. The test module discovery won't be on on real sites, but sites with 100+ contrib projects will not be far off from that). And that's not even counting Hooks discovery yet, because that's part of the container and not part of my test scenario. For Yaml discovery and parsing of those small files, the database lookups seem to take more time than the actual parsing.

Also created this issue #3490347: Replace ExtensionParser::scanDirectory() FileCache usage with InfoParser

Another semi-related thing: also could reproduce reports about massive increase of installer memory usage. from 36MB on 10.4 to 260MB on 11.x, that is with drush. with drush, because it all happens in a single process, none of the current issues are able to make any real improvements to that. web installer on 11.x HEAD with the filecache seems to remain < 40MB now. Not sure there is a solution. Drush might need to change and somehow push module installs into separate batch processes? No issue created about that yet.

catch’s picture

Title: Add a core FileCache implementation that is database-backed » [PP-1] Add a core FileCache implementation that is database-backed

So from that slack thread, about 800 database queries were due to #3490347: Replace ExtensionParser::scanDirectory() FileCache usage with InfoParser, we should postpone this on that issue.

The weird binary string 'queries' are also a blocker here I think at least until we know what they are, and so is excluding queries from the backend from performance tests since we can't guarantee whether anything comes from apcu or the db by design, so it'll need some work once that issue is done. And maybe being more selective about which file caches we apply it to (can be done with $default_config param to FileCacheFactory).

We could explore a performance logging decorator in performance_test module in a follow-up, probably could only catch things outside container building but that is fine for most of our use-cases, so even if we exclude this from performance tests which can later bring it back.

Berdir is right that the drush installer currently wouldn't benefit from this, however I would expect e.g. a drush cr after install to benefit because the entries will be cached by then (from the installer or previous drush crs).

However also from the pure memory reduction standpoint, I was seeing preprocess hooks in system module discovered as hooks and added to the container, which is a side-effect of not having #3490355: Add procedural hook short circuit per module or file.

So maybe the order should be:
#3478621: Add filecache to OOP hook attribute parsing - already in as of yesterday.

#3490355: Add procedural hook short circuit per module or file - try to reduce the amount of parsing we do.

I also noticed when checking what gets cached vs not, and it's visible in the performance test results too, that we are doing a lot of file scanning and parsing for plugins that aren't plugins.

e.g.

core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php
core/modules/system/src/Plugin/ImageToolkit/Operation/gd/ScaleAndCrop.php
core/modules/system/src/Plugin/ImageToolkit/Operation/gd/GDImageToolkitOperationBase.php
core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Crop.php
core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Resize.php
core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Rotate.php
core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Convert.php
core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Scale.php
core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Desaturate.php
core/modules/system/src/Plugin/ImageToolkit/Operation/gd/CreateNew.php

Only GDToolkit is the actual plugin, all the operations are not, so that is nine 'NULL' cache entries where there should be one.

If I run:

SELECT cid, data FROM database_file_cache WHERE data LIKE '%"data";a:1:{i:0;N;}%';

(which is looking for a filecache data NULL entry - what we use in plugin attribute parsing)
I get 139 results.

We could have an issue which adds an exception when a non-plugin is found in a plugin directory, and gradually move these out. This would save directory traversal, memory, and space in APCu. And then if/when we return here, that's 139 less database queries too. Will open a meta and a sub-issue for the GD toolkit to start.

An issue against drush to use its own batch system in drush si also sounds like a good idea, I'm a bit surprised it's not already doing that. However thinking about that, we might want to do that in co-ordination with #3416522: Add the ability to install multiple modules and only do a single container rebuild to ModuleInstaller, that issue also results in a lot less reflection/YAML parsing, although eventually the same number of files will be parsed, just not as many times. But installing x number of modules in drush, hitting one that needs a container build -> new process to continue, this would be a good balance if it's doable.

catch’s picture

Title: [PP-1] Add a core FileCache implementation that is database-backed » Add a core FileCache implementation that is database-backed

Actually another thought which would be a change here but not postponed on the info parser issue.

There are two kinds of things we're using file cache for - extension, services, hook parsing which are all done during container build and therefore need to use the singleton.

However config parsing, plugin discovery, route attributes when we have them, libary parsing - these are all from services, so could depend on a service.

So... I think we could change approach here and do the following:

- add a new file_cache.persistent service, and inject this into the various places we want to use it.

- this service can be linked to a file_cache.persistent_cache_backend service which uses an actual cache backend, we'll need a no-op tag invalidator to pass in is the only additional thing.

- this can use BackendChain (not chained fast because we don't need the invalidation support) with the APCu and database backends.

- that will in turn make it easier to swap out the database backend with redis or memcache

- cache backends support both multiple get and multiple set. Because this is a new service with a new interface, we can copy those aspects of the cache backend interface (+ mtime invalidation support) and that will make it easier to do a query-per-directory instead of query-per-file and similar.

- for YAML parsing we'd need to have a non-filecache option for YAML parsing then, don't want to cache things twice..

This would not help us with hook and service discovery then, but since route and plugin attributes also load PHP classes, and there'll be as many of those potentially, we can save the memory somewhere both on web requests and cli that way.

alexpott’s picture

@catch not all Yaml parsing is from services... the ExtensionDiscovery object is used from a few places for example. But also the config importer is not a service - it's complex there because caching config during imports and installs doesn't feel right - this stuff is never read multiple times (well apart from during tests). Unlike config schema yaml where there cache is definitely worth it.

catch’s picture

@alexpott yeah I think this is more interesting for plugin discovery to start with.

Config - it's not read often, but also you can end up with 1000+ config files in a sync directory and maybe 30 changing when you do a sync, so a persistent cache with a 3 month TTL and no invalidation apart from mtime might still help - but probably need to check how the db cache would actually compare vs. Symfony YAML parsing.

I guess if it's not a service, could just do the implementation that's already here for that (but maybe not default if we want to avoid it for container builds).

catch’s picture

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.

catch’s picture

Title: Add a core FileCache implementation that is database-backed » Add a persistent cache for file-based discovery based on FileCache
Issue tags: -beta target +Performance, +memory

Thought about this a bit more and might have a plan.

We can't do the original idea here because service and hook discovery (and potentially other discovery) can happen when there's no database available. Also if we use the same model as FileCache we'd end up with a lot of database queries (both gets and sets).

I think instead we can do something like the following:

1. Add a file_parsing.cache database 'backend' which is not tagged as a cache backend. Just a new service using an existing backend that won't get cleared on drush cr etc.

2. Add a CacheCollector subclass where the cache item is an array of mtime and data and the key is file paths. This CacheCollector should handle the mtime validation etc. It should probably also handle some garbage collection (e.g. on set, remove items from the cache if the mtime is more than a year old and they weren't requested in the request).

3. Not entirely sure about this, but to conserve memory, we might be able to directly instantiate the CacheCollector in the discovery code paths rather than using a service, this is done for the runtime theme registry already. That way when discovery is complete, we can free up the memory. If that doesn't work, then services will work.

4. We can potentially convert one usage of FileCache to use this, and/or one thing candidate that doesn't use it yet - js minification is one I'd like to use it for. Once we've got something working it'll be possible to profile the difference.

We won't be able to use this for service or hook discovery, or at least not without a lot of changes, but we should be able to use it for libraries, js/css minification, route discovery (YAML and attributes), plugin discovery (YAML and attributes) at least.

By using a cache collector, we only have one database get and one set per discovery type, so a 100% cold cache (usually the installer, or the first time this code runs on existing sites) shouldn't have too much overhead.

Where we can use this for existing FileCache implementation, it will allow us to remove some large and low hit-rate items from APCu, as well as increasing the hit rate against the new backend.

catch’s picture

Status: Needs work » Active

Going to move this back to active since the original MR was more or less a dead-end, don't think there's any actual code to re-use from it.

catch’s picture

Status: Active » Needs work

OK made a start on a new approach.

New 'cache bin' which is normal except for not being tagged as a cache bin, this means it won't get cleared on drush cr etc. which is the whole point here.

New YamlCacheCollector class - this handles Yaml::decode(file_get_contents()) and mtime validation. Used it in library discovery to start with. Net reduction of code in library discovery that currently handles FileCache itself.

Couple of considerations:

- this increases the peak memory of LibraryDiscoveryParser because it currently doesn't have a static cache in there. I think we will save peak memory if/when we can apply this to plugin attribute parsing and it's cleaner to have it as a service. One way to reduce the peak memory would be to manually instantiate the cache collector inline in LibraryDiscoveryParser but that could mean more cache backend access/locking if it's used multiple times per request etc. Leaving it as a service for now.

- right now there is zero garbage collection at all, but we need something. I think a good start would be to override ::updateCache(), loop over all entries (that aren't getting set), and if anything is more than 2 years old, remove it, then call the parent - this will mean stale files that are never coming back get dropped eventually. If a system started using this and then stopped again, you could also get stale info that way although technically they could delete their own cache item in a post update.

To use this for plugin attributes, js and css minification etc. we'll need separate cache collectors. I think we can make a base class with an abstract ::parseFile() method which the YAML and other ones then inherit from.

Next steps:

1. Try to use this in routing. Router rebuild in Umami should show a bigger change in numbers than standard profile library discovery.

2. Add an implementation for js minification via Peast and use this to validate the base class idea.

3. Possibly try to apply to attribute parsing but I have a feeling that will be complicated by the plugin system, so maybe a follow-up if it's complext.

4. Garbage collection per notes above.

catch’s picture

Adding route discovery support. Because routing uses YamlDiscovery, this means adding YamlDiscovery support - which means we'll be able to use that for YAML plugins too. Because YamlDiscovery isn't a service, added a subclass that accepts a YamlCacheCollector - this works and also saves memory. Did it without dependency injection to start with to get some numbers.

Before/after screenshots. Note these aren't like for like because the before includes everything that YamlDiscovery is used for and the after only includes routes, but routes are the main time in the first one, so it's possible to compare still.

Also note this is comparing a FileCache miss (on the basis that will happen on different web heads, between cli and web etc.) and a CacheCollector hit - because the goal here is better hit rates.

I got the 'before' via:

1. install standard.

2. Browse to admin/config/development/performance ready to clear the cache.

3. ddev restart && ddev xhprof on && ddev xhgui (so that apcu is empty)

4. Submit the cache clear form

For the after you just have to clear the cache via the form a couple of times.

Standard has less routes than Umami which has a lot less routes than most production sites, so I'd expect bigger savings in production.

Big advantage of this for routing is that the cache will be shared between drush cr building routes on the cli vs. something like saving a view, which also rebuilds the router.

On standard this saves about 190ms wall time and 350mb memory - but this is only applied to one of the caches on that request, there are lots of other candidates.

catch’s picture

catch’s picture

Issue summary: View changes
catch’s picture

Status: Needs work » Needs review

Had been wondering in the back of my head whether we should consider using hash_file instead of mtime for the file comparison, but the garbage collection is able to use the stored mtime and wouldn't be able to use hash_file() so probably better to stick with mtime, or at least we'd still need to store mtime either way or come up with a different garbage collection approach.

Went ahead and updated performance tests.

Per the issue summary, there are more places we can apply this to:

- Yaml cache collector can be used for menu links, local tasks, local actions, ckeditor5 plugins, breakpoints and more.

- we can add js/css minification caches

- attribute plugin cache

However some or all of these are follow-up material.

Updated performance tests - the updates show a single cache get after all cache bins are emptied, no set/lock etc. - this is showing the persistence working since the bin we use doesn't get cleared by the tests. The single cache get is a big different from the previous approach on this issue which has a cache entry per-file. Once we add it to more places the cache gets will go up by one per discovery type.

I think there's enough here for an initial review.

catch’s picture

Issue summary: View changes
catch’s picture

Issue summary: View changes
needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new1.52 KB

The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

catch’s picture

Status: Needs work » Needs review
godotislate’s picture

Status: Needs review » Needs work

This is nice work and surprisingly straightforward. A few comments on the MR, mostly minor or nits, but a couple questions about test coverage.

Also, should the issue title be updated to mention that this is for YAML discovery only?

catch’s picture

Status: Needs work » Needs review

This is nice work and surprisingly straightforward.

I was also surprised especially given how complicated the previous attempt ended up before abandonment.

Also, should the issue title be updated to mention that this is for YAML discovery only?

Not sure about this yet.

I'm pretty sure we can apply it to css/js minification and attribute plugin discovery. Doing that here might make the scope a bit to big, but it would also allow us to add a base class or trait which encapsulates all the non-parsing logic for the cache collector (mostly the mtime checks) in a way that hopefully would result in less churn later. So.. I am thinking of trying to add support for one or the other or both, then if it feels a bit unwieldy, splitting it out after that.

If that turns into a bigger job than expected though, then yes we should only cover YAML here and just look at everything else in follow-ups.

I applied several of the suggestions, responded to a couple, and also added test coverage for the ::updateCache() override.

godotislate’s picture

Will review MR updates later tonight, but one thought came to mind: Attribute plugin discovery is in the Component namespace, and CacheCollector is in Core. Do we need to introduce some interface in Component that a CacheCollector can implement? Or will the cache collector live only in the Core AttributeClassDiscovery subclass?

catch’s picture

Or will the cache collector live only in the Core AttributeClassDiscovery subclass?

What I'm hoping is that we can do something very similar to the YamlCacheCollectorDiscovery added here - subclass with a couple of method overrides to change how the caching works and not too much else.

godotislate’s picture

also added test coverage for the ::updateCache() override.

I don't see this test coverage? Other than that, MR looks good. I resolved a couple comments, and left couple of minor suggestions still open, though I'm fine if you want to resolve them without accepting.

I think this is good to expand to the other file caches.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new1.49 KB

The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

catch’s picture

Status: Needs work » Needs review

OK I started to look at attribute plugin discovery, the file parsing logic is a lot more complicated than YAML, so it's going to mean refactoring parts of the component attribute parsing out to a trait, then using those methods in a new core attribute discovery that shares the trait but instead inherits from the cache collector. It seems doable but too much to review in a single MR.

I've added the base class, so I think what I'll try is opening a separate draft MR for the plugin attribute parsing, that will validate whether the base class is flexible enough. We can then move those commits over to a new issue when this one is more or less ready.

On js minification when I opened this issue we hadn't done #3505776: Retain asset aggregates for 45 days by default to reduce generation of identical files, but that issue will mean a much lower cache rate here, so while it will still help for files duplicated between aggregates, or building an aggregate with 10 files and where only one changed, that's still a very low cache rate, and other asset changes in the queue should progressively make it even lower (by making the hit rate on aggregates much higher which is what we want). So going to hold off that bit for now.

godotislate’s picture

so it's going to mean refactoring parts of the component attribute parsing out to a trait

I wonder if doing so could help out with #3586328: [11.x] Handle attribute discovery fatal errors on missing traits for routing and other subsystems, at least with organizing things into reusable methods that would be useful across attribute discovery for different things.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new2.09 KB

The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

catch’s picture

Status: Needs work » Needs review
Issue tags: +no-needs-review-bot

Started the attributes branch.

Attribute parsing doesn't neatly fit into the cache collector API because you need more than the filename to parse the attribute, so the ::get()/resolveCacheMiss() pattern breaks down a bit. On the other hand it's easy to inline a bit of that logic into ::findDefinitions() and those other methods won't get called - probably need to BadMethodCallException from them.

I've done enough to the branch that it doesn't error, however it doesn't do dependency injection or bc properly yet and there are multiple layers of that to untangle (both the discovery and the various plugin managers will need various constructor bc layers). Haven't profiled it yet either. If it works enough to profile though, that should give us some numbers to look at - at which point can move the branch to its own issue and there should be some motivation to work on the constructor deprecations at that point.

catch’s picture

Very initial profiling of attribute discovery changes - just the standard profile saving the clear caches form. The form itself doesn't cause a lot of plugin discovery compared to a busy front end page with entities etc. so not the biggest saving, but still visible.

One is a cache hit, the other is a cache miss (e.g. after TRUNCATE cache_file_parsing).

The big thing is we're saving about 450kb of memory on a cache hit - again we already get that when there's a filecache APCu hit, the idea is to have that more often.

catch’s picture

I've opened #3591677: [PP-1] Add a persistent cache for plugin attribute parsing and moved the attributes MR progress over there, closed the one here.

Edit: and also #3591680: Use the YAML parsing cache collector for config file storage for another case where we parse a lot of YAML.

godotislate’s picture

Commented on the MR that maybe it makes sense to create cache collector services like library.parsing_cache to pass into the other discovery classes, so that we're only passing in 1 service instead of three (cache backend, lock, time).

Could maybe use a factory pattern of either a YamlCacheCollectorFactory/AttributeCacheCollectorFactory, or a FileParsingCacheCollectorFactory. Something like:

Drupal\Core\Utility\FileParsingCacheCollectorFactory:
  arguments: ['@cache.file_parsing', '@lock', '@datetime.time']
library.parsing_cache:
  factory: ['@Drupal\Core\Utility\FileParsingCacheCollectorFactory', 'get']
  arguments: ['yaml', 'library.parsing_cache']
  tags:
      - { name: needs_destruction }
class FileParsingCacheCollectorFactory {

  public function get(string $discoveryType, string $cid, array $tags = []) {
    return match ($discoveryType) {
      'yaml' => new YamlCacheCollector($cid, $this->cache, $this->lock, $this->time, $tags),
      'attributes' => new AttributesCacheCollector($cid, $this->cache, $this->lock, $this->time, $tags),
      default => //...
    }
  }
catch’s picture

Pushed the change to pass in a yaml cache collector instead of three services.

Factory pattern - didn't do this yet because I'm not sure what's best - it might be worth making any factory format-specific rather than one factory for the various formats. So start with a yaml one and see if we need an attributes one etc.

godotislate’s picture

Yeah, when I thought about all the plugin discovery subclasses and plugin managers, passing in 3 new arguments seemed like a lot.

And no opinion on whether it should be format specific, other than whether there are enough of each (or total) that it's worthwhile to use a factory or factories.

godotislate’s picture

Status: Needs review » Needs work

Merge conflict on the MR and PHPCS/PHPStan issues.

catch’s picture

Status: Needs work » Needs review

Should be happier.

godotislate’s picture

Status: Needs review » Reviewed & tested by the community

The deprecation message has version 11.4.0, which maybe will need to be bumped? But otherwise LGTM.

catch’s picture

Title: Add a persistent cache for file-based discovery based on FileCache » Add a file parsing cache collector to replace some uses of FileCache

Re-titling to be a bit closer to what the new implementation is.

catch’s picture

Rebased.

nicxvan’s picture

godotislate’s picture

I don't think so, but we can maybe use the db-backed file cache here in that issue.

catch’s picture

That one gets tricky due to the installer and container rebuilds so would definitely need to be it's own issue.

catch’s picture

Rebased this one and added an comment to the service definition.

godotislate’s picture

RTBC +1 for changes.

alexpott’s picture

Version: main » 11.4.x-dev
Status: Reviewed & tested by the community » Patch (to be ported)
Issue tags: +Needs change record

We need an 11.4.x / 11.x version of this... I also realised after committing that a CR is in order here.

Committed and pushed dfa3792d478 to main. Thanks!

  • alexpott committed dfa3792d on main
    task: #3486503 Add a file parsing cache collector to replace some uses...
catch’s picture

Status: Patch (to be ported) » Needs review

Added a change record.

Also pushed an 11.x backport.

  • catch committed 5768a3d2 on main
    task: #3486503 Add a file parsing cache collector to replace some uses...
catch’s picture

Pushed a small follow-up commit to main to fix the namespace of a unit test - which failed hard in the 11.x backport MR.

catch’s picture

Version: 11.4.x-dev » 11.x-dev
Status: Needs review » Reviewed & tested by the community

Backport is green - only test changes and apart from silly mistakes the conflicts were easy to fix, so moving back to RTBC.

alexpott’s picture

Version: 11.x-dev » 11.4.x-dev
Status: Reviewed & tested by the community » Fixed

Committed and pushed 0be9248e5c0 to 11.x and fd5a6fdad51 to 11.4.x. Thanks!

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

  • alexpott committed fd5a6fda on 11.4.x
    task: #3486503 Add a file parsing cache collector to replace some uses...

  • alexpott committed 0be9248e on 11.x
    task: #3486503 Add a file parsing cache collector to replace some uses...

Status: Fixed » Closed (fixed)

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