Repository navigation
[6.x] Build Stache indexes in one pass without holding every entry (opt-in) - #15856
Open
DanielDarrenJones wants to merge 6 commits into
Open
DanielDarrenJones wants to merge 6 commits into
DanielDarrenJones wants to merge 6 commits into
Conversation
Warming the Stache made every entry of a collection, kept them all, and then had each index read all of them. Items are now made and cached one at a time, and every index that takes its value from a single item is filled from one pass over them. Entries are let go of once they have been indexed. Entries that need another entry to be indexed (a localization, an entry in a structure, anything whose values go looking for other entries) are indexed in a second pass, read back one at a time. A structure's uri, parent and order indexes are still built the way they were, once the rest are there. Term associations are built from the entries' own indexes rather than by loading every entry that has a term.
A computed value that queries its own collection asks for an index while the entries are still being made. That entry waits for the second pass, but the index was left marked as loaded with nothing in it, so the second pass read it as empty. Warming also filled indexes when updating them was disabled, and a parallel warm's workers didn't know a warm was running.
DanielDarrenJones
marked this pull request as ready for review
November 12, 2026 00:07
Filling a store's indexes in one pass is off unless statamic.stache.single_pass_indexing is enabled. With it off, paths, warming and term associations run the code they did before.
Whether a collection has a structure was asked for every entry as it was released, and each time the collection was read back from the cache. On a site of 9,780 entries that was about a second of a twelve second first query.
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.
Warming the Stache makes every entry of a collection, keeps all of them in memory, and then has each index read through all of them. Peak memory is the whole collection, parsed, and it grows with the content.
Entries are now made and cached one at a time, and the indexes are filled from a single pass over them. An entry is let go of once it has been indexed, so what's held is the index values rather than the entries.
Opt-in on 6.x
This changes how every store builds its indexes, so on 6.x it is off by default:
A site whose published
stache.phppredates this needs that line added before the env variable does anything. With it off,paths(), warming and term associations run the code they run on6.41.0: the old paths are kept beside the new ones rather than rewritten, and the numbers and the rendered output match6.41.0(below).If you'd rather not carry a switch and two code paths in 6.x, I'm happy to hold this back, take the opt-in out and re-target it at 7.x as the only behaviour. That version is the first three commits here; the opt-in is the last one on its own.
What changes
With the option on:
Store::paths()makes, caches and moves on, rather than building every item before caching any.Store::fillIndexes()fills every index that takes its value from one item at a time (Indexes\Valueand its subclasses) in one pass.Index::update()goes through it, so the first index a cold request needs fills the rest of that store's indexes in the same pass, instead of each reading every item for itself later.uri,parentandorderindexes are still built the way they were, after the others. Finding an entry's place in a tree queries the collection, which needs its other indexes to exist. Entries of a structured collection are therefore still held, as they are today.An index that overrides
getItems(), or a store that overridesgetItemsFromFiles(), is left to build itself as before.Benchmark
Against
6.41.0, with the option off and with it on, alternating, five rounds each, on an Apple M1 Pro with PHP 8.5 and the file cache store. Each run starts from a cleared Stache. Medians, with the range of the five runs.A generated site of 9,780 entries (3,000 of them localizations across three sites, nested page builder content, two taxonomies):
stache:warm, timestache:warm, peak memoryA second generated site of 2,700 files (two sites, a structured pages collection, a collection mounted on a page):
stache:warm5.1 s and 357 MB before, 5.4 s and 87 MB after. One run each, so the times are level and the memory is the result.The first query on a cold Stache is slower. It's a query for twelve books ordered by title. Before, it built the four indexes it used and left the rest for later requests. Now it builds all of that collection's indexes in its one pass. The total work is less, but more of it lands on that first request. A site that runs
stache:warmon deploy doesn't see this.Profiling the warm: 23% of the time on
6.41.0goes on reading entries back out of the cache, against 9% here.Parallel warming
With
STATAMIC_STACHE_PARALLEL_WARMING=trueon the first site, three rounds each:On
6.41.0the worker processes run into PHP's 512 MB memory limit (Allowed memory size of 536870912 bytes exhausted), and the warm logsParallel warming failed, falling back to sequential. Here each worker stays under the limit, so it runs as intended. The indexes and URLs are the same as a sequential warm on6.41.0. It isn't faster than a sequential warm on this site, because one worker still does nearly all of the entries.Checked
Same result. Every index of every store and every entry and term URL is identical before and after on both sites (83 indexes and 12,840 URLs; 106 indexes and 2,884 URLs).
Octane and queue workers. One long-running process, with a second process saving an entry, rebuilding the Stache and clearing it in between, run as a worker would handle requests (new container sandbox and facades each time) and as it would handle jobs (the between-jobs reset). Every value the worker read matched
6.41.0at every step. Nothing is kept on a store between requests or jobs: the state of a pass is cleared when it ends, and the marker that stops a store being filled twice in one warm only exists whileStache::warm()is running.Entries held by that worker after first use went from 9,780 to about 30, and its memory from about 650 MB to about 265 MB.
Off is 6.41.0. On a third generated site with deliberately awkward content (hand-written YAML, number and duplicate ids, computed values that query, scheduled entries, a thousand-term taxonomy, relations across sites, six-level trees), every page, Control Panel response, a fixed list of 136 indexes and the results of 44 queries are identical to
6.41.0with the option off and with it on.Not covered
Tests
FillingIndexesTest(new) and three additions toEntriesStoreTest:The existing snapshot tests from #15795 pass unchanged. The full suite passes with the option off and with it switched on for every test (11,758 passed, 12 skipped, both ways).