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

Reference: https://www.drupal.org/core/beta-changes
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.

CommentFileSizeAuthor
#52 support_placeholder-2349011-52.patch14.64 KBFabianx
#52 interdiff.txt1.24 KBFabianx
#47 support_placeholder-2349011-46.patch14.67 KBFabianx
#47 interdiff.txt7.23 KBFabianx
#40 implement_placeholder-2349011-39.patch14.78 KBFabianx
#40 interdiff.txt4.46 KBFabianx
#37 interdiff.txt2.95 KBFabianx
#36 implement_placeholder-2349011-36.patch14.17 KBFabianx
#36 interdiff.txt8.83 KBFabianx
#33 implement_placeholder-2349011-33.patch13.98 KBFabianx
#33 interdiff.txt8.83 KBFabianx
#32 interdiff.txt4.46 KBWim Leers
#32 implement_placeholder-2349011-32.patch16.16 KBWim Leers
#30 implement_placeholder-2349011-28.patch15.32 KBFabianx
#30 interdiff.txt2.59 KBFabianx
#30 interdiff-rename.txt12.75 KBFabianx
#25 implement_placeholder-2349011-25.patch14.33 KBFabianx
#25 interdiff.txt15.82 KBFabianx
#19 implement_placeholder-2349011-19.patch22.48 KBFabianx
#19 interdiff.txt3.46 KBFabianx
#17 implement_placeholder-2349011-17.patch19.83 KBFabianx
#17 interdiff.txt3.02 KBFabianx
#15 implement_placeholder-2349011-15.patch16.82 KBFabianx
#15 interdiff.txt13.47 KBFabianx
#11 implement_placeholder-2349011-11.patch12.86 KBFabianx
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Fabianx’s picture

Wim Leers’s picture

Wim Leers’s picture

Fabianx’s picture

Status: Closed (duplicate) » Active

Not really, it is a sub-task that can go independently, will provide a patch today or tomorrow.

Wim Leers’s picture

Oh, 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. render strategies? If so, let's make it a child of that one, and link to this from that issue's IS.

Wim Leers’s picture

Title: Implement placeholder based rendering strategies to support ESI, AJAX, BIG PIPE, BOOT, etc. like in render_cache » Implement placeholder based-rendering strategies to support SingleFlush, BigPipeESI, AJAX…
Component: base system » render system

Clarifying title. And adding to the IS of [#2469431.

Wim Leers’s picture

Title: Implement placeholder based-rendering strategies to support SingleFlush, BigPipeESI, AJAX… » Implement placeholder based-rendering strategies to support SingleFlush, BigPipe, ESI…
Wim Leers’s picture

The IS definitely needs an overhaul.

Fabianx’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update +Needs tests
Fabianx’s picture

Status: Active » Needs review
FileSize
12.86 KB

Thanks 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.

Wim Leers’s picture

First: glad to hear that :)

Second: wow, you weren't kidding -- this patch is ridiculously simple!

