Problem/Motivation

See #2499157: [meta] Auto-placeholdering. Blocked on #2543332: Auto-placeholdering for #lazy_builder without bubbling.

Proposed resolution

Elements that have:

  1. #lazy_builder
  2. no poor cacheability (the other case was already handled in #2543332: Auto-placeholdering for #lazy_builder without bubbling)

… where the lazy builder returns content that results in the bubbling of poor cacheability (low max-age, high-cardinality cache contexts or high-invalidation frequency cache tags (concrete examples: see below)

Remaining tasks

User interface changes

None.

API changes

None.

Data model changes

None.

CommentFileSizeAuthor
#75 interdiff.txt1.06 KBWim Leers
#75 auto-placeholdering-step2-2543334-75.patch50.65 KBWim Leers
#71 auto-placeholdering-step2-2543334-71.patch50 KBWim Leers
#71 interdiff.txt1.48 KBWim Leers
#71 interdiff-rebase.txt2.09 KBWim Leers
#70 auto_placeholdering_for-2543334-70.patch49.23 KBlauriii
#66 interdiff.txt3.14 KBWim Leers
#66 auto-placeholdering-step2-2543334-66.patch50.05 KBWim Leers
#65 interdiff.txt2.16 KBlauriii
#65 auto_placeholdering_for-2543334-65.patch49.5 KBlauriii
#62 auto-placeholdering-step2-2543334-62.patch47.66 KBWim Leers
#57 interdiff.txt930 bytesWim Leers
#57 auto-placeholdering-step2-2543334-57.patch47.66 KBWim Leers
#54 interdiff.txt5.12 KBWim Leers
#54 auto-placeholdering-step2-2543334-54.patch47.63 KBWim Leers
#53 auto_placeholdering_for-2543334-53.patch61.09 KBFabianx
#53 auto_placeholdering_for-2543334-53--on-top-of-placeholder-strategies.txt46.88 KBFabianx
#53 interdiff.txt6.37 KBFabianx
#44 interdiff.txt15.11 KBWim Leers
#44 auto-placeholdering-step2-2543334-44.patch44.33 KBWim Leers
#42 interdiff.txt948 bytesWim Leers
#42 auto-placeholdering-step2-2543334-42.patch53.33 KBWim Leers
#40 auto-placeholdering-step2-2543334-40.patch53.33 KBWim Leers
#32 interdiff.txt898 bytesWim Leers
#32 auto-placeholdering-step2-2543334-32.patch53.34 KBWim Leers
#30 interdiff.txt3.54 KBWim Leers
#30 auto-placeholdering-step2-2543334-30.patch52.52 KBWim Leers
#29 interdiff.txt1.56 KBWim Leers
#29 auto-placeholdering-step2-2543334-29.patch52.45 KBWim Leers
#28 interdiff.txt6.06 KBWim Leers
#28 auto-placeholdering-step2-2543334-28.patch52.09 KBWim Leers
#27 interdiff.txt21.58 KBWim Leers
#27 auto-placeholdering-step2-2543334-27.patch51.97 KBWim Leers
#20 auto-placeholdering-step2-2543334-20.patch41.15 KBWim Leers
#15 interdiff.txt28.98 KBWim Leers
#15 auto-placeholdering-step2-15.patch89.47 KBWim Leers
#14 interdiff.txt3.73 KBWim Leers
#14 auto-placeholdering-step2-14.patch60.59 KBWim Leers
#13 interdiff.txt9.8 KBWim Leers
#13 auto-placeholdering-step2-13.patch57.27 KBWim Leers
#7 interdiff.txt14.63 KBWim Leers
#7 auto-placeholdering-step2-2543334-7.patch52.18 KBWim Leers
#6 interdiff.txt9.99 KBWim Leers
#6 auto-placeholdering-step2-2543334-6.patch42.57 KBWim Leers
#5 interdiff.txt13.38 KBWim Leers
#5 auto-placeholdering-step2-2543334-5.patch38.8 KBWim Leers
#4 auto-placeholdering-step2-2543334-4.patch27.48 KBWim Leers
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers’s picture

Title: [PP-1] Auto-placeholdering for <code>#lazy_builder</code> with bubbled cache contexts (and max-age=0?) » [PP-1] Auto-placeholdering for #lazy_builder with bubbled cache contexts (and max-age=0?)
Wim Leers’s picture

Title: [PP-1] Auto-placeholdering for #lazy_builder with bubbled cache contexts (and max-age=0?) » Auto-placeholdering for #lazy_builder with bubbled cache contexts (and max-age=0?)
Assigned: Unassigned » Wim Leers
Status: Postponed » Active

#2543332: Auto-placeholdering for #lazy_builder without bubbling just landed, this is now unblocked! Have the patch ready, just need to rebase and clean it up.

Wim Leers’s picture

Issue summary: View changes

Here's an updated IS already. Patch is rebased. Patch is also updated to support high-invalidation frequency cache tags, which was added in #2543332: Auto-placeholdering for #lazy_builder without bubbling, but was missing in the original patch.

Now cleaning up the patch in steps that make sense so that it's much more reviewable.

Wim Leers’s picture

Status: Active » Needs review
FileSize
27.48 KB

Step 1/4: support for auto-placeholdering in case of bubbled cache contexts

Notable changes

  • Implemented at the render cache service level
  • Specifically, implemented by decorating the existing RenderCache class, so that that class can continue to do the basics — so: class PlaceholderingRenderCache extends RenderCache
  • The renderer now also tracks #lazy_builder and #create_placeholder in its $pre_bubbling_elements, instead of only #cache. This is necessary to allow the render cache to correctly determine when it can auto-placeholder something.

Conceptual explanation

Implementing this at the render cache level makes a lot of sense conceptually: you first retrieve something from the render cache, then notice that its cacheability meets auto-placeholdering conditions. (The whole point of render caching is that the subtree no longer needs to be rendered: that already happened. Which means you have all bubbled cacheability metadata, et voila: bubbling support!) When you notice that, this render cache implementation decides not to return the actually stored render cache item, but a placeholder, because it doesn't want to poison the rest of the page with poor cacheability. And then when the placeholder is replaced, it will be a simple render cache hit.

Wim Leers’s picture

Step 2/4: cache tags for cache redirects

Conceptual explanation

From the patch:

+      // Create a redirect cache tag, which is used for two purposes:
+      // 1. invalidate cached supertrees (otherwise redirected render cache
+      //    items may continue to not be automatically placeholdered, because a
+      //    high-cardinality cache context was not yet known originally)
+      // 2. invalidate cache items that become obsolete when the cache contexts
+      //    stored in the cache redirect are discovered to be incomplete.

So, this helps with keeping the number of cache items lower (2), but much more importantly, it is necessary for correctness: in case a redirect changes (i.e. additional cache contexts are bubbled), then something that is not yet placeholdered because only the ['user.roles'] cache context was bubbled, may suddenly need to become placeholdered, because now the ['session'] cache context is also bubbled. Without a cache tag to invalidate e.g. a SmartCache-cached page that contains the redirected, but non-placeholdered output that varies by user, that would not start showing a placeholder as it should.

Wim Leers’s picture

Step 3/4: support for auto-placeholdering in case of bubbled cache tags

This really does not need additional logic; step 1 already provides all the necessary infrastructure. This just adds test coverage for this case.

Wim Leers’s picture

Step 4/4: support for auto-placeholdering in case of bubbled max-age

Notable changes

  • RenderCache is split up into slightly more methods, so that PlaceholderingRenderCache is able to override in the right places
  • PlaceholderingRenderCache grew significantly more complex to be able to handle the efficient auto-placeholdering of bubbled max-age=0.

Conceptual explanation

To be able to efficiently know that a subtree bubbles max-age=0, we must be able to … cache the fact that max-age=0 was bubbled! This seems utterly and completely wrong at first sight :)
But! If the subtree bubbled max-age=0, then that must be deterministic. Therefore, given the same cache contexts (i.e. same input from the request context) and given the same tags (i.e. same input data from Drupal), it must remain max-age=0.

This is highly counter-intuitive, but I haven't been able to find any example where this breaks down. Props to the amazing @Fabianx for dreaming this up! :D

See the documentation in the patch for more explanations.

Wim Leers’s picture

Title: Auto-placeholdering for #lazy_builder with bubbled cache contexts (and max-age=0?) » Auto-placeholdering for #lazy_builder with bubbling
Issue summary: View changes

Next step: as we said in #2543332: Auto-placeholdering for #lazy_builder without bubbling, we should update the comment form to bubble the necessary cache contexts, so we can simplify CommentDefaultFormatter.

Wim Leers’s picture

Finally, I really want to stress that the above would not have been possible without @Fabianx. This is basically a completely rewritten implementation of his PoC code in #2469431: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts — this code should be of sufficient quality to be acceptable for D8 core, with sufficient test coverage, and so on.

I used his PoC as a general guideline, but often did things differently to achieve a simpler code flow.

In any case, without his PoC, i.e. without him having validated that this was at all possible, we wouldn't be here.

Fabianx++
Fabianx++
Fabianx++

The last submitted patch, 5: auto-placeholdering-step2-2543334-5.patch, failed testing.

The last submitted patch, 6: auto-placeholdering-step2-2543334-6.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 7: auto-placeholdering-step2-2543334-7.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
57.27 KB
9.8 KB

Before fixing the test failures: here's a test showing (proving) why step 2 (#5), i.e. a cache tag for every cache redirect, is necessary.

Note that this could technically be moved into a separate issue, but it is only once we have placeholdering for bubbled metadata (which is exactly what this issue adds), that this pre-existing bug becomes more apparent.

Wim Leers’s picture

And here's an additional unit test that verifies render cache redirect cache tags are indeed generated in the expected manner.

Wim Leers’s picture

And here are then finally the test fixes for the test failures in all preceding patches; all due to missing cache redirect cache tags in some of the integration tests.

This makes the patch 50% bigger. Which is why I'm now inclined to move the cache redirect cache tag portion of this patch into a separate patch/issue. It's a pre-existing bug anyway. And that will make this patch significantly simpler to review.

The last submitted patch, 13: auto-placeholdering-step2-13.patch, failed testing.

The last submitted patch, 14: auto-placeholdering-step2-14.patch, failed testing.

Wim Leers’s picture

New failures, so this won't be green.

Status: Needs review » Needs work

The last submitted patch, 15: auto-placeholdering-step2-15.patch, failed testing.

Wim Leers’s picture

I moved all the cache redirect stuff to #2548891: Cache context bubbling: cache redirects need cache tags. This makes comments #5, #13, #15 and #16 obsolete here.

Hence this patch is now back to just #4 + #6 + #7, i.e. steps 1, 3 and 4. Step 2 is moved to #2548891.

Wim Leers’s picture

I'm thinking that adding a private render_placeholder service (just like we have a private render_cache service) that is used by both the renderer and render_cache services is the simplest/sanest way to fix the duplication that we currently have in renderer + render_cache services.

Fabianx’s picture

+1 to #32#21, sounds great!

Fabianx’s picture

Status: Needs review » Needs work

CNW for #21, so no longer blocked on reviews.

Wim Leers’s picture

#22 referred to #32, that should have been #21 I assume? :)

Also, you surely must have *some* remarks, questions or at least nitpicks? :)

