Repository navigation
[6.x] Add an opt-in Stache cache store that OPcache can hold in shared memory - #15884
Draft
DanielDarrenJones wants to merge 16 commits into
Draft
DanielDarrenJones wants to merge 16 commits into
DanielDarrenJones wants to merge 16 commits into
Conversation
An opt-in store, chosen by setting statamic.stache.cache_store to stache_compiled. Entries and plain values (indexes, paths, keys) are written as PHP files that return arrays, named after a hash of their contents, so OPcache serves them from shared memory and never has a stale copy. Anything else, and anything with a time to live, goes to an ordinary file store underneath. Writes are held until the end of the request, job or command and then merged with what other processes wrote in the meantime. Where two processes changed the same entry or the same key of an index, neither copy is kept, and the Stache works it out again from the content files.
…orker A save or delete is written as soon as the store has it, so another process never works from the old copy for longer than it would with the file store. Filling the cache is still held until the end of the request. What was read, and what was just written, is kept across writes so a process that saves many times doesn't read everything back each time.
…one process Clearing forgot the duplicates in the cache but not the ones in memory. When the warm that followed found the same duplicate again it wasn't new, so nothing was written back, and the list stayed empty until the next clear. Every other stache:refresh lost them.
A collection's entries were always split over 32 files, so saving one entry rewrote a thirty-second of the collection: about 60 entries of a 2,000-entry collection, and more as it grew. The number of files is now settled when the collection is built, at about eight entries to a file. A save also no longer reads everything back under the lock when no other process has written, or rewrites a value that was put back as it was.
Saving an entry took its rows out of each taxonomy's associations, added them again under new keys, and wrote the whole index, whether or not its terms had changed. On a site with a 1,000-term taxonomy used by 2,000 entries that is a couple of megabytes written per save. When the rows that are there are the rows that would be put there, they're now left as they are.
What a process first saw of a value is kept for as long as the Stache may still be working from it, so a second write in one request is compared with the right thing. An entry save writes again once its terms and structure have been dealt with. Files are cleared out by when they stopped being needed, under the lock; the lock is beside the store, so flushing leaves it; another process's writes are noticed by a generation marker; and what the pointer no longer needs is deleted only after the pointer has been replaced. False, arrays of objects, dates, items with their own serializing, and something that expires being incremented all now come back as the file store would give them.
A value kept serialized that another process changed is not written over on a second write either. An array is looked through for objects when it is stored, however deep they are. An item that implements Serializable is left to be serialized. What tells a process that another has written is now the start of the pointer file itself, so the two can't be out of step.
… list of indexes A process that has had one of its writes turned down for an entry, a value or anything kept serialized has every later write for it turned down too, until it starts afresh. Checking against what it first saw wasn't enough when that was nothing at all. The list of a store's indexes is the exception to forgetting what two processes disagree on. An index the Stache doesn't know it has is never updated or cleared, so two versions of the list are combined. An array is always looked through for objects and references when it's stored.
A value is kept as PHP or serialized, and each was compared with what was first seen on its own, so one process writing it one way while another wrote it the other went unnoticed. Disagreements are now noted under the Stache's own key for both. The duplicate ids found while building paths are combined like the list of indexes, since nothing rebuilds them once the paths are kept.
A process that worked something out before another forgot it could put it back: forgetting left nothing to see where the thing was not there to begin with, and a thing that had gone was taken as never written. Forgetting now leaves a marker that is different each time, a collection built again over a different number of files turns down what was held from before, and two versions are no longer combined when what they started from has been cleared away.
A marker left for every forgotten key stayed in the pointer for as long as the key went unread, which after clearing a large site is most of them. One of 256 shared markers is changed instead, chosen by the key, so the pointer stays the size it was however much is forgotten.
The list of a store's indexes and the duplicate ids were put together with nothing when another process had forgotten them, which put them back. They are turned down now like anything else, and with the list go the indexes that would have been found through it. Something turned down a second time marks it again, for a process that began after the first.
A queue worker forgets what it has read before each job, so that it sees what other processes have written, and then read every file again. Without OPcache, which is the usual thing on the command line, that meant compiling each one again for every job. A file never changes, so it is kept by name and let go of once the pointer no longer has it.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Stache keeps entries and indexes in a cache store, serialized. Every request that needs them reads the files and unserializes them, into memory of its own. This adds a second store the Stache can be pointed at, which writes the same things as PHP files that return arrays. OPcache holds those in shared memory, so a request reads them without a file read, without unserializing, and without a copy of its own.
It is the store from Flatpack, a free addon for Statamic 5.11 and up released earlier this month, without the parts of Flatpack that aren't about storage, and with the handling of several processes writing at once tightened up for core.
Turning it on
or
STATAMIC_STACHE_CACHE_STORE=stache_compiledwith a config file that reads it. Nothing changes for a site that doesn't.How it works
[id => entry as an array]. How many is settled when the collection is built, at about eight entries to a file, so saving an entry rewrites a handful of others rather than a fixed share of the collection.validate_timestampsis set to. A small pointer file maps each name to its current file and is read as JSON.Benchmark
Against
6.xat 9d20aef with the file store, PHP 8.5, Apple M1 Pro. A generated site of about 2,500 entries on two sites, three taxonomies (one of 1,000 terms), 2,000 assets, and deliberately awkward content. The machine was busy while I measured; the ranges were wide, the direction was the same in every run.Pages served from one long-lived worker, so that OPcache is holding the files in memory as PHP-FPM would (medians of 20 requests each):
All 24 pages measured are between 13% and 33% quicker.
Stache::warm()from nothingWhere it isn't better:
stache:warmon a deploy is quicker with this store, as above. It can't fill PHP-FPM's OPcache from the command line, though, so the first web request to touch each file after a deploy compiles it, once.opcache.enable_cli=1. The first job is slower (about 190 to 260 ms against 90 to 170 ms), and with OPcache off the worker holds what it has compiled itself: 116 MB against 48 MB here. With OPcache on in the CLI it is 13 MB against 24 MB.Checked
With the store on, against the same code with it off:
Known gaps
stache:warm.opcache.max_accelerated_files.This branch also carries
#15882 (term associations left alone when an entry's terms haven't changed) and #15883 (duplicate ids kept when the Stache is cleared and warmed in one process), so that the numbers and the checks above are of the store and not of them. They'll drop out when those merge.
Tests
CompiledStoreTest: what it keeps and where; nothing written until it persists; values kept as they were when given; forgetting and flushing; expiry; seeing another process's writes; two processes saving different entries, the same entry, different keys of an index, the same key; a second write compared with what was first seen; something another process has forgotten, whether or not it was there, kept either way; a collection built again over a different number of files; an index whose list was forgotten; what was first seen having been cleared away; garbage collection by when a file was last needed; a file read once however often what was read is forgotten; a file that has gone; entries, dates and items with their own serializing coming back as unserializing would give them.