Here's an initial code review.

  1. +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
    @@ -124,11 +133,13 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
    -    // @todo https://www.drupal.org/node/2495001 Make renderRoot return a
    -    //       cacheable render array directly.
    -    $this->renderer->renderRoot($html);
    +    // This uses render() instead of renderRoot() to ensure that placeholders
    +    // are not replaced.
    +    $this->renderer->render($html);
    

    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.

  2. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -118,10 +118,8 @@ public function renderPlain(&$elements) {
    @@ -211,12 +209,6 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    
    @@ -211,12 +209,6 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
           $cached_element = $this->renderCache->get($elements);
           if ($cached_element !== FALSE) {
             $elements = $cached_element;
    -        // Only when we're in a root (non-recursive) Renderer::render() call,
    -        // placeholders must be processed, to prevent breaking the render cache
    -        // in case of nested elements with #cache set.
    -        if ($is_root_call) {
    -          $this->replacePlaceholders($elements);
    -        }
             // Mark the element markup as safe. If we have cached children, we need
             // to mark them as safe too. The parent markup contains the child
             // markup, so if the parent markup is safe, then the markup of the
    @@ -233,6 +225,14 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    
    @@ -233,6 +225,14 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
             // Render cache hit, so rendering is finished, all necessary info
             // collected!
             $this->bubbleStack();
    +
    +        // Only when we're in a root (non-recursive) Renderer::render() call,
    +        // placeholders must be processed, to prevent breaking the render cache
    +        // in case of nested elements with #cache set.
    +        if ($is_root_call) {
    +          $this->replacePlaceholders($elements);
    +        }
    +
             return $elements['#markup'];
           }
         }
    

    Can you explain why this needed to be moved?

  3. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -470,6 +470,15 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    if ($is_root_call) {
    +      if (static::$stack->count() !== 1) {
    +        throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
    +      }
    +    }
    +
    +    // Rendering is finished, all necessary info collected!
    +    $this->bubbleStack();
    +
         // Only when we're in a root (non-recursive) Renderer::render() call,
         // placeholders must be processed, to prevent breaking the render cache in
         // case of nested elements with #cache set.
    @@ -481,14 +490,8 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    
    @@ -481,14 +490,8 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
         // that is handled earlier in Renderer::render().
         if ($is_root_call) {
           $this->replacePlaceholders($elements);
    -      if (static::$stack->count() !== 1) {
    -        throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
    -      }
         }
     
    -    // Rendering is finished, all necessary info collected!
    -    $this->bubbleStack();
    

    This I don't understand at first sight either. Again a bit of the background info/reasoning would be welcome :)

  4. +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyInterface.php
    @@ -0,0 +1,27 @@
    +   * Processes placeholders to render them with different strategies.
    
    +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManager.php
    @@ -0,0 +1,84 @@
    + * Provides a class which allows to render placeholders.
    

    "process" vs. "render"

    Let's be consistent.

  5. +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyInterface.php
    @@ -0,0 +1,27 @@
    +   *   The placeholders to process keyed by $placeholder_id with the render
    +   *   cache array as value.
    
    +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManagerInterface.php
    @@ -0,0 +1,26 @@
    +   *   The render cache array containing #markup and #attached placeholders.
    

    s/render cache array/placeholder render array/

    (Or whatever the exact name is that HEAD uses. In any case *not* "render cache array".)

  6. +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyInterface.php
    @@ -0,0 +1,27 @@
    +   *   The new values for the placeholders, keyed by $placeholder_id => $render_array.
    

    80 cols.

  7. +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManager.php
    @@ -0,0 +1,84 @@
    +  /**
    +   * The RenderStrategy services.
    +   *
    +   * @var Drupal\Core\Render\Strategy\RenderStrategyInterface[]
    +   */
    +  protected $renderStrategies;
    ...
    +  public function addRenderStrategy(RenderStrategyInterface $strategy) {
    +    $this->renderStrategies[] = $strategy;
    +  }
    

    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.

  8. +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManager.php
    @@ -0,0 +1,84 @@
    +   *   The strategy to add to the render strategies.
    

    The render strategy to add.

  9. +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManager.php
    @@ -0,0 +1,84 @@
    +  public function renderPlaceholders(&$elements) {
    

    array

  10. +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManager.php
    @@ -0,0 +1,84 @@
    +    // Now render the new placeholders.
    

    s/new/processed/

  11. +++ b/core/lib/Drupal/Core/Render/Strategy/SingleFlushRenderStrategy.php
    @@ -0,0 +1,24 @@
    +  }
    +}
    

    Missing newline in between.

Wim Leers’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -124,11 +133,13 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
+    // This uses render() instead of renderRoot() to ensure that placeholders
+    // are not replaced.
+    $this->renderer->render($html);
     $content = $this->renderCache->getCacheableRenderArray($html);
 
+    $this->renderStrategyManager->renderPlaceholders($content);

This 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 in executeInRenderContext().

Then the $this->renderStrategyManager->renderPlaceholders() call can still happen as it is right now.

Wim Leers’s picture

I 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.

  1. The patch makes small internal changes in Renderer, none of which cause API changes.
  2. The patch makes a small internal change in HtmlRenderer, none of which cause API changes.
  3. Everything else in the patch are pure additions.