Fabianx’s picture

#24: Yes, will review more later.

Fabianx’s picture

Incomplete review. this needs some deep thinking.

  1. +++ b/core/lib/Drupal/Core/Render/PlaceholderingRenderCache.php
    @@ -0,0 +1,241 @@
    +   * @var FALSE|int
    ...
    +  protected $expireOverride = FALSE;
    ...
    +      $this->expireOverride = Cache::PERMANENT;
    ...
    +      $this->expireOverride = FALSE;
    

    I think I would prefer NULL and isset() instead of FALSE.

  2. +++ b/core/lib/Drupal/Core/Render/PlaceholderingRenderCache.php
    @@ -0,0 +1,241 @@
    +    if ($result === FALSE && $this->canCreatePlaceholder($pre_bubbling_elements) && $this->requestStack->getCurrentRequest()->isMethodSafe() && !$this->maxAgeAllowsCaching($elements) && $this->maxAgeAllowsCaching($pre_bubbling_elements) && $cid = $this->createCacheID($elements)) {
    

    Should this not only cache the max-age-0 if if _should_ placeholder the max-age=0 case?

    I know its an edge case.

    Interesting solution, I am still analyzing all cases I had.

  3. +++ b/core/lib/Drupal/Core/Render/RenderCache.php
    @@ -64,10 +64,11 @@ public function get(array $elements) {
    -    if (!$this->requestStack->getCurrentRequest()->isMethodSafe() || !$cid = $this->createCacheID($elements)) {
    +    if (!$this->requestStack->getCurrentRequest()->isMethodSafe() || !$this->maxAgeAllowsCaching($elements) || !isset($elements['#cache']['keys'])) {
    
    @@ -91,10 +92,24 @@ public function set(array &$elements, array $pre_bubbling_elements) {
    -    if (!$this->requestStack->getCurrentRequest()->isMethodSafe() || !$cid = $this->createCacheID($elements)) {
    +    if (!$this->requestStack->getCurrentRequest()->isMethodSafe() || !isset($elements['#cache']['keys']) || !$this->maxAgeAllowsCaching($elements)) {
    

    I think the order should be consistent in both same checks?

  4. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -530,6 +533,12 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +      $context->pop();
    +      $context->push(new BubbleableMetadata());
    +      $context->update($elements);
    

    Nice! :)

    Now you finally know what I meant there.

    I like the solution in throwing away the top frame and populating it again from whatever is in $elements.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
51.97 KB
21.58 KB

First doing my proposal in #21, since Fabianx was +1 to that in #22.

Wim Leers’s picture

And when we did #27, we might as well move canCreatePlaceholder() in there too, out of PlaceholderingRenderCache. Then Renderer can reuse that as well.

Wim Leers’s picture

Oops, I meant this to be part of #28 too. Well, in any case: this makes the service private and adds some documentation to PlaceholderGeneratorInterface.

Wim Leers’s picture

#26

  1. Done.
  2. You're right, but the only reason this would not be cached already is if max-age = 0. So it's implied by the other checks, but you're right, we should do this for consistency.
  3. Of course, good catch, fixed.
  4. :) Yes, much simpler than what you had.

The last submitted patch, 27: auto-placeholdering-step2-2543334-27.patch, failed testing.

Wim Leers’s picture

Apparently \Drupal\Tests\views\Unit\Controller\ViewAjaxControllerTest instantiates a Renderer. Had to update that too.

The last submitted patch, 28: auto-placeholdering-step2-2543334-28.patch, failed testing.

The last submitted patch, 29: auto-placeholdering-step2-2543334-29.patch, failed testing.

The last submitted patch, 30: auto-placeholdering-step2-2543334-30.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 32: auto-placeholdering-step2-2543334-32.patch, failed testing.

Wim Leers’s picture

Failed on old testbot, passed on DrupalCI. Weird. Re-testing.

The last submitted patch, 32: auto-placeholdering-step2-2543334-32.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
53.33 KB

First, a straight reroll. Let's see if this was a PIFR problem after all. Because DrupalCI was reporting #32 as all-green.

Status: Needs review » Needs work

The last submitted patch, 40: auto-placeholdering-step2-2543334-40.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
53.33 KB
948 bytes

Could it be this?

Crell’s picture

  1. +++ b/core/lib/Drupal/Core/Render/PlaceholderingRenderCache.php
    @@ -0,0 +1,163 @@
    +      if ($this->placeholderGenerator->canCreatePlaceholder($elements) && $this->placeholderGenerator->shouldAutomaticallyPlaceholder($cached_element)) {
    +        return $this->placeholderGenerator->createPlaceholder($elements);
    +      }
    

    This is inefficient as it calls createPlaceholder() twice. Instead, check shouldPlaceholder() first, and then $placeholder = createPlaceholder() within the if statement, then you can simply return $placeholder.

  2. +++ b/core/lib/Drupal/Core/Render/PlaceholderingRenderCache.php
    @@ -0,0 +1,163 @@
    +      // PlaceholderingRenderCache::set() has cached a bubbled max-age = 0, and
    +      // placeholders are currently being replaced. Don't return the cached
    +      // element, because it *only* contains max-age = 0, it doesn't contain the
    +      // rendered content (which it cannot and may not, because after all it was
    +      // uncacheable, due to that max-age = 0).
    +      // When not replacing placeholders, the above will ensure that this
    +      // max-age=0 element is placeholdered.
    +      if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === FALSE && !$this->maxAgeAllowsCaching($cached_element)) {
    +        return FALSE;
    +      }
    

    I read this paragraph twice and I'm still not following. This method seems to be responsible for returning, um, a placeholder or filtering out what? Sorry, I'm just confused.

    Also, a method returning a meaningful value or FALSE is always wrong, always, period. If you mean "there's no value here", that's NULL, not FALSE.

  3. +++ b/core/lib/Drupal/Core/Render/PlaceholderingRenderCache.php
    @@ -0,0 +1,163 @@
    +  protected function maxAgeToExpire($max_age) {
    +    // This is a quite hacky override, but otherwise this would have to
    +    // reimplement all of \Drupal\Core\Render\RenderCache::doSet().
    +    if (isset($this->expireOverride)) {
    +      return $this->expireOverride;
    +    }
    +    return parent::maxAgeToExpire($max_age);
    +  }
    

    So just refactor RenderCache::doSet() to make it easier to swap out the pieces you need. :-) Just from this comment it sounds like doSet() needs to be broken out into multiple methods anyway.

Wim Leers’s picture

#43: Thanks for the review! Much appreciated :)

  1. Is it possible that you misread the patch slightly? The if-test calls canCreatePlaceholder(), the code within the if-test calls createPlaceholder(). (So the code you cited first check if it can be placeholdered at all, then it checks if it should be placeholdered, and if the answer to those questions is "yes" in both cases, then it does create a placeholder.
  2. First, note that this was added in #7. This is only necessary for a bubbled max-age that causes placeholdering. Max-age-triggered placeholdering definitely is by far the most complex part here. Perhaps it's better to split that off into a separate issue?

    Second: an attempted explanation. In the ::set() method, we are caching the fact that a max-age = 0 was bubbled. That sounds strange, but we need to cache that to not have to render the entire subtree to know in the first place that a max-age=0 is being bubbled, otherwise we're still rendering everything only to throw it away. So, we need to cache the fact that this subtree can not be cached. This aspect is cacheable, because it must be happening for a specific combination of cache contexts, that is, because the fact that this subtree is not cacheable must be deterministic. So, put simply: we're caching the fact that this particular branch of code results in a render array that cannot be cached.
    Now, given that we've cached the uncacheability of this branch of code, we actually get to the point where I can explain the reasoning behind the code you quoted. That code is dealing with a render cache hit, and specifically a render cache item that caches the uncacheability of a branch of code (of a render array subtree). And not only that, but it specifically checks whether placeholders are currently being replaced (see \Drupal\Core\Render\Renderer::renderPlaceholder(), which contains a comment // Prevent the render array from being auto-placeholdered again. for the code $placeholder_elements['#create_placeholder'] = FALSE; — this ensures that we don't do recursive placeholdering) by checking if #create_placeholder === FALSE. In that case, it needs to return FALSE, to ensure that we actually build the entire subtree (by executing the #lazy_builder) and render it. In the other case (where #create_placeholder is not set or TRUE), the preceding code block is executed, and the cached max-age=0 causes this subtree to be placeholdered.
    (Who said Drupal is only joining bits and pieces together and you don't need algorithmic knowledge/understanding? This is a fairly advanced algorithm exploiting tree properties and deterministicness.)

    If this does either still leave you with questions or makes you uncertain about this part of the patch (which I would completely understand, because this is very tricky), then I'd ask that we move max-age-triggered bubbling to a follow-up, because as I mentioned earlier, this is by far the most complex part of the patch.

    (While writing this comment, I tried to simplify the logic, but I couldn't. Because it needs to check specifically for a subset of reasons to placeholder (only max-age, the first condition of shouldAutomaticallyPlaceholder()) and because it needs to check for a subset of the reasons that show whether something can be placeholdered (only the second condition of canCreatePlaceholder()), it is indeed very confusing.

  3. I don't think it makes sense to split doSet(). It's long, but only because of that very (very!) long comment. And this too is only complex/nasty because o bubbled-max-age-triggered auto-placeholdering is so complex.
    If there's a way to split up doSet(), I'm not seeing it. Because we want all that logic, we just want to override the max-age. We could add an optional $overridden_max_age parameter to doSet(), but it would never make sense for RenderCache itself. So it'd make that method more difficult to understand.

    So… let's move bubbled-max-age-triggered auto-placeholdering to a separate issue?


In this reroll, I removed the bubbled-max-age-triggered auto-placeholdering, so we can do that in a separate issue, opened #2559847: Auto-placeholdering for #lazy_builder with bubbling of max-age for that. That makes this issue just about "placeholdering for things that are cacheable but that we don't want to cache because it'll be too expensive to cache". Either because of too many variations (certain cache contexts) or too frequent invalidations (certain cache tags).

The max-age portion that lived in this patch until now is much more complex because it is very different: it's for placeholdering things that are not cacheable!

Wim Leers’s picture

Title: Auto-placeholdering for #lazy_builder with bubbling » Auto-placeholdering for #lazy_builder with bubbling of contexts and tags

Clarifying title for #44.

Fabianx’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Core/Render/PlaceholderingRenderCache.php
@@ -0,0 +1,109 @@
+    $cached_element = parent::get($elements);
...
+      if ($this->placeholderGenerator->canCreatePlaceholder($elements) && $this->placeholderGenerator->shouldAutomaticallyPlaceholder($cached_element)) {
+        return $this->placeholderGenerator->createPlaceholder($elements);
+      }

The limitation of this solution is that you will always need to have a cache hit for the placeholder to work.

So you can never make use of the information that is available in the redirect.

As the parent::get() won't give you that information, which is why I had way more tight coupling in my proof-of-concept patch.

e.g. updating the redirecting cache item on ::set().

e.g.

For a block cacheable by user, where the user is bubbled up:

My solution:

After any user loads a page with that block:

- Cache redirect contains 'user'
- Detected as frequent cache context
- #create_placeholder is added to 'redirecting cache item'

The next time for a different user:

- Cache miss on the cache, but a cache hit on the 'redirecting cache item' and seeing '#create_placeholder' => creating a placeholder directly (if it canCreatePlaceholder), as such BigPipe could deliver the block later.

Your solution / solution here:

- Cache miss
- Item is re-created by executing the block
- Checking for should-placeholder
- Placeholder then
- Item is re-created by re-executing the block

---

Also we have double cache_get() here, right?

Which is why I stored that information somewhere, in this case the "cache" could live in the placeholderGenerator.


Overall looks great!

I think it is a good intermediate step and I think adding more smarter auto-placeholdering should be fine as an internal refactoring.

The double cache_get is a little unfortunate, but without render strategies, where that could live in a 'cached placeholder' render strategy, this is difficult to generically resolve here.

The critical problem is that a block would be executed twice, as the executed lazy builder callback result is stored nowhere - however it would theoretically be cached - unless it is max-age = 0, which could happen.

I suggest as a quick fix to:

- add a second argument to createPlaceholder

e.g. RenderCache::get

$placeholderGenerator->createPlaceholder($elements, $cached_element);

e.g. RenderCache::set

$placeholderGenerator->createPlaceholder($pre_bubbling_elements, $elements);

Then in Renderer ask the placeholderGenerator if the placeholder is already statically cached and use that:

e.g. in Renderer::renderPlaceholder

  // @todo Replace with static caching render strategies once ... lands.
  $markup = $this->placeholderGenerator->getPlaceholderData($placeholder);

  // Render the placeholder into markup.
  if (!isset($markup)) {
    $markup = $this->renderPlain($placeholder_elements);
  }

That would solve both cases of double cache_get and throwing away existing information.

Fabianx’s picture

The scope for this patch that needs work and which I would like to see is:

- Ensure that any data we pre-computed is statically cached (for this request) and used in renderPlaceholder() of the renderer (until render strategies are in).

Motivation:

I don't think we can afford to build a block that takes 5 or 10 seconds and is uncacheable, but should be placeholdered after bubbling per the rules twice. As that would double the time used for that block.

The implementation details I propose are in #46 starting from "I suggest as a quick fix to:" ...


But here they are again in pseudo diff form:

PlaceholderingRenderCache::get

 - $this->placeholderGenerator->createPlaceholder($elements);
 + $this->placeholderGenerator->createPlaceholder($elements, $cached_element);

PlaceholderingRenderCache::set

- $this->placeholderGenerator->createPlaceholder($pre_bubbling_elements);
+ $this->placeholderGenerator->createPlaceholder($pre_bubbling_elements, $elements);

In PlaceholderGenrator::createPlaceholder:

placeholderGenerator::createPlaceholder($elements, $data = NULL) {
  // ...

  if (isset($data)) {
+    $this->placeholderData[$placeholder] = $data;
  }
}

Then in Renderer ask the placeholderGenerator if the placeholder is already statically cached and use that:

In Renderer::renderPlaceholder

+  // @todo Replace with static caching render strategies once ... lands.
+  $markup = $this->placeholderGenerator->getPlaceholderData($placeholder);

  // Render the placeholder into markup.
+  if (!isset($markup)) {
    $markup = $this->renderPlain($placeholder_elements);
+  }
Wim Leers’s picture

Status: Needs work » Needs review
  1. A double cache get is harmless, because DatabaseBackend and others already have static caches.
  2. I don't think we can afford to build a block that takes 5 or 10 seconds and is uncacheable, but should be placeholdered after bubbling per the rules twice. As that would double the time used for that block.

    To clarify:

    1. if the expensive value for the placeholder is cacheable at all (e.g. per user), then it would simply be a cache hit once it is computed a first time. => the claimed problem does not exist in this case.
    2. if the expensive value is not cacheable (i.e. max-age=0), then #2559847: Auto-placeholdering for #lazy_builder with bubbling of max-age ensures a cache item is created with just the metadata to let the placeholdering render cache know that a placeholder must be created. => with just this patch, the claimed problem does exist; but once #2559847 is in, it will no longer exist

Therefore, I don't think any extra work is necessary here.

Fabianx’s picture

#48: Would be nice for certain cases, but unfortunately it is not true:

https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Cache%21D...

There is no static cache.

catch in particular does not like double Cache::get, so I don't think this will fly.

Wim Leers’s picture

Darn, misleading variable names! You're right.

So, #48.1 is invalid and needs a solution.

But point #48.2 still stands. And that's the trickier point you raised. Could you reply to that also?

Fabianx’s picture

We will need a way to store computed data in general, we could postpone this one on placeholder strategies, which makes it way more straightforward to add, but as I said even if we retrieve it again from cache, I don't think a cache:set, cache:get to itself is a good practice.

Fabianx’s picture

Status: Needs review » Needs work

#48.2: For the first request it would exist as well as that information would need to come from somewhere and also this would at the moment mean that if auto-placeholdering for max-age = 0 is on, then it would render all of that twice instead.

Per pure logic:

 if ($this->placeholderGenerator->canCreatePlaceholder($pre_bubbling_elements) && $this->placeholderGenerator->shouldAutomaticallyPlaceholder($elements)) {
    // create placeholder
 }

So if any of that is TRUE it will create a placeholder and throw away the computed data.

- canCreatePlaceholder checks for #lazy_builder present and that the #create_placeholder is not FALSE
- shouldCreatePlaceholder checks max-age condition

=> Can be met
=> Placeholder created for max-age = 0
=> Expensive block re-computed as result is stored nowhere
=> Needs work

Fabianx’s picture

diff --git a/core/modules/system/src/Plugin/Block/SystemPoweredByBlock.php b/core/modules/system/src/Plugin/Block/SystemPoweredByBlock.php
index c768e21..c9d3f2d 100644
--- a/core/modules/system/src/Plugin/Block/SystemPoweredByBlock.php
+++ b/core/modules/system/src/Plugin/Block/SystemPoweredByBlock.php
@@ -25,7 +25,11 @@ class SystemPoweredByBlock extends BlockBase {
    * {@inheritdoc}
    */
   public function build() {
-    return array('#markup' => '<span>' . $this->t('Powered by <a href="@poweredby">Drupal</a>', array('@poweredby' => 'https://www.drupal.org')) . '</span>');
+    error_log(print_r(['Build called!'], TRUE));
+    return array(
+      '#markup' => '<span>' . $this->t('Powered by <a href="@poweredby">Drupal</a>', array('@poweredby' => 'https://www.drupal.org')) . '</span>',
+      '#cache' => ['max-age' => 0],
+    );
   }
 
   /**

can be used to show the problem of #52.

With HEAD, build is just called once; with #44 patch above build is called twice.

The attached patch solves the problem using placeholder strategies and is rolled on top of them and build is again just called once.

Wim Leers’s picture

I was very hopeful #53 would be an elegant solution. But… we still have a static cache in PlaceholderGenerator. I thought the point was that the StaticCacheStrategy placeholder strategy would contain all that? That the rest would not have to think/be aware about this?
Hence this solution much more complex than I had anticipated
I expected that nothing in the existing auto-placeholdering patch would have to change. But that all the static caching/double-get avoiding stuff would live in a static cache placeholder strategy. Instead, it now adds static caching to both the existing patch and the static cache placeholder strategy.

I now see why that was foolish of me, why it is absolutely necessary that this happens inside the render cache logic: that's the place where auto-placeholdering happens, so it's also where we have to statically cache them (again, to avoid double cache read or double render).

Hence I propose this patch, which is relative to #44, which adds a static cache to PlaceholderingRenderCache, which makes this much easier to understand IMHO.

dawehner’s picture

+++ b/core/lib/Drupal/Core/Render/PlaceholderingRenderCache.php
@@ -100,10 +134,54 @@ public function set(array &$elements, array $pre_bubbling_elements) {
+    $this->placeholderResultsCache[$placeholder] = $rendered_elements;

I think if possible we should key that by the request, just to be sure.

Fabianx’s picture

Overall the counter proposal looks great!

It really is a matter of which pill to take and which service to add the state, too.

Note: That adding it here does not prevent us from moving that to render strategies transparently, so I am inclined to RTBC for now, but there is a little problem, which is that this is missing to use getCacheableRenderArray(), even for a static cache its important to use the cacheable version, not whatever is passed in.

  1. +++ b/core/lib/Drupal/Core/Render/PlaceholderingRenderCache.php
    @@ -76,6 +101,15 @@ public function __construct(RequestStack $request_stack, CacheFactoryInterface $
    +    // When rendering placeholders, special case auto-placeholdered elements:
    +    // avoid retrieving them from cache again, or rendering them again.
    +    if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === FALSE) {
    +      $cached_placeholder_result = $this->getFromPlaceholderResultsCache($elements);
    +      if ($cached_placeholder_result !== FALSE) {
    +        return $cached_placeholder_result;
    +      }
    +    }
    

    The advantage of moving that to placeholder strategies is that there is no additional run-time overhead for normal RenderCache::get() requests of which there could be more than just a few.

  2. +++ b/core/lib/Drupal/Core/Render/PlaceholderingRenderCache.php
    @@ -100,10 +134,54 @@ public function set(array &$elements, array $pre_bubbling_elements) {
    -      $elements = $this->placeholderGenerator->createPlaceholder($pre_bubbling_elements);
    +      $elements = $this->createPlaceholderAndRemember($elements, $pre_bubbling_elements);
    

    I made the mistake first, too.

    Either here or in createPlaceholderAndRemember it should be used via $this->getCacheableRenderArray(). Else you get a ->getPlugin() not found error.

Wim Leers’s picture

  1. This is why I'm wrapping it in if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === FALSE) {, which only evaluates to TRUE when replacing placeholders or when an element explicitly marks itself as unplaceholderable. So the overhead is extremely, extremely limited.
  2. Sure, added the getCacheableRenderArray() call in the cache set case. I actually first had that, but I thought it'd make the code slightly more complex. But I totally agree it's better.
Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

RTBC, thanks all feedback has been addressed.

Wim Leers’s picture

Status: Reviewed & tested by the community » Needs review

#55's I think if possible we should key that by the request, just to be sure. still needs to be addressed

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community

So, to answer #55:

<Fabianx-screen> Subrequests should not have their own cache.
<Fabianx-screen> As we only deal with it in the master request.

… and that just happens to make it effectively the same question that catch asked over at #2349011-53: Support placeholder render strategies so contrib can support BigPipe, ESI….1: he was wondering about placeholder strategies not applying to subrequests (and thus placeholder replacement not happening in subrequests).

But, already in HEAD, placeholder replacement only happens in the master request, because HtmlResponseAttachmentsProcessor only runs on the master request. Hence this cannot be a problem.

Back to RTBC per #58.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 57: auto-placeholdering-step2-2543334-57.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
47.66 KB

Chasing HEAD. Straight reroll.

alexpott’s picture

diff --git a/core/lib/Drupal/Core/Render/PlaceholderingRenderCache.php b/core/lib/Drupal/Core/Render/PlaceholderingRenderCache.php
index 16e2622..bd3937b 100644
--- a/core/lib/Drupal/Core/Render/PlaceholderingRenderCache.php
+++ b/core/lib/Drupal/Core/Render/PlaceholderingRenderCache.php
@@ -7,12 +7,8 @@
 
 namespace Drupal\Core\Render;
 
-use Drupal\Component\Utility\SafeMarkup;
-use Drupal\Component\Utility\UrlHelper;
-use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheFactoryInterface;
 use Drupal\Core\Cache\Context\CacheContextsManager;
-use Drupal\Core\Template\Attribute;
 use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
index 59a072b..e2bc876 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
@@ -688,7 +688,7 @@ public function testCacheableParent($test_element, $args, array $expected_placeh
     }
     // The normally cacheable edge case: a high-invalidation frequency cache tag
     // is bubbled from a #lazy_builder callback for an uncacheable placeholder.
-    // The element continaing the uncacheable placeholder has cache keys set,
+    // The element containing the uncacheable placeholder has cache keys set,
     // and also has the bubbled cache tags.
     elseif ($edge_case_a7_uncacheable) {
       $cached_element = $cached->data;

Some nits that can be fixed on commit.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 62: auto-placeholdering-step2-2543334-62.patch, failed testing.

lauriii’s picture

Status: Needs work » Needs review
FileSize
49.5 KB
2.16 KB

Just quick reroll. It seems like something adds user.permissions cache context which is being optimized away and for that reason some extra cache tags are bubbling up. Don't credit me for this because this is a small change in a big change :)

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
50.05 KB
3.14 KB

#2511516: Make local tasks and actions hooks provide cacheability metadata, to make the blocks showing them cacheable causes additional cache tags, hence these new failures. lauriii already fixed them, I was cross-posting with him :P. Thanks, lauriii! :)

Also fixed #63. And fixed two typos in #65.

(This reroll is 100% documentation nits and unused use statement removals.)

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 66: auto-placeholdering-step2-2543334-66.patch, failed testing.

The last submitted patch, 66: auto-placeholdering-step2-2543334-66.patch, failed testing.

lauriii’s picture

Status: Needs work » Needs review
FileSize
49.23 KB

Rerolled the patch.

  1. +++ b/core/lib/Drupal/Core/Render/PlaceholderGenerator.php
    @@ -0,0 +1,101 @@
    +    $callback = $placeholder_render_array['#lazy_builder'][0];
    +    $arguments = UrlHelper::buildQuery($placeholder_render_array['#lazy_builder'][1]);
    +    $token = hash('crc32b', serialize($placeholder_render_array));
    +    $placeholder_markup = '<drupal-render-placeholder callback="' . $callback . '" arguments="' . $arguments . '" token="' . $token . '"></drupal-render-placeholder>';
    

    This chunk has changed where its copied from

  2. +++ b/core/lib/Drupal/Core/Render/PlaceholderingRenderCache.php
    @@ -0,0 +1,183 @@
    +    $this->placeholderResultsCache[(string) $placeholder] = $rendered_elements;
    ...
    +    $placeholder = (string) $placeholder_element['#markup'];
    

    Also had to add casting here

Wim Leers’s picture

Thanks, @lauriii!

I also rebased my own local branch, and arrived at the same end result. Here is a rebase interdiff (for #70.1) and an interdiff for the necessary casting changes (#70.2).

And a patch is relative to latest HEAD as of today while I'm posting interdiffs anyway. This patch is equivalent with #70.


Given that this is a straight rebase, with code moved by this patch being updated, and two simple string casts necessary due to changes in HEAD in that moved code, and those string casts being used only to allow them to be used as array keys, I think it's fine for me to re-RTBC this.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 71: auto-placeholdering-step2-2543334-71.patch, failed testing.

The last submitted patch, 71: auto-placeholdering-step2-2543334-71.patch, failed testing.

Wim Leers’s picture

#2545972: Remove all code usages SafeMarkup::checkPlain() and rely more on Twig autoescaping, which landed after #70 was posted, added a test that is now failing. Reproduced locally. Working on a fix.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
50.65 KB
1.06 KB

Cause of those two failures: the local tasks on the tracker page bubbles the user cache context. In HEAD, this causes Dynamic Page Cache to not cache it. But with this patch, the local tasks block is instead auto-placeholdered, allowing the rest of the page to be cached. And because those two failures are both due to expected changes in output after setting something using \Drupal::state(), naturally, the pages are still cached by Dynamic Page Cache.

The fix is simple: apply the same rule as we've applied dozens upon dozens of times now: if a test does things by using State, then we have to manually force it to bypass caches.

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Changes look great to me.

Back to RTBC.

  • catch committed 7d6f560 on 8.0.x
    Issue #2543334 by Wim Leers, lauriii, Fabianx: Auto-placeholdering for #...
catch’s picture

Status: Reviewed & tested by the community » Fixed

This looks great. The docs explain what's going on, and this should be very transparent to most people which is of course the point.

I didn't find anything other than a couple of very minor nits.

Committed/pushed to 8.0.x, thanks!

Wim Leers’s picture

Glad you liked it so much, @catch! And indeed the point is that it's a transparent change :)

Next step: #2559847: Auto-placeholdering for #lazy_builder with bubbling of max-age, which can easily happen after RC1, precisely because it's also a transparent change.

Status: Fixed » Closed (fixed)

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