Proposed commit message:
Issue #2429617 by Wim Leers, Fabianx, Berdir, yched, dawehner, effulgentsia, catch, borisson_, jhodgdon, martin107, torgosPizza: Make D8 2x as fast: Dynamic Page Cache: context-dependent page caching (for *all* users!)
Dynamic Page Cache (formerly known as SmartCache, until comment ~380) in a nutshell:
- cache the
CacheableResponseInterfaceobject (which in case of a
HtmlResponsestill has the
#attached[placeholders; those are replaced at a later time, by
- return that cached response immediately after routing has taken place, hence avoiding the controller service (and its dependencies) being initialized, along with everything else for building the response.
The resulting performance improvement as user 1 on the frontpage (APCu off, OpCache on, PHP 5.5, XDebug off, XHProf on):
|Run #frontpage-before||Run #frontpage-after||Diff||Diff%|
|Number of Function Calls||31,794||16,749||-15,045||-47.3%|
|Incl. Wall Time (microsec)||89,291||57,922||-31,369||-35.1%|
|Incl. MemUse (bytes)||17,050,176||13,120,368||-3,929,808||-23.0%|
|Incl. PeakMemUse (bytes)||17,101,080||13,153,056||-3,948,024||-23.1%|
(See #205 for details.)
- We want D8's authenticated user page loads to be fast.
- Some parts of the page are cacheable across users. To actually cache that across users, we have .
- Other parts need to be dynamically calculated per user, or are simply uncacheable.
- Those dynamic parts should not prevent us from showing the rest of the page first.
(Drupal 8's anonymous user page loads already are fast since.)
We're render caching bits and pieces (blocks & entities), but we're still running expensive controllers to build the main content array. And we're initializing dozens and dozens of services that we barely use. It's getting more difficult to see what to optimize next.
Drupal has historically always generated the majority of the response dynamically for every request.
But we've been doing massive amounts of work to make things more cacheable. Could we make use that work to break the trend, and make Drupal 8 the first release to generate a minority of the response dynamically for every request?
Proposed resolution: the simplified version
Assumption: everything that depends on some (request) context, specifies a cache context. has made sure that this is implemented consistently (and tested) for the 99% use cases.
- On a cache miss
KernelEvents::RESPONSEevent subscriber detects whether it's a
CacheableResponseInterfaceobject, i.e. a response with cacheability metadata that SmartCache can therefore safely cache. This an object representing the entire response. In case of a
HtmlResponse, it still has the placeholders (that need to be replaced on every request, i.e. dynamically, uncacheable).
- The result is that we therefore have the response that is cacheable across all users, because we know which cache contexts are associated. We use the
RenderCacheclass that is capable of handling cache redirects (which is in fact based on early versions of the SmartCache patch), and ask it to cache the response per route, and if necessary, perform cache redirection.
- The event subscriber ends. Then the response is finished and sent as usual. (The other event subscribers fire, including in case of a
HtmlResponseSubscriber, which calls
HtmlResponseAttachmentsProcessor, which does all the final rendering (just like for any other request): it renders the attached placeholders, bubbles the bubbleable metadata from the placeholders, and given that final set of attachments, it is able to render the CSS/JS assets, plus HTML
<head>tags + HTTP headers.)
- On a cache hit
- We've already done the above, and now we're hitting the same route again.
- Immediately after routing, before even calling the controller (the thing that would otherwise do DB queries and whatnot to build a render array), SmartCache's event subscriber checks if we have a cache item in the
smart_cachecache bin for the current route, following any redirects if necessary.
- If such a cache item (including potential cache redirect) exists, then the response for this route is already cached. Hence the controller never needs to be executed, and all that still needs to happen (in case of a response other than
HtmlResponse, otherwise we're done already), is processing the attachments! (Which includes rendering the placeholders.) This is handled automatically; just like for any
How is this related to the BigPipe issue?
The BigPipe issue:.
Basically, SmartCache doesn't care about things that are dynamic (
max-age = 0 or cache contexts that we consider "too dynamic to cache", such as "per user"). It's very simple, dumb and stupid. It simply varies by all cache contexts on the page.
So, if e.g.:
/foovaries only by interface language, then SmartCache's entry for that will also vary by interface language
/barvaries by interface language, route, permissions, query parameter, user and whatnot, then the SmartCache entry will also vary by all those things
That's where the intersection with BigPipe begins.
BigPipe allows us to NOT have the bits that are "too dynamic" to affect the overall page: it allows us to NOT bubble up the "per user" cache context, for example. Because we replace it with a placeholder, and render it separately.
- See the simplified proposed resolution above.
- Read the issue summary at .
Given what you've just read in #2429287, you may now realize that once:
- Cache contexts are defined by everything, as they should be (i.e.
once that meta is completedsince is 99% done, which it now is)
- Cache context bubbling is implemented (child of the meta: )
- All uncacheable things use placeholders (just like: node links, comment links, the comment form on entities, and more)
… then we can start to put all of that cache metadata to the originally intended powerful use: cache the entire
HtmlResponse, varied by the contexts associated with that route!
(And once we have that, we can go even further! We could make it configurable which cache contexts (high-frequency ones, e.g. "per user") we don't actually want to vary by, which we could then automatically replace with placeholders. We could then run replace those placeholders either when rendering a response (like today), replace them using something like Facebook's BigPipe, or replace them using ESI. It would be configurable. We have an issue now for auto-placeholdering: .)
A final note: SmartCache works post-routing and caches per-route. You may wonder:
why not pre-routing and caching per-URL? — see #219 for a detailed answer. But in short: because the SmartCache architecture is based on cache contexts, and several cache contexts are not available pre-routing/pre-bootstrap (
route, and likely more). Which means SmartCache cannot work pre-routing.
SmartCache reuses the logic in
RenderCache, at the cost of some (isolated!) ugliness because
RenderCache only knows how to deal with render arrays. The benefit of this reuse is that we don't duplicate the logic, and can just depend on the existing comprehensive test coverage.
Algorithm, proofs & next steps — comments wanted!
This issue corresponds to step 1 in the Google Doc: https://docs.google.com/document/d/1Gw7ohBOUKu38t4kMbN9zj6cX-4-_2ZNXGotv...
(We will move this onto Drupal.org once it's 100% fleshed out.)
When this gets committed, please also credit Fabianx.
User interface changes
None. This is a pure addition; that doesn't change any APIs.
But, notable changes:
- Forms are now marked as uncacheable (
max-age = 0) by default.
- The REQUEST event subscriber
ContentControllerSubscriber::onRequestDeriveFormWrapper()has a different priority, because it was impossible to inject an event subscriber at the desired place (i.e. to inject a new event subscriber in between).
And a few small bugfixes, mostly small remnants of things that were fixed in blocking child issues, but don't really make sense to fix separately because of process overhead:
TestAccessBlock::blockAccess()specifies max-age=0 because it depends on state.
renderedcache tag rather than deleting everything in the
renderedcache tag because it is expecting path alias changes to show up immediately, despite there still being an open issue ( ) to make path aliases handle cache invalidation correctly. So, this is a temporary fix, with a @todo added.
ConfigCacheTagconfig save event subscriber was only invalidating the
renderedcache tag for
system.theme.global, it also needs to do it for
TestControllers::testEntityLanguage()'s render array was failing to pass on cacheability metadata.
HUGE HUGE HUGE thanks to Fabianx, who's helped me enormously in verifying that this actually works! Without him, this issue would be much vaguer. Plus, once this is in, he has amazing ideas for further improvements, he even sees a way to make it work without executing a request at all — resulting in 10–20 ms response times! See the aforementioned Google Doc, steps 2 & 3, for details.