Therefore, AFAICT, this patch does not cause any API changes, which means it can definitely remain major.

Fabianx’s picture

Assigned: Unassigned » Fabianx
Status: Needs work » Needs review
FileSize
13.47 KB
16.82 KB

Re-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.

Status: Needs review » Needs work

The last submitted patch, 15: implement_placeholder-2349011-15.patch, failed testing.

Fabianx’s picture

Status: Needs work » Needs review
FileSize
3.02 KB
19.83 KB

It helps to add all used proxies :)

Status: Needs review » Needs work

The last submitted patch, 17: implement_placeholder-2349011-17.patch, failed testing.

Fabianx’s picture

Status: Needs work » Needs review
FileSize
3.46 KB
22.48 KB

Thanks 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.

Wim Leers’s picture

Patch review:

  1. +++ b/core/core.services.yml
    @@ -1479,6 +1479,22 @@ services:
    +    lazy: true
    

    Why lazy? Almost every page contains at least one placeholder: that for the status messages. So we don't actually gain anything AFAICT.

  2. +++ b/core/lib/Drupal/Core/EventSubscriber/HtmlRenderStrategySubscriber.php
    @@ -0,0 +1,74 @@
    +class HtmlRenderStrategySubscriber implements EventSubscriberInterface {
    

    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)?

  3. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -272,12 +272,6 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    -        // Only when we're in a root (non-recursive) Renderer::render() call,
    -        // placeholders must be processed, to prevent breaking the render cache
    -        // in case of nested elements with #cache set.
    -        if ($is_root_call) {
    -          $this->replacePlaceholders($elements);
    -        }
             // Mark the element markup as safe if is it a string.
             if (is_string($elements['#markup'])) {
               $elements['#markup'] = SafeString::create($elements['#markup']);
    @@ -288,6 +282,14 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    
    @@ -288,6 +282,14 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
             // Render cache hit, so rendering is finished, all necessary info
             // collected!
             $context->bubble();
    +
    +        // Only when we're in a root (non-recursive) Renderer::render() call,
    +        // placeholders must be processed, to prevent breaking the render cache
    +        // in case of nested elements with #cache set.
    +        if ($is_root_call) {
    +          $this->replacePlaceholders($elements);
    +        }
    

    Why does this need to be moved?

  4. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -525,6 +527,16 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    if ($is_root_call) {
    +      // @todo remove as part of https://www.drupal.org/node/2511330.
    +      if ($context->count() !== 1) {
    +        throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
    +      }
    +    }
    
    @@ -536,15 +548,8 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    -      // @todo remove as part of https://www.drupal.org/node/2511330.
    -      if ($context->count() !== 1) {
    -        throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
    -      }
    

    And why does this need to be moved?

  5. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -525,6 +527,16 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    // Rendering is finished, all necessary info collected!
    +    $context->bubble();
    
    @@ -536,15 +548,8 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    -    // Rendering is finished, all necessary info collected!
    -    $context->bubble();
    

    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?

  6. +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyInterface.php
    @@ -0,0 +1,27 @@
    +interface RenderStrategyInterface {
    ...
    +  public function processPlaceholders(array $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?

  7. +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManager.php
    @@ -0,0 +1,76 @@
    +  public function processPlaceholders(array $placeholders) {
    +    if (empty($placeholders)) {
    +      return [];
    +    }
    +
    +    $new_placeholders = [];
    +
    +    // Give each render strategy a chance to replace all not-yet replaced
    +    // placeholders.
    +    foreach ($this->renderStrategies as $strategy) {
    +      $processed_placeholders = $strategy->processPlaceholders($placeholders);
    +      $placeholders = array_diff_key($placeholders, $processed_placeholders);
    +      $new_placeholders += $processed_placeholders;
    +
    +      if (empty($placeholders)) {
    +        break;
    +      }
    +    }
    +
    +    return $new_placeholders;
    +  }
    

    This needs a unit test.

  8. +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManagerInterface.php
    @@ -0,0 +1,26 @@
    +interface RenderStrategyManagerInterface {
    

    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 by PathProcessorManager. In other words: the manager is just a special aggregating strategy, rather than a separate thing.


Interdiff review:

  1. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -255,6 +255,8 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +      // This is inlined for performance reasons. Also see
    +      // applyRequiredCacheContexts() function below.
    

    s/Also see FUNC function below/@see FUNC/

  2. +++ b/core/lib/Drupal/Core/Render/RendererInterface.php
    @@ -365,6 +365,21 @@ public function hasRenderContext();
    +   * Applies the required cache contexts to a render array.
    +   *
    +   * Note: This function should only be needed in edge-case circumstances; e.g.
    +   * when interacting directly with the RenderCache class or by rendering
    +   * without replacing placeholders.
    

    This strange documentation is precisely why I'd argue we should have ::getRequiredCacheContexts() instead. This would then be usable by any code, regardless of the RendererInterface 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

Fabianx’s picture

#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.

Wim Leers’s picture

2. 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 :)

Wim Leers’s picture

Status: Needs review » Needs work

Reflecting the current state of this issue.

Wim Leers’s picture

I 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!

Fabianx’s picture

Status: Needs work » Needs review
FileSize
15.82 KB
14.33 KB

#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?

Wim Leers’s picture

Status: Needs review » Needs work

3 + 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.


  1. +++ b/core/core.services.yml
    @@ -1507,6 +1507,20 @@ services:
    +  # Render strategies for rendering placeholders.
    
    +++ b/core/lib/Drupal/Core/EventSubscriber/HtmlRenderStrategySubscriber.php
    @@ -0,0 +1,74 @@
    + * HTML Response subscriber to handle placeholder render strategies.
    

    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.

  2. +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyInterface.php
    @@ -0,0 +1,27 @@
    +interface RenderStrategyInterface {
    

    So then this would be called PlaceholderRenderStrategyInterface. Or even just PlaceholderStrategyInterface. If "render strategy" is okay, then surely "placeholder strategy" is too?

  3. +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManager.php
    @@ -0,0 +1,59 @@
    +    // Give each render strategy a chance to replace all not-yet replaced
    +    // placeholders.
    

    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.

  4. +++ b/core/lib/Drupal/Core/Render/Strategy/SingleFlushRenderStrategy.php
    @@ -0,0 +1,24 @@
    + * This is the last strategy that always replaces all remaining placeholders.
    

    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".

moshe weitzman’s picture

Issue tags: +sprint
Fabianx’s picture

#26: We can definitely discuss the naming, but what I really need answers on is #25, the Note: part.

Wim Leers’s picture

#28: oh, ok!

Behavior if someone removed all RenderStrategies or had only one render strategy that did not process any placeholders is to throw away those placeholder

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: a) like currently - discard any remaining placeholders (though that should never happen as SingleFlushRenderStrategy comes last) seems to be the answer already.

- Behavior if there is not a single render strategy registered is throwing a notice due to NULL not being an array.

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.

Fabianx’s picture

- 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.

Fabianx’s picture

Status: Needs work » Needs review
Wim Leers’s picture

+++ b/core/tests/Drupal/Tests/Core/Render/Placeholder/PlaceholderStrategyManagerTest.php
@@ -43,6 +43,12 @@ public function providerProcessPlaceholders() {
+      'ignore-me' => ['#markup' => 'I-am-a-llama-that-will-be-ignored-by-the-placeholder-strategy-manager.'],

Poor llama :D

(Also: strange llama :P)


Fixed lots of nits:

  1. +++ b/core/lib/Drupal/Core/EventSubscriber/HtmlResponsePlaceholderStrategySubscriber.php
    @@ -0,0 +1,74 @@
    +   * Processes placeholders for HtmlResponse responses.
    

    s/HtmlResponse responses/HTML responses/

  2. +++ b/core/lib/Drupal/Core/Render/Placeholder/PlaceholderStrategyInterface.php
    @@ -0,0 +1,27 @@
    +  /**
    +   * Processes placeholders to render them with different strategies.
    +   *
    +   * @param array $placeholders
    +   *   The placeholders to process keyed by $placeholder_id with the render
    +   *   cache array as value.
    +   *
    +   * @return array $placeholders
    +   *   The new values for the placeholders, keyed by $placeholder_id => $render_array.
    +   */
    

    This needs quite a bit of clean-up.

  3. +++ b/core/lib/Drupal/Core/Render/Placeholder/PlaceholderStrategyManager.php
    @@ -0,0 +1,65 @@
    +use Drupal\Core\Render\RendererInterface;
    

    Unused.

  4. +++ b/core/lib/Drupal/Core/Render/Placeholder/PlaceholderStrategyManager.php
    @@ -0,0 +1,65 @@
    +   * @var Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface[]
    

    Missing leading slash.

  5. +++ b/core/lib/Drupal/Core/Render/Placeholder/PlaceholderStrategyManager.php
    @@ -0,0 +1,65 @@
    +  protected $placeholderStrategies;
    ...
    +  public function addPlaceholderStrategy(PlaceholderStrategyInterface $strategy) {
    ...
    +    // placeholders. The order of placeholder strategies is well defined
    +    // and this uses a variation of the "chain of responsibility" design pattern.
    

    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.

  6. +++ b/core/lib/Drupal/Core/Render/Placeholder/PlaceholderStrategyManager.php
    @@ -0,0 +1,65 @@
    +   * @param PlaceholderStrategyInterface $strategy
    

    Missing FQCN.


And these still need to be addressed:

  1. +++ b/core/lib/Drupal/Core/Render/Placeholder/PlaceholderStrategyManager.php
    @@ -0,0 +1,65 @@
    +    // In case there are no placeholder strategies, return all placeholders.
    +    if (empty($this->placeholderStrategies)) {
    +      return $placeholders;
    +    }
    
    +++ b/core/lib/Drupal/Core/Render/Placeholder/SingleFlushStrategy.php
    @@ -0,0 +1,25 @@
    +class SingleFlushStrategy implements PlaceholderStrategyInterface {
    +
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function processPlaceholders(array $placeholders) {
    +    // Return all placeholders as is; they should be rendered directly.
    +    return $placeholders;
    +  }
    +}
    

    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.

  2. +++ b/core/lib/Drupal/Core/Render/Placeholder/PlaceholderStrategyManager.php
    @@ -0,0 +1,65 @@
    +      $processed_placeholders = $strategy->processPlaceholders($placeholders);
    +      $placeholders = array_diff_key($placeholders, $processed_placeholders);
    

    Let's add an assertion here that ensures that array_keys($processed_placeholders) is a subset of array_keys($placeholders). We want to prevent additional placeholders being added, or existing placeholders being changed, because that could break things.

  3. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -163,10 +163,8 @@ public function renderPlain(&$elements) {
    -  protected function renderPlaceholder($placeholder, array $elements) {
    +  public function renderPlaceholder($placeholder, array $elements) {
    

    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.

  4. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -275,12 +273,6 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    -        if ($is_root_call) {
    -          $this->replacePlaceholders($elements);
    -        }
    
    @@ -291,6 +283,14 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +        if ($is_root_call) {
    +          $this->replacePlaceholders($elements);
    +        }
    

    Then these changes could also be done in that separate issue.

  5. +++ b/core/tests/Drupal/Tests/Core/Render/Placeholder/PlaceholderStrategyManagerTest.php
    @@ -0,0 +1,129 @@
    +    $prophecy = $this->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
    +    $single_flush_strategy = $prophecy->reveal();
    

    Let's assert this is not ever called.

Fabianx’s picture

Thanks 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

Wim Leers’s picture

Title: Implement placeholder based-rendering strategies to support SingleFlush, BigPipe, ESI… » Support placeholder render strategies so contrib can support BigPipe, ESI…
Issue tags: -Needs tests +Needs issue summary update

This 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 :)

  1. +++ b/core/lib/Drupal/Core/Render/Placeholder/PlaceholderStrategyManager.php
    @@ -39,10 +39,8 @@ public function processPlaceholders(array $placeholders) {
    +    assert('!empty($this->placeholderStrategies)', 'At least one placeholder strategy needs to be present.');
    

    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/

  2. +++ b/core/lib/Drupal/Core/Render/Placeholder/PlaceholderStrategyManager.php
    @@ -51,6 +49,7 @@ public function processPlaceholders(array $placeholders) {
    +      assert('array_intersect_key($processed_placeholders, $placeholders) === $processed_placeholders', 'Processed placeholders need to be a subset of all placeholders.');
    

    s/need to/must/

  3. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -164,8 +164,10 @@ public function renderPlain(&$elements) {
    +   * @todo Make public as part of https://www.drupal.org/node/2469431
    
    @@ -554,8 +544,15 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +      // @todo remove as part of https://www.drupal.org/node/2511330.
    

    Thanks!

Wim Leers’s picture

Let'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 …).

Fabianx’s picture

Updated IS and added beta evaluation.

Fixed #34.

Fabianx’s picture

FileSize
2.95 KB

The right interdiff for #36.

Wim Leers’s picture

I think this is RTBC-worthy. I'll ask @moshe to also take a look at this.


Just one more thing:

+++ b/core/lib/Drupal/Core/EventSubscriber/HtmlResponsePlaceholderStrategySubscriber.php
@@ -0,0 +1,74 @@
+/**
+ * HTML response subscriber to allow for different  placeholder strategies.
+ */
+class HtmlResponsePlaceholderStrategySubscriber implements EventSubscriberInterface {

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.

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/EventSubscriber/HtmlResponsePlaceholderStrategySubscriber.php
    @@ -0,0 +1,74 @@
    + * HTML response subscriber to allow for different  placeholder strategies.
    

    Nitpick alarm: two spaces :P

  2. +++ b/core/lib/Drupal/Core/Render/Placeholder/PlaceholderStrategyManager.php
    @@ -0,0 +1,64 @@
    +class PlaceholderStrategyManager implements PlaceholderStrategyInterface {
    

    I'm curious whether we can come up with a better name than "Manager". Manager pretty much tells you nothing about it

  3. +++ b/core/lib/Drupal/Core/Render/Placeholder/SingleFlushStrategy.php
    @@ -0,0 +1,25 @@
    +/**
    + * Defines the 'single_flush' placeholder strategy.
    + *
    

    It would be nice to define what single flush means

  4. +++ b/core/tests/Drupal/Tests/Core/Render/Placeholder/PlaceholderStrategyManagerTest.php
    @@ -0,0 +1,169 @@
    +    $data = [];
    ...
    +    $data[] = [[], [], []];
    ...
    +    $data[] = [[$dev_null_strategy], $placeholders, []];
    ...
    +    $data[] = [[$single_flush_strategy], $placeholders, $placeholders];
    ...
    +    $data[] = [[$esi_strategy], $placeholders, $result];
    ...
    +    $data[] = [[$esi_strategy, $single_flush_strategy], $placeholders, $result];
    ...
    +    $data[] = [[$esi_strategy, $single_flush_strategy], $placeholders, $result];
    

    For future reference, you can name these entries by providing an array key

Fabianx’s picture

Adressed 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!

effulgentsia’s picture

Patch looks great to me. Just a couple tiny nits:

  1. +++ b/core/lib/Drupal/Core/EventSubscriber/HtmlResponsePlaceholderStrategySubscriber.php
    @@ -0,0 +1,80 @@
    +    $event->setResponse($response);
    

    Do we need this? Isn't $response still the same object (just mutated) that's already in the event?

  2. +++ b/core/tests/Drupal/Tests/Core/Render/Placeholder/PlaceholderStrategyManagerTest.php
    @@ -0,0 +1,169 @@
    +  public function testProcessPlaceholdersSingleFlush($strategies, $placeholders, $result) {
    

    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:

Major because it blocks BigPipe and other things that can make Drupal 8 way faster.

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?

kim.pepper’s picture

Re: #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?

Wim Leers’s picture

Status: Needs review » Needs work

I'm curious whether we can come up with a better name than "Manager". Manager pretty much tells you nothing about it

Twitter 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.

Crell’s picture

+++ b/core/lib/Drupal/Core/Render/Placeholder/PlaceholderStrategyManager.php
@@ -0,0 +1,64 @@
+/**
+ * Renders placeholders using different strategies depending on priorities.
+ */
+class PlaceholderStrategyManager implements PlaceholderStrategyInterface {
+

Core 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.

Fabianx’s picture

Will 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.

yched’s picture

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.

If 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 ?)
?

Fabianx’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
7.23 KB
14.67 KB

Addressed 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.

Wim Leers’s picture

This patch now looks beautiful and is wonderfully simple and elegant. Great job :)

  1. +++ b/core/core.services.yml
    @@ -1503,6 +1503,20 @@ services:
    +  placeholder_strategy.chained:
    +    class: Drupal\Core\Render\Placeholder\ChainedPlaceholderStrategy
    +    tags:
    +      - { name: service_collector, tag: placeholder_strategy, call: addPlaceholderStrategy }
    +  placeholder_strategy.single_flush:
    +    class: Drupal\Core\Render\Placeholder\SingleFlushStrategy
    +    tags:
    +      - { name: placeholder_strategy, priority: -1000 }
    

    This makes a lot of sense! :) Much better, much more elegant than before :)

  2. +++ b/core/lib/Drupal/Core/Render/Placeholder/ChainedPlaceholderStrategy.php
    @@ -0,0 +1,64 @@
    + * Renders placeholders using different strategies depending on priorities.
    

    Nit: Renders placeholders using a chain of placeholder strategies.

Fabianx’s picture

Issue summary: View changes
Crell’s picture

  1. +++ b/core/core.services.yml
    @@ -1503,6 +1503,20 @@ services:
    +  placeholder_strategy.chained:
    +    class: Drupal\Core\Render\Placeholder\ChainedPlaceholderStrategy
    +    tags:
    +      - { name: service_collector, tag: placeholder_strategy, call: addPlaceholderStrategy }
    

    There'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.)

  2. +++ b/core/lib/Drupal/Core/Render/Placeholder/ChainedPlaceholderStrategy.php
    @@ -0,0 +1,64 @@
    +class ChainedPlaceholderStrategy implements PlaceholderStrategyInterface {
    

    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.

dawehner’s picture

In some cases we also have an extra interface for the add*() method, but I'm not sure if we're doing that everywhere.

Yeah IMHO this is an antipattern to do that.

Fabianx’s picture

Thanks, #48 and #50 are fixed.

Got that with the @placeholder_strategy now! :)

catch’s picture

Issue tags: +rc target

Not a full review. This looks good to me in general. Only one real question:

  1. +++ b/core/lib/Drupal/Core/EventSubscriber/HtmlResponsePlaceholderStrategySubscriber.php
    @@ -0,0 +1,79 @@
    +    if (!$event->isMasterRequest()) {
    

    Does comment permalink rendering still use a subrequest?

  2. +++ b/core/tests/Drupal/Tests/Core/Render/Placeholder/ChainedPlaceholderStrategyTest.php
    @@ -0,0 +1,168 @@
    +      'remove-me' => ['#markup' => 'I-am-a-llama-that-will-be-removed-sad-face.'],
    

    :)

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.

Wim Leers’s picture

#53.1: Yes, see \Drupal\comment\Controller\CommentController::commentPermalink().

catch’s picture

#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.

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community

#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.

catch’s picture

but it is then "promoted" to become the master response.

OK 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 ;)

andypost’s picture

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 52: support_placeholder-2349011-52.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community

Testbotfail:

Drupal\system\Tests\Update\UpdatePathTestBaseFilledTest	
GET http://ec2-52-88-182-40.us-west-2.compute.amazonaws.com/checkout/update.php/run returned 0 (0 bytes).	
…

catch’s picture

Status: Reviewed & tested by the community » Fixed

I read through again and didn't find anything noteworthy.

So.. committed/pushed to 8.0.x, thanks!

  • catch committed 4489252 on 8.0.x
    Issue #2349011 by Fabianx, Wim Leers: Support placeholder render...

Status: Fixed » Closed (fixed)

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