Problem/Motivation
With the introduction of placeholders it would now be possible to render those placeholders with different strategies.
However one key ingredient is missing:
A ChainedPlaceholderStrategy that is used to coordinate different rendering strategies as placeholders can be rendered via BigPipe, ESIBigPipe, ESI, Ajax, AjaxStream, SingleFlush, etc.
The reason why this is important even in core is:
a) For auto-placeholdering after rendering, it is needed to store the already calculated data in a static per request cache, but some placeholder strategies like ESI MUST ignore that and be able to override the output, while SingleFlush and BigPipe MUST make use of it to not render things twice ... Hence one needs to come before, the other afterwards. #2543334: Auto-placeholdering for #lazy_builder with bubbling of contexts and tags is affected by that.
b) It is great if core and contrib can rely on one common API to coordinate on.
c) BigPipe in particular would be combined with a CachedPlaceholderStrategy. Compared to single flush, that one could make use of $renderCache->getMultiple() (as soon as that exists), because again something that is cached already does not need to be BigPipe'd.
d) Especially with custom code placeholders make it really simple to e.g. render this one block via custom JS + cookie or update it via NodeJS or update it via Angular, etc.
e) We want to keep the API especially simple as for the 99% case this simple is sufficient. For anything more complex a custom response subscriber can still be used as this approach intentionally allows altering before and after this subscriber.
Proposed resolution
- Add placeholder_strategy.chained service as ChainedPlaceholderStrategy
- Add placeholder_stategy.single_flush
- Call the ChainedPlaceholderStrategy from a HtmlResponse subscriber.
Remaining tasks
- None
User interface changes
- None
API changes
- API addition: Added a render_strategy_manager service
This is just a very thin API layer to allow different strategies to coordinate.
Data model changes
- None
Beta phase evaluation
Issue category | Task because nothing is broken. |
---|---|
Issue priority | Major because it blocks BigPipe and other things that can make Drupal 8 way faster. |
Prioritized changes | The main goal of this issue is to allow to improve Performance of Drupal 8 and unblock contrib / BigPipe by unifying and completing the API around placeholders. |
Disruption | Not disruptive for core/contributed and custom modules/themes. |
Original report
Motivation
Given the cache contexts we can have authenticated user caching, big pipe, esi etc, out of the box and it is _easy_.
Given cache IDs that look like e.g.
block1:%user
we can check if cache contexts match.
So if we try to render a block with a %user cache context in a %page context, we replace it with a placeholder in #post_render_cache. (see my nice RenderCachePlaceholderInterface class in other issue)
Then the different rendering strategies can determine how to replace the placeholders.
e.g. in an alter hook, there is a strategy chain like:
esi, big_pipe
And the ESIRenderingStrategy can determine if ESI is possible (based on Varnish header), then replace the placeholders, but if it does not support it, then it would fallback to the next strategy until all placeholders are replaced.
Another use case is an authenticated user cache (this was discussed with Mark Sonnabaum and he liked the idea):
We store the page with the placeholders in the cache, but replacing the default #post_render_cache placeholders with cache IDs and pushing the cache IDs to [#attached]['placeholders'] e.g. block1:%user, then after caching rendering the placeholders normally (so they are cached).
On a new request to the page:
All a HtmlKernel Middleware would need to do is:
- Get cache IDs from #attached
- Expand the Cache IDs %user-> user:2 (resolving the cache context)
- cache_get_multiple()
-> Hit: Replace placeholders
-> Miss: Abort and run ->handle();
And thats it!
Authenticated user caching out of the box!
Resolution
- Add getRenderStrategy() to the CacheableInterface or to some similar place
- Ensure rendering strategies can be altered and also globally disabled per request:
e.g. google bot should never see big piped content, but its okay to use the stale_cache rendering strategy.
Comment | File | Size | Author |
---|---|---|---|
#52 | support_placeholder-2349011-52.patch | 14.64 KB | Fabianx |
| |||
#52 | interdiff.txt | 1.24 KB | Fabianx |
Comments
Comment #1
Fabianx CreditAttribution: Fabianx for Acquia commentedThis predated the work in #2429287: [meta] Finalize the cache contexts API & DX/usage, enable a leap forward in performance, but adding that as parent now.
Comment #2
Wim LeersAFAICT this is wholly a duplicate of #2469431: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts by now.
Comment #3
Wim LeersComment #4
Fabianx CreditAttribution: Fabianx for Acquia commentedNot really, it is a sub-task that can go independently, will provide a patch today or tomorrow.
Comment #5
Wim LeersOh, wait, is this supposed to be the issue representing step 3 of #2469431: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts, i.e. ? If so, let's make it a child of that one, and link to this from that issue's IS.
Comment #6
Fabianx CreditAttribution: Fabianx for Acquia commentedYes, it is, just that this issue was long existing ...
Comment #7
Wim LeersClarifying title. And adding to the IS of [#2469431.
Comment #8
Wim LeersComment #9
Wim LeersThe IS definitely needs an overhaul.
Comment #10
Fabianx CreditAttribution: Fabianx for Acquia commentedComment #11
Fabianx CreditAttribution: Fabianx for Acquia commentedThanks to all the pre-work [placeholders, HtmlResponse introduction] this has become a very very simple task by now.
Attached patch is pretty much complete (I will update the BigPipe issue with it attached) and useful on its own.
It only needs some unit tests and should then be ready. :)
This hopefully is already green.
@Wim:
Please note the changes to move replacePlaceholders() around are not 100% necessary for this patch.
However the justification is that if it works within the renderer outside of the stack bubbling (because it uses renderPlain and merges metadata itself), then it should also work outside.
So this is just some hardening to make the code easier to understand that rendering placeholders is 100% stack / render context isolated rendering.
The public renderPlaceholders() works for me - even in BigPipe! (yeah!)
Great and fantastic work on the pre-issues to this one!
I could just take my WIP, simplify and strip all the complicated parts and my render strategies started to work.
Comment #12
Wim LeersFirst: glad to hear that :)
Second: wow, you weren't kidding -- this patch is ridiculously simple!
Here's an initial code review.
Once #2450993: Rendered Cache Metadata created during the main controller request gets lost lands, this will then have to be executed in a render context. But that's fine.
Can you explain why this needed to be moved?
This I don't understand at first sight either. Again a bit of the background info/reasoning would be welcome :)
"process" vs. "render"
Let's be consistent.
s/render cache array/placeholder render array/
(Or whatever the exact name is that HEAD uses. In any case *not* "render cache array".)
80 cols.
I think it's worth calling out that this is a *list*, i.e. an array where order matters, because it determines render strategy execution order.
This should mention it relies on service priorities etc.
The render strategy to add.
array
s/new/processed/
Missing newline in between.
Comment #13
Wim LeersThis won't work anymore now that #2450993: Rendered Cache Metadata created during the main controller request gets lost is in.
You need to wrap the
->render()
call inexecuteInRenderContext()
.Then the
$this->renderStrategyManager->renderPlaceholders()
call can still happen as it is right now.Comment #14
Wim LeersI thoroughly reviewed this issue/patch. The patch in #11 was green — per #13 it needs to be rerolled, but that doesn't mean the patch isn't already functionally working.
Renderer
, none of which cause API changes.HtmlRenderer
, none of which cause API changes.Therefore, AFAICT, this patch does not cause any API changes, which means it can definitely remain major.
Comment #15
Fabianx CreditAttribution: Fabianx for Acquia commentedRe-roll and moving to smart cache strategy (being compatible with that now again):
- Added new HtmlRenderStrategySubscriber
- Moved from renderPlaceholders to processPlaceholders().
Render Strategies just interact with placeholders in attachments and nothing else.
Still need:
- to address feedback
- to write tests
Needs review for test bot.
Comment #17
Fabianx CreditAttribution: Fabianx for Acquia commentedIt helps to add all used proxies :)
Comment #19
Fabianx CreditAttribution: Fabianx for Acquia commentedThanks to Wim, I found out what I was missing:
- The required cache contexts are not applied when using ->render() unless '#cache' keys is set - due to performance reasons.
Added an applyRequiredCacheContexts() function for now to Renderer(Interface) to easily make those available, without exposing internal details.
Comment #20
Wim LeersPatch review:
Why lazy? Almost every page contains at least one placeholder: that for the status messages. So we don't actually gain anything AFAICT.
Why an event subscriber, and not what I proposed in #2429617-207: Make D8 2x as fast: Dynamic Page Cache: context-dependent page caching (for *all* users!).3 (i.e. letting
HtmlResponseAttachmentsProcessor
call the render strategy manager)?Why does this need to be moved?
And why does this need to be moved?
I think you're moving this simply because there's no need for that to happen so late anymore, now that
Renderer::render()
no longer is replacing placeholders?Render strategies are specifically about placeholders.
So, shouldn't we rename them to "placeholder render strategies"?
Or do you foresee additional things besides placeholder rendering in the future?
This needs a unit test.
This interface is effectively identical to
RenderStrategyInterface
. Do we really need it? If we remove it, the pattern here would match that of e.g.OutboundPathProcessorInterface
being implemented byPathProcessorManager
. In other words: the manager is just a special aggregating strategy, rather than a separate thing.Interdiff review:
s/Also see FUNC function below/@see FUNC/
This strange documentation is precisely why I'd argue we should have
::getRequiredCacheContexts()
instead. This would then be usable by any code, regardless of theRendererInterface
implementation.(e.g. a module that does some automated cacheability analysis; it'd need to know the required cache context of the renderer, and should not have
Comment #21
Fabianx CreditAttribution: Fabianx for Acquia commented#20
1. removed and fixed, also removed the ProxyClass
2. Because that makes it impossible for another subscriber to do anything with the Response in between render strategies and replacing placeholders. Splitting that up is the best way to ensure that someone can hook in in between.
3. This does not need to be moved, but it _should_ be moved, because else the rendering of the placeholders is technically still within the stack frame, even though it does not affect the stack. This just cleans it up, to ensure that rendering placeholders _always_ is taking place outside of non-applied data on the stack.
4. Same as 3. - It is pure code cleanup, that I would like to see here to ensure that the high level flow is always: render element, bubble stack, render placeholders
5. It is still replacing placeholders for isRoot call at this moment, but it could then be moved easier to renderRoot instead.
6. Yes, but I think PlaceholderStrategyInterface or PlaceholderRenderStrategyInterface is a little long. Symfony also has ESI render strategies and implement those via placeholders (PHP code in ESI tag) in a way, so I vote to just keep RenderStrategy.
7. Yes and an integration test.
8. Nice! It was not identical before the move to the subscriber. Now it is, so yes it can be removed now.
1. Fixed
2. Okay, changed to getRequiredCacheContexts().
New patch soon.
Comment #22
Wim Leers2. Ok! (I can't think of anything needed in between, but that sure is a good general best practice.)
3+4. Can we then add test coverage for this?
6. Okay. But then the docs should specifically say "placeholder render strategies".
8. Ok :)
2. Ok :)
Comment #23
Wim LeersReflecting the current state of this issue.
Comment #24
Wim LeersI moved the parts of the SmartCache patch that blocked this issue (since #15, see that comment) to #2551989: Move replacing of placeholders from HtmlRenderer to HtmlResponseAttachmentsProcessor, which just got committed. So this is now fully unblocked!
Comment #25
Fabianx CreditAttribution: Fabianx for Acquia commented#22: 3+4 - I tried but it was not easily possible as it is not bug due to the strong guarantees that executeInRenderContext() provides.
New patch based on committed #24 work.
Also added the unit tests for the RenderStrategyManager.
Note:
- Behavior if someone removed all RenderStrategies or had only one render strategy that did not process any placeholders is to throw away those placeholder.
Not sure that is what we want, but at least it is tested.
We have two possible ways for the edge case behavior:
If there are any placeholders left after processing all strategies:
a) like currently - discard any remaining placeholders (though that should never happen as SingleFlushRenderStrategy comes last)
b) Or just add them to the returned array, but that would make the SingleFlushRenderStrategy redundant.
- Behavior if there is not a single render strategy registered is throwing a notice due to NULL not being an array.
We could bail out early for that case and just return the placeholders as is?
Thoughts?
Comment #26
Wim Leers3 + 4: thanks, I now see why you did it this way. In fact, your previous reply to my questions was crystal clear. I don't know why I didn't get that before.
The first doesn't mention "HTML", the second does.
Reading this patch again, it still feels like it'd be clearer if this were called "placeholder render strategies" and not "render strategies".
Because "render strategies" implies a significantly broader level of impact, but we really are just talking about placeholder render strategies.
So then this would be called
PlaceholderRenderStrategyInterface
. Or even justPlaceholderStrategyInterface
. If "render strategy" is okay, then surely "placeholder strategy" is too?This should stress that the different render strategies are called in a specific order. If that order were not strictly defined, this functionality would work in an undeterministic way.
Why is it the last? Because its priority is -1000.
So this comment should rather say something like "this is designed to be the fallback strategy, so should have the lowest priority".
Comment #27
moshe weitzman CreditAttribution: moshe weitzman at Acquia commentedComment #28
Fabianx CreditAttribution: Fabianx for Acquia commented#26: We can definitely discuss the naming, but what I really need answers on is #25, the Note: part.
Comment #29
Wim Leers#28: oh, ok!
Having only one placeholder render strategy does not seem to be a problem at all? Especially considering that the SingleFlush strategy is guaranteed to be always available.
So: seems to be the answer already.
Again, the SingleFlush strategy is guaranteed to be always available. So I don't see how/when/why this can be a real-world problem.
Comment #30
Fabianx CreditAttribution: Fabianx for Acquia commented- Renamed all the things to Placeholder* as discussed in IRC. (interdiff-rename.txt)
- Addressed all feedback.
- Fixed little edge case - should not happen, but check does at least define consistent behavior.
Comment #31
Fabianx CreditAttribution: Fabianx for Acquia commentedComment #32
Wim LeersPoor llama :D
(Also: strange llama :P)
Fixed lots of nits:
s/HtmlResponse responses/HTML responses/
This needs quite a bit of clean-up.
Unused.
Missing leading slash.
What's not clear from the member's documentation nor the method's documentation is that it's the service priority that determines the order.
Missing FQCN.
And these still need to be addressed:
These are effectively equivalent.
On the one hand, it is impossible to not have the Single Flush strategy, so the
if (empty()
always evaluates to false.On the other hand, if that evaluated to true (because some module modified the container), then it'd effectively still be the same as the single flush strategy.
Therefore I think we should have either, but not both. I'll leave it to you on how you want to solve this.
Let's add an assertion here that ensures that
array_keys($processed_placeholders)
is a subset ofarray_keys($placeholders)
. We want to prevent additional placeholders being added, or existing placeholders being changed, because that could break things.AFAICT nothing in this patch calls this, so there's no need to do this here. Let's do that in a separate issue. Even more so because it needs to update
RendererInterface
too.That keeps the scope of this issue much clearer.
Then these changes could also be done in that separate issue.
Let's assert this is not ever called.
Comment #33
Fabianx CreditAttribution: Fabianx for Acquia commentedThanks so much Wim for fixing the nits.
Assertions are pure joy to use! :)
#32:
1. Changed to use assert() as well.
2. Done
3. / 4. Removed
5. Done
Comment #34
Wim LeersThis looks great now, and very simple. The latest patch makes it very clear this is a pure, simple, thin API addition. Let's also update the IS to bring it in sync with the latest patch. Then I think this is ready :)
Let's just be more explicit and state that we always expect the
\Drupal\Core\Render\Placeholder\SingleFlushStrategy
strategy to be present.Also: s/need to/must/
s/need to/must/
Thanks!
Comment #35
Wim LeersLet's make it clear that this is a contributed project blocker: it blocks BigPipe/ESI/… modules in D8 contrib. With this change, they'll be able to work automatically: Drupal 8 core already has the concept of placeholders, they'd just effectively take over placeholder rendering from Drupal core. This patch makes that possible.
It therefore unblocks #2469431: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts — which at this point is unlikely to happen for D8 core, but with this simple, thin patch, it can actually be done in D8 contrib, so that it can mature there first before moving into D8 core at a later time (think 8.1.0, 8.2.0 …).
Comment #36
Fabianx CreditAttribution: Fabianx for Acquia commentedUpdated IS and added beta evaluation.
Fixed #34.
Comment #37
Fabianx CreditAttribution: Fabianx for Acquia commentedThe right interdiff for #36.
Comment #38
Wim LeersI think this is RTBC-worthy. I'll ask @moshe to also take a look at this.
Just one more thing:
I think this could use a paragraph, perhaps two, explaining what exactly this does. (Part of what #35's first paragraph says, plus some of the technical details.)
Then we have a place where we document *in* code how this relates to Renderer, HtmlResponse, HtmlResponseAttachmentProcessor, etc.
Comment #39
dawehnerNitpick alarm: two spaces :P
I'm curious whether we can come up with a better name than "Manager". Manager pretty much tells you nothing about it
It would be nice to define what single flush means
For future reference, you can name these entries by providing an array key
Comment #40
Fabianx CreditAttribution: Fabianx for Acquia commentedAdressed all feedback of #38 and #39:
#38: Thanks, tried my best to show some examples on how this can be used by contrib. It will be important for core, too though.
As else auto-placeholdering after rendered data will be very difficult to coordinate.
e.g. if we statically cache how a placeholder is replaced, then core can do so on its own, but with render strategies we can be way way smarter:
Consider the following chain:
1. EsiStrategy
2. StaticCachingStrategy
3. CachedStrategy
4. BigPipeStrategy
5. SingleFlushStrategy
1. ESI cannot make use of static caching, so we probably want to always ESI those things, regardless of if its cached or not currently as we want to cache in Varnish / at the edge.
2. The static caching strategy replaces everything that we auto-placeholdered after rendering, because as we have rendered that already, why should we re-create it again?
3. A (contrib) or core strategy - especially useful in combination with BigPipe: If the data is already cached, why would we BigPipe it?
4. BigPipe all that say it can be BigPipe'd (or rather that say that it cannot be NOT BigPipe'd)
5. All the rest
#39:
1. Fixed, also added some explanation per #38 on what this does.
2. It manages placeholder strategies - I can't think of a better name.
3. Added some explanation.
4. Neat! Fixed!
Comment #41
effulgentsia CreditAttribution: effulgentsia at Acquia commentedPatch looks great to me. Just a couple tiny nits:
Do we need this? Isn't $response still the same object (just mutated) that's already in the event?
This test function now does a lot more than just testing single flush, so should be renamed, right?
Besides the patch, I'm curious about this claim in the beta eval:
How does this patch block BigPipe? Can't this entire patch be just as effectively implemented in a contrib module (e.g., placeholder_strategy_manager.module)? Not saying we need to punt this to 8.1, but should we? Would there be benefit to letting this evolve a bit in contrib first, in case that once BigPipe, ESI, etc. modules start using it, things get discovered that would be harder to change in core?
Comment #42
kim.pepperRe: #39.2 > I'm curious whether we can come up with a better name than "Manager". Manager pretty much tells you nothing about it
How about PlaceholderStrategyDelegator?
Comment #43
Wim LeersTwitter seems to have concluded "chain": https://twitter.com/da_wehner/status/638776059664728064. Chain makes more sense than "delegator" (or "manager"), because it is implemented as a chain.
NW for #41, #42 and this comment.
Comment #44
Crell CreditAttribution: Crell at Palantir.net commentedCore is rather inconsistent on the naming here, but if we want to avoid "Manager" (we do), then "ChainedPlaceholderStrategy" is probably the next logical name. Or, really, it should be a verb so ChainedPlaceholderProcessor?
That said, I'm curious of the point of this patch. It appears to allow multiple placeholder replacement systems to run concurrently, so that a given request could have some inline, some bigpipe, and some ESI placeholders all in a single response. I am struggling to see where that is useful.
Comment #45
Fabianx CreditAttribution: Fabianx for Acquia commentedWill work on the feedback, thanks for the twitter thread!
#44: See #40 for an example.
The real nice thing of e.g. combining BigPipe with a Cached Strategy is that core is "auto-improving" its performance over time.
So e.g. something can be BigPipe'd only when its not (micro)-cached already.
Also as placeholders are nicely named and can usually be identified by either #cache keys or #lazy_builder easily, it is very simple to replace something with a custom strategy and that is where this especially shines; e.g. also possibilities to integrate with custom javascript + cookie, NodeJS, Angular, React, etc.
That is also why I think it would be great to have this in core, so that the coordinator / chained placeholder processor is something that core and contrib can rely on.
Comment #46
yched CreditAttribution: yched commentedIf so, do we want to make that logic live :
- in "just a regular Strategy that happens to call the other strategies that registered using the right service tag" (i.e ChainedPlaceholderStrategy as @Crell suggest in #44),
- or in something that is not a Strategy itself and lives "above" strategies (the current "Manager" - could be PlaceholderResolver if we don't like Manager ?)
?
Comment #47
Fabianx CreditAttribution: Fabianx for Acquia commentedAddressed all feedback!
- ChainedPlaceholderStrategy it is!
Lets see how the attached patch flies, I updated the IS to show some more motivation why I think this belongs in core rather than contrib.
EDIT:
X-POST with #46 - So ChainedPlaceholderStrategy felt better with just injecting a Strategy.
Comment #48
Wim LeersThis patch now looks beautiful and is wonderfully simple and elegant. Great job :)
This makes a lot of sense! :) Much better, much more elegant than before :)
Nit: Renders placeholders using a chain of placeholder strategies.
Comment #49
Fabianx CreditAttribution: Fabianx for Acquia commentedComment #50
Crell CreditAttribution: Crell as a volunteer commentedThere's no need to call this .chained. The site's placeholder strategy is what's registered at placeholder_strategy and has the PlaceholderInterface. That it by default happens to be a chained implementation is irrelevant to anyone but that class. That's by design.
Our router service is router, not router.chained, even though we're using a chained router there, too. (And in core have only a single implementation of it.)
Yep, that's exactly how it's done elsewhere, as Druplicon intended. :-)
In some cases we also have an extra interface for the add*() method, but I'm not sure if we're doing that everywhere.
Comment #51
dawehnerYeah IMHO this is an antipattern to do that.
Comment #52
Fabianx CreditAttribution: Fabianx for Acquia commentedThanks, #48 and #50 are fixed.
Got that with the @placeholder_strategy now! :)
Comment #53
catchNot a full review. This looks good to me in general. Only one real question:
Does comment permalink rendering still use a subrequest?
:)
Tagging RC target because this is a proper API addition that we'd probably only be able to do in a minor release and at the very latest for 8.0.0 during RC if we decide the disruption is minimal - we need a new tag for this like 'minor release target' or something.
Comment #54
Wim Leers#53.1: Yes, see
\Drupal\comment\Controller\CommentController::commentPermalink()
.Comment #55
catch#54 if it does, I'd expect us to still want placeholdering to work there. Not sure what the implications are, possibly not using a subrequest would be best.
Comment #56
Wim Leers#55: No.
HtmlResponseSubscriber
is what does placeholder replacement in HEAD, and it also doesn't run for subrequests. And everything is working fine. The subrequest is only happening to retrieve the actual response to send, to do in-app redirection (rather than sending a redirect response), but it is then "promoted" to become the master response. Therefore using the same pattern here is totally fine. :)EDIT: I forgot to mention that I stepped through both HEAD and the patch to make sure that what I wrote above is actually the case.
Comment #57
catchOK this is what I couldn't remember properly at all. So isMasterRequest() returns TRUE even though the comment code uses a subrequest. Not confusing at all :P
But not at all the fault of this patch, slightly my fault because the subrequest for comments is ported from one of the very first core patches I worked on ;)
Comment #58
andypost@catch suppose you mean related #2113323: Rename Comment::permalink() to not be ambiguous with ::uri()
Comment #60
Wim LeersTestbotfail:
Comment #62
catchI read through again and didn't find anything noteworthy.
So.. committed/pushed to 8.0.x, thanks!