Problem/Motivation

See #2499157: [meta] Auto-placeholdering.

If blocks were to use the #lazy_builder pattern, they could be automatically placeholderable:

(The above means that blocks will de facto be the unit of placeholdering. Of course, other things that use #lazy_builders will also be individually placeholderable. But, for those things that don't use #lazy_builders, the block that they are rendered in will then automatically be placeholdered, and thus ensure great cacheability for the containing pages.)

Proposed resolution

Have BlockViewBuilder implement #lazy_builder; no block plugins need to be changed, because they are already being rendered lazily. This issue simply changes it from #pre_render to #lazy_builder.

Remaining tasks

Generally: Review. See #41 and #42 in particular.

User interface changes

None.

API changes

  • Change: hook_block_view_alter() can no longer be used to alter cache keys/contexts/tags/max-age. The newly added hook_block_build_alter() needs to be used for that.

Data model changes

None.

CommentFileSizeAuthor
#50 interdiff.txt2 KBwim leers
#50 auto-placeholdering-block_lazy_builder-2543340-50.patch26.99 KBwim leers
#47 interdiff.txt1.09 KBwim leers
#47 auto-placeholdering-block_lazy_builder-2543340-47.patch26.86 KBwim leers
#45 interdiff.txt4.23 KBwim leers
#45 auto-placeholdering-block_lazy_builder-2543340-45.patch26.82 KBwim leers
#42 interdiff.txt1.56 KBwim leers
#42 auto-placeholdering-block_lazy_builder-2543340-42.patch29.75 KBwim leers
#42 auto-placeholdering-block_lazy_builder-2543340-40-fail.patch28.26 KBwim leers
#40 xhprof runs.zip1.1 MBwim leers
#40 interdiff.txt3.79 KBwim leers
#40 auto-placeholdering-block_lazy_builder-2543340-40.patch28.26 KBwim leers
#39 interdiff.txt2.55 KBwim leers
#39 auto-placeholdering-block_lazy_builder-2543340-39.patch26.64 KBwim leers
#37 interdiff.txt12.86 KBwim leers
#37 auto-placeholdering-block_lazy_builder-2543340-37.patch27.55 KBwim leers
#27 interdiff.txt2.08 KBwim leers
#27 auto-placeholdering-block_lazy_builder-2543340-27.patch17.57 KBwim leers
#26 auto-placeholdering-block_lazy_builder-2543340-26.patch17.3 KBwim leers
#23 interdiff.txt2.78 KBwim leers
#23 auto-placeholdering-block_lazy_builder-2543340-23.patch19.1 KBwim leers
#18 interdiff.txt6.38 KBwim leers
#18 auto-placeholdering-block_lazy_builder-2543340-18.patch18.98 KBwim leers
#16 interdiff.txt5.06 KBwim leers
#16 auto-placeholdering-block_lazy_builder-2543340-16.patch22.79 KBwim leers
#15 interdiff.txt1.28 KBwim leers
#15 auto-placeholdering-block_lazy_builder-2543340-14.patch23.01 KBwim leers
#11 interdiff-6-11.txt5.77 KBwim leers
#11 interdiff.txt968 byteswim leers
#11 auto-placeholdering-block_lazy_builder-2543340-11.patch22.36 KBwim leers
#8 interdiff.txt2.37 KBwim leers
#8 auto-placeholdering-block_lazy_builder-2543340-8.patch22.76 KBwim leers
#6 interdiff.txt3.66 KBwim leers
#6 auto-placeholdering-block_lazy_builder-2543340-5.patch21.43 KBwim leers
#4 interdiff.txt1.91 KBwim leers
#4 auto-placeholdering-block_lazy_builder-2543340-4.patch19.23 KBwim leers
#3 interdiff.txt6.7 KBwim leers
#3 auto-placeholdering-block_lazy_builder-2543340-3.patch17.4 KBwim leers
#1 auto-placeholdering-block_lazy_builder-1.patch10.76 KBwim leers

Comments

wim leers’s picture

Issue summary: View changes
Status: Active » Needs review
StatusFileSize
new10.76 KB

Note: this will fail at least BlockViewBuilderTest. I felt it was more important to show the key parts of the patch soon, than to post a perfect patch.

Status: Needs review » Needs work

The last submitted patch, 1: auto-placeholdering-block_lazy_builder-1.patch, failed testing.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new17.4 KB
new6.7 KB

This should fix the 11 failures in BlockViewBuilderTest.

  • The fact that we now use a #lazy_builder no longer allows us to easily unset #theme, which means we cannot easily avoid the theme system executing just to simplify tests, therefore… as of this patch, we don't do that anymore.
  • The above makes it much harder to test the $build['#suffix'] = '<br>Goodbye!'; test case, so I've changed it to $build['#attributes']['foo'] = 'bar'; — we're still testing the same modifiability.
  • Then… what is surely going to be the most contentious change: this patch no longer allows cache keys to be altered in. We explicitly kept this (in #2158003: Remove Block Cache API in favor of blocks returning #cache with cache tags), but I don't think it makes sense. If you are using a specific block in a very narrow, optimized way, and you're smart enough to alter its cache keys (well, that should actually be cache contexts, but anyway), then you're also going to be just fine with subclassing the existing class, and overriding the getCacheContexts() method. If we really turn out to have a need for such an alter hook, we should add a separate alter hook.
wim leers’s picture

StatusFileSize
new19.23 KB
new1.91 KB

And apparently the help and statistics blocks also aren't placeholderable, because both carry state: they store data in properties in their respective classes in their blockAccess() implementations.

So this is quite an excellent way to detect stateful blocks.

This should be green.

yched’s picture

+      // Some blocks cannot be lazily built (using #lazy_builder callbacks). For
+      // example: the main content block and Views exposed filter blocks. They
+      // depend on the original request context, can hence not be generated out
+      // of band, and can consequently not be built lazily. In other words: they
+      // cannot be rendered using ESI, BigPipe, or other similar techniques

Just wondering : don't ESI and BigPipe differ on that regard though ? ESI rendering happens during a different request, but I thought BigPipe placeholders were rendered as part of the original request (just, delayed after we flush the initial HTML skeleton to the browser).

Also, yeah, not fully sure of when a block should use the NonLazyBuildableBlockPluginInterface marker interface. I thought "my output depends on this and that" was already supposed to be carried by the cache contexts ? but here we're adding another flag somewhere else ?

wim leers’s picture

StatusFileSize
new21.43 KB
new3.66 KB

Fixed the statistics block to not be stateful instead.

(It was written in that way because it used to be the case that to not render a block, you had to have the block's access result not grant access, even if the user actually had access. Because otherwise it'd always render the block.)

wim leers’s picture

#5: Excellent points. You're right regarding ESI & BigPipe — that comment is a tad wrong. And agreed on the concern that cache contexts should already do this, but there's a difference here: these blocks carry state. And that is the real problem here: if a block is relying on $this->something, that got set either in a blockAccess() method (like Help & Statistics), or that is injected because it is a very special block (the main content block has a ->setMainContent() method that is necessary for the rendering system to be able to get the main content into the block without relying on state).

So, I've been thinking that StatefulBlockPluginInterface would actually be a better name. Thoughts?

wim leers’s picture

StatusFileSize
new22.76 KB
new2.37 KB

Also fixed the Help block to not be stateful.

The last submitted patch, 3: auto-placeholdering-block_lazy_builder-2543340-3.patch, failed testing.

wim leers’s picture

#5/@yched: Please look at the #6 & #8 interdiffs. I think they quite clearly demonstrate what the "problem" is with these blocks.

99% of core's blocks don't do this. So, I'd think that only in rare cases, you have to think about this.

wim leers’s picture

StatusFileSize
new22.36 KB
new968 bytes
new5.77 KB

Oops, I was slightly too fast: forgot to update the Help block to no longer use that interface.

Attached is also an interdiff that shows all changes for #6 -> #11 (this comment). It is that diff that most clearly answers @yched's questions in #5.

The last submitted patch, 8: auto-placeholdering-block_lazy_builder-2543340-8.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 11: auto-placeholdering-block_lazy_builder-2543340-11.patch, failed testing.

fabianx’s picture

#5:

We have to distinguish two cases, which BigPipe can handle:

a) A block that is just placeholder'ed for this request. We simply send it later. In that case we need to store the data to process, send max-age=0 to avoid someone caching it, then process it later. That works, because state is known. We just defer rendering some parts. Main property: Smart cache can't cache that. Only BigPipe can deal with such a dynamically cached block.

b) A block that is placeholdered always. In that case smart cache will cache it as #attached placeholders. So we will probably not execute the request, so any state of the blocks needs to be fixed.

---

To the patch itself:

  1. +++ b/core/lib/Drupal/Core/Block/MainContentBlockPluginInterface.php
    @@ -14,7 +14,7 @@
    -interface MainContentBlockPluginInterface extends BlockPluginInterface {
    +interface MainContentBlockPluginInterface extends NonLazyBuildableBlockPluginInterface {
    

    I like StatefulBlockInterface best.

  2. +++ b/core/lib/Drupal/Core/Block/NonLazyBuildableBlockPluginInterface.php
    @@ -0,0 +1,21 @@
    +interface NonLazyBuildableBlockPluginInterface extends BlockPluginInterface { }
    

    An interface works well for blocks.

    For other things I think the best is to use:

    #create_placeholder => FALSE,

    in combination with the #lazy_builder.

    As we don't really want two code paths, which means what would be most nicely if that was set by an alter() callback on the plugin or via a hook (see below).

  3. +++ b/core/modules/block/src/BlockViewBuilder.php
    @@ -77,23 +61,100 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
    +      if ($plugin instanceof NonLazyBuildableBlockPluginInterface) {
    +        // Immediately build a #pre_render-able block, since this block cannot
    +        // be built lazily.
    +        $build[$entity_id] += self::buildPreRenderableBlock($entity, $this->moduleHandler());
    +      }
    

    Hm, yes maybe it really cannot be lazily build and this is not just about placeholdering, but just a side-effect that comes later.

  4. +++ b/core/modules/block/src/BlockViewBuilder.php
    @@ -77,23 +61,100 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
    +        $build[$entity_id] += [
    +          '#lazy_builder' => [get_class($this) . '::lazyBuilder', [$entity_id, $view_mode, $langcode]],
    +        ];
    +      }
         }
    +
         return $build;
    

    Lets add an alter hook here:

    hook_block_pre_view_alter();

    or

    hook_block_lazy_view_alter();

    And I still think it would be best to give the plugin a chance to alter the blocks cache keys.

    if ($this->plugin instanceof AlterableBlockPluginInterface) {
    $this->plugin->alter($build);
    }


    This does not yet deal with blocks using context values that are injected, which need to be able to change the cache keys themselves. (we could opt those out of lazy building for now).

  5. +++ b/core/modules/block/src/BlockViewBuilder.php
    @@ -77,23 +61,100 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
    +  public static function lazyBuilder($entity_id, $view_mode, $langcode) {
    +    return self::buildPreRenderableBlock(Block::load($entity_id), \Drupal::service('module_handler'));
    +  }
    

    This is probably fast for all cases where smart cache is not involved, but we should measure that.

    As we need to again load the things.

    Also why use static methods here, we can have that be a controller callback.

    Background:

    We really should implement LazyBuilderPrepareInterface soon, so we can multi-load all entities in advance.

    While for blocks that is not too huge a concern by adding #lazy_builder to entities it will.

  6. +++ b/core/modules/help/src/Plugin/Block/HelpBlock.php
    @@ -96,13 +89,7 @@ public static function create(ContainerInterface $container, array $configuratio
    +    return AccessResult::allowed();
    

    So the help block would then always be visible right?

    I think placeholdering the help block atm. while interesting should be a follow-up and making the block non lazy-buildable first.

wim leers’s picture

Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new23.01 KB
new1.28 KB

Fail caused by fixing the help block. Now the help block's url cache context bubbles always, like it should. Fixed test expectations.

Also simplified HelpBlock code that I already touched slightly more: I was able to just delete it because it now was identical to the base implementation :)

wim leers’s picture

StatusFileSize
new22.79 KB
new5.06 KB

#14.1: renamed to StatefulBlockPluginInterface.

wim leers’s picture

#14.4: I'd prefer to not implement any of those suggestions in this issue, because:

[…] this patch no longer allows cache keys to be altered in. We explicitly kept this (in #2158003: Remove Block Cache API in favor of blocks returning #cache with cache tags), but I don't think it makes sense. If you are using a specific block in a very narrow, optimized way, and you're smart enough to alter its cache keys (well, that should actually be cache contexts, but anyway), then you're also going to be just fine with subclassing the existing class, and overriding the getCacheContexts() method. If we really turn out to have a need for such an alter hook, we should add a separate alter hook.

#14.5: We need to again load the block config entity, but that's statically cached already anyway. It uses a static method because BlockViewBuilder is not a service, and you can only point to an object if it's also a service. Hence static method.

#14.6: Yep, see #15 :)

wim leers’s picture

StatusFileSize
new18.98 KB
new6.38 KB

Fabian asked me to do the fixes for the Help & Statistics blocks in a separate issue, to keep this issue focused. Opened #2543554: Clean up Help & Statistics blocks for that (patch already posted!).

fabianx’s picture

#17:

4. There are use cases to change the cache keys. Cache Contexts are only useful for state that comes from the outside, not for state that is injected in.

However that is only useful for context block plugins (as that is the only outside injected state) and that has poor support in core, so that is not a good reason.

But core itself does:

        // The main content block cannot be cached: it is a placeholder for the
        // render array returned by the controller. It should be rendered as-is,
        // with other placed blocks "decorating" it.
        if ($block_plugin instanceof MainContentBlockPluginInterface) {
          unset($build[$region][$key]['#cache']['keys']);
        }

So I think it would be cleaner to move that to an ->alter() method instead of hard-coding into BlockPageVariant as contrib cannot and should not override the BlockViewBuilder just for that.

We already have an issue for that though and its a pure API addition, so not totally fixed on that as long as there is some way to alter it:

I am not sure we can do an API change to remove the ability to change #cache at all - as we also removed hook_page_alter(). So how can you change the cacheability of a block from an outside module then?

e.g. I know that this block is user cacheable in my configuration I want to bundle all of that in a mysite_performance module.

That is perfectly possible right now, but no longer after this patch.

So the MVP is to add another alter hook to keep the API / behavior change to a minimum.

14.5:

If that is not a service how will we be able to make that a LazyBuilderPreparableInterface service then? We really will need that as loading entities (and plugins) in bulk is way faster.

wim leers’s picture

  • RE: 14.4: So how can you change the cacheability of a block from an outside module then? — I already answered this:

    If you are using a specific block in a very narrow, optimized way, and you're smart enough to alter its cache keys (well, that should actually be cache contexts, but anyway), then you're also going to be just fine with subclassing the existing class, and overriding the getCacheContexts() method

    Why is that not sufficient?

  • RE: 14.5: we could make it a service in the issue where we introduce that LazyBuilderPreparableInterface, but right now, it is hard to justify.
fabianx’s picture

#20.4: Because subclassing is not the right pattern here. I want to alter, not change. And I want to alter in whatever way I want. (... and and and msonnabaum promised me an alter hook :p.)

Also we are supposed to keep API changes to a minimum and removing an alterable thing is a way bigger API change then splitting it up in two alter hooks.

20.5:

Okay, that sounds good. Can we open an issue for that - if we don't have one already? Yes, pure API addition, just a performance improvement.

Crell’s picture

  1. +++ b/core/lib/Drupal/Core/Block/StatefulBlockPluginInterface.php
    @@ -0,0 +1,18 @@
    +/**
    + * The interface for blocks that carry state and can hence not be lazily built.
    + *
    

    "blocks that carry state" is a meaningless phrase to most developers. Blocks should never carry state, because they're more of a service than a value object.

    What we're actually talking about here is blocks that call some method in the access() method, cache it to a property, and then check that property in the build() method. "Stateful block" is a useless description of that case, and I would have no idea what that even meant if I were not reading this issue.

  2. +++ b/core/modules/block/src/BlockViewBuilder.php
    @@ -77,23 +61,100 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
    +      // Some blocks are stateful (carry properties, hence have state) and can
    +      // therefore not be lazily built (using #lazy_builder callbacks). For
    +      // example: the main content block, which gets the main content injected.
    +      // If they were lazily built, then we would not have any way to recreate
    +      // the original state.
    +      if ($plugin instanceof StatefulBlockPluginInterface) {
    

    This comment block is a bit better, and something like this is what belongs on the interface instead. However, "carry properties" isn't descriptive since all blocks have properties for things like configuration.

    Recommended alternative, to be put on the interface docblock:

    Generally, blocks should not have dependencies between their methods. For example, the build() method and access() method will not always be called, and not called in the same order, depending on how the block is being rendered. That allows Drupal to render blocks out-of-order for better performance and caching.

    In rare cases, a block may have a good reason to ensure that its methods are called in a predictable order. Generally that is because data that is computed in the access() method is also used in the build() method, and is cached in a property for performance. Such blocks should implement this interface to signal to Drupal that they are not safe to call out-of-sequence.

    ----

    And now that I think about it, what (good) reason is there for a block to even do that at all? If it's properly written, then both build() and access() would sub-call to the same method, and that method would generate the value if needed. That way if they're called independently of each other, the data still gets generated as needed. Or is the issue deeper than that?

  3. +++ b/core/modules/block/src/BlockViewBuilder.php
    @@ -77,23 +61,100 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
    +        $build[$entity_id] += self::buildPreRenderableBlock($entity, $this->moduleHandler());
    

    Normally should use static, no?

  4. +++ b/core/modules/block/src/BlockViewBuilder.php
    @@ -77,23 +61,100 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
    +    return self::buildPreRenderableBlock(Block::load($entity_id), \Drupal::service('module_handler'));
    

    static::, not self::, in pretty much any case I can think of. (self: here means a child class cannot ever override that method.)

    Of course, then I would also ask WTH this is static and not a normal method. :-)

  5. +++ b/core/modules/block/src/BlockViewBuilder.php
    @@ -102,7 +163,7 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
    -  public function buildBlock($build) {
    +  public static function preRender($build) {
    

    Insert obvious criticism here...

  6. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -138,7 +138,8 @@ public function build() {
             // The main content block cannot be cached: it is a placeholder for the
             // render array returned by the controller. It should be rendered as-is,
    -        // with other placed blocks "decorating" it.
    +        // with other placed blocks "decorating" it. Furthermore, it should
    +        // never be turned into a placeholder, because doing so would result
             if ($block_plugin instanceof MainContentBlockPluginInterface) {
               unset($build[$region][$key]['#cache']['keys']);
             }
    

    Then shouldn't MainContentBlockPlugin implement the Stateful interface, since that's the opt-out flag? If not, then it means Stateful is the wrong name for that interface if we can't use it consistently. :-)

wim leers’s picture

StatusFileSize
new19.1 KB
new2.78 KB

#21.4:

#21.5: I wouldn't know how to word that issue; it's a proposal you made — that makes sense at a high level — but I think it'd be better if you create that issue. You'll want that issue anyway, regardless of this one, so :)


#22: many thanks for your review! It's much appreciated!

  1. I agree that "Stateful" is a less than great name. :( It was "NonLazyBuildable" before that. Fabianx preferred "Stateful". Also see next point.
  2. RE: now that I think about it […] properly written […]: totally agreed. That's why we have #2543554: Clean up Help & Statistics blocks, to fix the broken cases. But, there are legitimate use cases too, like the main content block. I already explained in #7 why it was done that way for the main content block: the main content block has a ->setMainContent() method that is necessary for the rendering system to be able to get the main content into the block without relying on state. It also doesn't make sense to placeholder the entire main content.
    This is why I originally called it "NonLazyBuildable".

    RE: your proposed alternative interface docblock: I didn't go with that, because the "out of sequence" part doesn't capture what the problem is. It really is about a block plugin instance that is carrying state. i.e. this line:

      public static function preRender($build) {
        $content = $build['#block']->getPlugin()->build();
    

    When that is called immediately (like we do for stateful block plugins in the current patch), then $build['#block'] is set to the entity that has the plugin instance that contains the state (e.g. the main content injected into the main content block plugin). But, if it is lazily built, then a new block plugin instance is created, which hence doesn't have the state.

    So, to make a first step to address your feedback (I'm sure we'll hash it out further in subsequent comments), I already moved the docs you liked better from BlockViewBuilder to StatefulBlockPluginInterface.

  3. Sorry, yes. Fixed. PHP--
  4. Also fixed. Why static? Already answered in #17, but repeated for your convience: It uses a static method because BlockViewBuilder is not a service, and you can only point to an object if it's also a service. Hence static method.
  5. See previous point. Also: http://drupal4hu.com/node/416.html
  6. It does, because: interface MainContentBlockPluginInterface extends StatefulBlockPluginInterface.
wim leers’s picture

#2543332: Auto-placeholdering for #lazy_builder without bubbling landed. And soft-blocker #2543554: Clean up Help & Statistics blocks landed also!

Lets' get this moving forward again? This has been blocked on reviews for almost a week now. (EDIT: by that I meant: gently nudging Fabianx & Crell :P — whose feedback I replied to in my previous comment and am waiting for their further thoughts so we can work towards consensus.)

dawehner’s picture

Status: Needs review » Needs work

Apparently a bad comment

  1. +++ b/core/lib/Drupal/Core/Block/StatefulBlockPluginInterface.php
    @@ -0,0 +1,26 @@
    + * The interface for blocks that carry state and can hence not be lazily built.
    

    I think non lazy has been worse, because its coupling the name to one specific concept of rendering rather than with stateful, which deals with generic state problems

  2. +++ b/core/modules/block/src/BlockViewBuilder.php
    @@ -77,23 +61,95 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
    +          '#lazy_builder' => [get_class($this) . '::lazyBuilder', [$entity_id, $view_mode, $langcode]],
    

    So get_class() works the same as __CLASS_, but it doesn't take into account late binding, so we should better be able to deal with that. And well, for that case static::CLASS is exactly what you need, see http://3v4l.org/le0Di for an example about that.

  3. +++ b/core/modules/block/src/BlockViewBuilder.php
    @@ -77,23 +61,95 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
    +        get_called_class() . '::preRender',
    

    You can use static::CLASS here as well, see http://3v4l.org/vlA9g

  4. +++ b/core/modules/block/src/BlockViewBuilder.php
    @@ -77,23 +61,95 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
    +    $build['#configuration']['label'] = SafeMarkup::checkPlain($configuration['label']);
    

    So why do we still need that escaping? Don't we have autoescaping now? Can we have a follow up for that? Please also add a todo

  5. +++ b/core/modules/block/src/Tests/BlockViewBuilderTest.php
    @@ -190,57 +190,20 @@ protected function verifyRenderCacheHandling() {
    +    $this->setRawContent((string)$this->renderer->renderRoot($build));
    +    $this->assertIdentical(trim((string)$this->cssSelect('div')[0]), 'Llamas > unicorns!');
    ...
    +    $this->assertIdentical(trim((string)$this->cssSelect('[foo=bar]')[0]), 'Llamas > unicorns!');
    

    Please add the empty strings

  6. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -20,7 +21,7 @@
    -class StatisticsPopularBlock extends BlockBase {
    +class StatisticsPopularBlock extends BlockBase implements StatefulBlockPluginInterface {
     
    

    This is not obvious, why it should be stateful

  7. +++ b/core/modules/views/src/Plugin/Block/ViewsExposedFilterBlock.php
    @@ -16,7 +18,7 @@
    -class ViewsExposedFilterBlock extends ViewsBlockBase {
    +class ViewsExposedFilterBlock extends ViewsBlockBase implements StatefulBlockPluginInterface {
    

    Do you mind adding a comment why? I mean its state depends on the exposed cache contexts which should be basically the entire of url for example

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new17.3 KB

First, a straight reroll now that #2543554: Clean up Help & Statistics blocks has landed, because #23 still contained that. So this patch is now smaller again, like it originally was.

wim leers’s picture

StatusFileSize
new17.57 KB
new2.08 KB
  1. Agreed.
  2. I was merely mimicking other code in core, but sure, done.
  3. Done. Much better :) Thanks!
  4. Filed #2548817: Remove SafeMarkup::checkPlain() in BlockViewbuilder.
  5. ?
  6. Fixed in #2543554: Clean up Help & Statistics blocks.
  7. Good question. We've always used Views' exposed filters block as the canonical example of "a block that depends on the main request, which therefore cannot be rendered via ESI". Perhaps that's no longer true? Can you point to a good test/STR to step through how Views does this, so I can verify the details myself? Documented based on my understanding of how Views deals with this, but likely inaccurate.
dawehner’s picture

?

OH well, I was suggesting to use t((string) $this->renderer->renderRoot($build)); instead of t((string)$this->renderer->renderRoot($build));

fabianx’s picture

Status: Needs review » Needs work
Issue tags: +Needs manual testing, +needs profiling

Found not much, looks great!

Two main concerns:

- hook_block_build_alter() or hook_block_lazy_build_alter() is missing; use cases described below.
- FeedBlock is not taken care of. At least some manual testing would be nice.

One little concern:

- Needs some profiling so we don't regress here (much).

  1. +++ b/core/lib/Drupal/Core/Block/MainContentBlockPluginInterface.php
    @@ -14,7 +14,7 @@
    -interface MainContentBlockPluginInterface extends BlockPluginInterface {
    +interface MainContentBlockPluginInterface extends StatefulBlockPluginInterface {
    

    IIRC, we are still nit-picking on the name. I defer to Crell on that.

  2. +++ b/core/lib/Drupal/Core/Block/StatefulBlockPluginInterface.php
    @@ -0,0 +1,26 @@
    +interface StatefulBlockPluginInterface extends BlockPluginInterface { }
    

    Can we somehow ensure that all blocks that use contexts from e.g. URL extend that class?

    e.g. the FeedBlock?

  3. +++ b/core/modules/block/src/BlockViewBuilder.php
    @@ -77,23 +61,95 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
    -      $this->moduleHandler()->alter(array('block_view', "block_view_$base_id"), $build[$entity_id], $plugin);
    

    I still want my alter hook here.

    hook_block_build_alter().

    Another reason is that its currently impossible to set:

    #create_placeholder => TRUE,

    which would be helpful.

    While for blocks itself this still can be follow-up, it should be added a hook_block_build_alter() here to allow that.

    That makes it possible to set 'auto-placeholdering' in stone and also turn it off for select blocks easily.

  4. +++ b/core/modules/block/src/BlockViewBuilder.php
    @@ -77,23 +61,95 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
    +        $build[$entity_id] += [
    +          '#lazy_builder' => [static::class . '::lazyBuilder', [$entity_id, $view_mode, $langcode]],
    +        ];
    

    Did we measure how much impact that has?

    e.g. some little profiling.

    Especially when e.g. all visible blocks are somehow placeholdered.

wim leers’s picture

Will do the profiling and add the alter hook.

Before doing so, can you clarify this:

Can we somehow ensure that all blocks that use contexts from e.g. URL extend that class?

I have no idea what you mean by that.

fabianx’s picture

#30: There is one sole user of "contexts" in core. The Feed Block.

The Feed Block uses a configured context to select the feed to be shown, if I understood it correctly.

There also is some test coverage that tests contexts for blocks by providing I think a node block showing a node from the URL.

I want to avoid this breaking blocks-with-contexts, when a block-using-context is placeholdered.

I might be wrong and it all works, but lets at least manually test that it is the case.

And if it breaks it lets ensure those blocks use StatefulBlockInterface for now.

wim leers’s picture

From AggregatorFeedBlock:

public function build() {
    // Load the selected feed.
    if ($feed = $this->feedStorage->load($this->configuration['feed'])) {

So this is not at all using contexts.

In fact, the only one that does AFAICT is \Drupal\plugin_test\Plugin\plugin_test\mock_block\MockComplexContextBlock, which exists only for testing purposes. Given that, I'm not entirely sure what you want me to do.

wim leers’s picture

Tim Plunkett pointed me to \Drupal\block_test\Plugin\Block\TestContextAwareBlock, which is the one used in "contexts + blocks" tests.

wim leers’s picture

Assigned: Unassigned » wim leers

And he also pointed me to #2377757: Expose Block Context mapping in the UI, which makes the feed block use contexts indeed. I bet that's why you thought that was the case :)

Working on this next.

andypost’s picture

+++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
@@ -138,7 +138,8 @@ public function build() {
+        // with other placed blocks "decorating" it. Furthermore, it should
+        // never be turned into a placeholder, because doing so would result
         if ($block_plugin instanceof MainContentBlockPluginInterface) {

incomplete comment

fabianx’s picture

To be more clear:

I am neither expecting full support for blocks using context here, nor full coverage, etc. This patch is great as is and way within scope!

I just want to make sure that we either exclude those blocks by default or give an easy way to exclude. And especially that we keep those in mind.

Similar to how we used max-age=0 for some uncacheable things, then later worked on the cacheability in a follow-up.

I guess we need a new word: placeholderability :-p

wim leers’s picture

Issue summary: View changes
StatusFileSize
new27.55 KB
new12.86 KB

#29.1: Blocked on Crell, added to remaining tasks in IS.

#29.2 + 36: postponing handling that to a next comment, until Fabian and I get a chance to chat so we can get on the same page

#29.3: Done. Also vastly expanded the test coverage: now tests not only cache keys and tags, but also contexts and max-age, plus all in combination (keys, contexts, tags, max-age), and also tests #create_placeholder. Added API docs. Ensured the wording is consistent with the existing docs for hook_block_view_alter().

#28, #29.4, #35: Up next.

wim leers’s picture

Status: Needs work » Needs review

d.o--

wim leers’s picture

StatusFileSize
new26.64 KB
new2.55 KB

#28: done.

#35: done.

wim leers’s picture

Issue tags: -needs profiling
StatusFileSize
new28.26 KB
new3.79 KB
new1.1 MB

#29.4: done. In doing so, I discovered two small things that unnecessarily worsened performance:

  1. using \Drupal::service('module_handler') in BlockViewBuilder::viewMultiple()instead of injecting that service; fixed
  2. using Block::load($id) instead of entity_load('block', $id) in the #lazy_builder callback. (Block::load()required a fair bit of magic, and a single call to it resulted in ~70 additional function calls

I profiled both with and without the breadcrumb block marked as cacheable, since #2483183: Make breadcrumb block cacheable is very close, at which point all blocks are cacheable and none are auto-placeholdered.

Note that the wall time is effectively meaningless, there's a lot of variance on there, it is essentially equivalent. In none of the cases, the I/O differs, memory usage is equivalent, so only number of function calls matter.

/contact before vs. after
Run #contact-HEAD Run #contact-patch Diff Diff%
Number of Function Calls 39,289 39,528 239 0.6%
Incl. Wall Time (microsec) 103,833 103,124 -709 -0.7%
Incl. MemUse (bytes) 17,998,176 17,972,256 -25,920 -0.1%
Incl. PeakMemUse (bytes) 18,133,664 18,096,960 -36,704 -0.2%
/contact before vs. after, with cacheable breadcrumb block
Run #contact-cacheable_breadcrumb-HEAD Run #contact-cacheable_breadcrumb-patch Diff Diff%
Number of Function Calls 38,751 38,622 -129 -0.3%
Incl. Wall Time (microsec) 102,416 100,928 -1,488 -1.5%
Incl. MemUse (bytes) 17,830,688 17,825,176 -5,512 -0.0%
Incl. PeakMemUse (bytes) 17,996,352 17,950,240 -46,112 -0.3%
/node/1 before vs. after
Run #node1-HEAD Run #node1-patch Diff Diff%
Number of Function Calls 58,852 59,101 249 0.4%
Incl. Wall Time (microsec) 148,409 143,943 -4,466 -3.0%
Incl. MemUse (bytes) 22,044,952 22,113,360 68,408 0.3%
Incl. PeakMemUse (bytes) 22,157,432 22,209,560 52,128 0.2%
/node/1 before vs. after, with cacheable breadcrumb block
Run #node1-cacheable_breadcrumb-HEAD Run #node1-cacheable_breadcrumb-patch Diff Diff%
Number of Function Calls 58,310 58,181 -129 -0.2%
Incl. Wall Time (microsec) 141,880 142,577 697 0.5%
Incl. MemUse (bytes) 21,963,888 21,972,432 8,544 0.0%
Incl. PeakMemUse (bytes) 22,071,208 22,070,032 -1,176 -0.0%
Conclusion
Building the basic render array for a block is cheaper now. (As expected.)
When all blocks are cacheable (i.e. none are placeholdered), then this has slightly better performance. Because less work needs to be done on a cache hit: see previous point.
When not all blocks are cacheable (i.e. some are placeholdered), then this has slightly worse performance. Because render placeholders means rendering the placeholdered blocks in isolation, which has a small amount of overhead.
Note that with #2469431: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts, that small amount of overhead only occurs after the majority (cacheable parts) of the page has already been sent. This is also what even allows us to render those blocks later, after the initial page is sent.
Effectively, performance remains unchanged.
wim leers’s picture

Title: Convert BlockViewBuilder to use #lazy_builder » Convert BlockViewBuilder to use #lazy_builder (but don't yet let context-aware blocks be placeholdered)

So, this issue completely failed to address one part: Contexts, i.e. context-aware blocks. This is what @Fabianx was getting at in #29.2 & #36.

A. Discussion to find a better name for StatefulBlockPluginInterface

And while discussing #29.1 with Crell — i.e. determining the final name for what is currently called StatefulBlockPluginInteface — that came up too. We were exploring the reasons why a block plugin would need to implement that interface, to find the best possible name. And when we got to the point where we said block plugins that depend on more than purely what they get injected (services & configuration), we arrived at the natural conclusion/question: well, what about contexts, because we get the context manager injected, not specific contexts.
That brought us back to #29.2, and is where we got stuck.

B. Context-aware blocks are a problem

The reason it was easy to forget about Contexts/context-aware blocks is simple: core has zero actual blocks that are context-aware. (There are a few test-only blocks, but those are only tested in non-rendering ways.) That's being fixed slowly in #2377757: Expose Block Context mapping in the UI + #2550199: Add a ContextProvider for Feeds.

The problem with Contexts/context-aware blocks are manifold:

  1. if it is possible to determine for any given blocks what the entire set of contexts is that a block depends on, it's not clear how that is determined (again, because no block actually uses contexts)
  2. AFAIK we have the concept of both required and optional contexts. Knowing just the required contexts won't be sufficient. We need to know all input data for a block, so that includes optional contexts.
  3. Assuming the two points above are not actually a problem, then the next question is: how do we serialize the actual contexts for a given request? The Context API, does not yet support that rather crucial feature. Berdir's proposal:
    optional SerializableContextInterface (because not all context can be serialized, not sure about the name as it could be mixed up with php serialize()), if present, use that and pass it along. if not, no lazy-building for you

    — but then we'd of course also need a way to deserialize the serialized Context back into an actual Context.

(I can't answer points 1 and 2 myself because I can find neither documentation nor test coverage. I'm sure tests exist, but not in the form of integration tests for blocks.)

Conclusion: the Context system is not designed to run in isolation, because if it were, we'd be able to both capture a specific context and recreate it. Neither is currently supported.

This is why @Fabianx said this in #36:

I am neither expecting full support for blocks using context here, nor full coverage, etc. This patch is great as is and way within scope!

I just want to make sure that we either exclude those blocks by default or give an easy way to exclude. And especially that we keep those in mind.

Similar to how we used max-age=0 for some uncacheable things, then later worked on the cacheability in a follow-up.

C. Context-aware blocks are actually not a problem

… at least not in the SingleFlush and BigPipe render strategies. (SingleFlush is the render strategy that Drupal has used forever: build the entire HTML response and then send it when it's completely ready.)

Because when we replace placeholders in both SingleFlush and BigPipe render strategies, we are still operating within the actual/original request context. Which means that blocks will actually be able to access the Contexts they need.

For an ESI render strategy, however, this breaks down. Because with ESI, the rendering of the placeholder happens in an ESI request, not in the actual/original request. Which means Contexts won't be available.

Conclusion: in the current situation wrt Contexts + Blocks, we are explicitly relying on a leaky abstraction, on imperfect isolation. Because the context system is not designed to be able to run in isolation (see earlier). And, that's even fine for SingleFlush and BigPipe, because those only render blocks in the actual/original request context.

D. Context-aware blocks: able to render them in isolation when using ESI

We need two things to truly be able to render context-aware blocks in isolation:

  1. Ability to serialize and deserialize Contexts.
  2. If the block itself varies by route or url (or url.*, e.g. url.query_args:foo), then the only way to render it in isolation is to do routing. Which means the ESI client needs to send the actual request URL to the origin, so that the origin (Drupal) can run routing. Drupal would get a X-Original-Request-Uri header.

This can be done in a follow-up.

Proposed next steps

(Issue title updated according to the proposed next steps.)

  • In this issue: If a block has contexts (detect using !empty($block->getPlugin()->getPluginDefinition()['context'])) => make it non-placeholderable by setting #create_placeholder => FALSE.
  • In a follow-up issue, only set #create_placeholder => FALSE if not all contexts can be serialized.
wim leers’s picture

This comment and reroll are merely an unfortunate distraction. The next reroll will simplify the patch.


In a testing issue where I was trying something to simplify this patch further, I discovered that the last green patch (in #40) is no longer passing in HEAD. Specifically, \Drupal\system\Tests\Form\RedirectTest::testRedirectFromErrorPages() is failing (that test was introduced by #2206909: Regression: Form submit redirects from 403/404 pages are no longer possible, >1.5 year ago).

It's failing because it is testing that a form in a block (as opposed to a block in the main content area) is able to do a redirect on a 403/404 page (i.e. on a page where the kernel chooses what to render based on a kernel exception), and this patch automatically placeholders that block, which causes the redirect to occur later.

The root cause of the problem is that forms do redirects like this:

    // If the form returns a response, skip subsequent page construction by
    // throwing an exception.
    // @see Drupal\Core\EventSubscriber\EnforcedFormResponseSubscriber
    //
    // @todo Exceptions should not be used for code flow control. However, the
    //   Form API does not integrate with the HTTP Kernel based architecture of
    //   Drupal 8. In order to resolve this issue properly it is necessary to
    //   completely separate form submission from rendering.
    //   @see https://www.drupal.org/node/2367555
    if ($response instanceof Response) {
      throw new EnforcedResponseException($response);
    }

i.e. a pre-existing bit of nastiness in HEAD.

The reason it was passing in #40, is because #2551989: Move replacing of placeholders from HtmlRenderer to HtmlResponseAttachmentsProcessor got committed in the mean time, which causes blocks to be rendered later, during the kernels' RESPONSE event. Which means that when the EnforcedResponseException exception finishes bubbling, it does not trigger a kernel exception, because we're already in the response event.

Whereas in #40/earlier HEAD, a nested kernel exception occurs (first a 403/404, then the EnforcedResponseException within the handling of that 403/404 exception).

In other words, this small interdiff fixes a bug in #2551989: Move replacing of placeholders from HtmlRenderer to HtmlResponseAttachmentsProcessor, which only is an actual problem because Form API works in this crazy backward way :/


Attached is a reuploaded (identical, just rebased) #40 patch that is expected to fail, and a new patch with the fix (see interdiff).

The last submitted patch, 42: auto-placeholdering-block_lazy_builder-2543340-40-fail.patch, failed testing.

wim leers’s picture

Alright, the fail patch failed as expected, the other patch passed as expected. Great.

Now on to the next reroll, which will simplify this patch again.

wim leers’s picture

Issue summary: View changes
StatusFileSize
new26.82 KB
new4.23 KB

So, a key part that we got stuck on earlier, is the naming of this new StatefulBlockPluginInterface. But, the only examples we have are the main content block, and the Views exposed filters block.

We always thought the latter is directly dependent on "the main View" for the page (i.e. the one that's the main content, i.e. generated by the route's controller). But a closer look at ViewsExposedFilterBlock made me doubt that. So, I stepped through it. And, indeed, the Views exposed filters block in Drupal 8 no longer depends on the main content at all :) It has its own, non-executed ViewExecutable that is constructed independently. So, hurray! This is great!

That means the only block with special needs is the main content block. And that block already is special.

Conclusion: we can remove StatefulBlockPluginInterface! A whole layer of complexity is avoided. When the need arises in contrib, we can still add something like this, with more wisdom and experience based on those real-world examples that we encounter — if any.

Patch & IS updated accordingly.

Status: Needs review » Needs work

The last submitted patch, 45: auto-placeholdering-block_lazy_builder-2543340-45.patch, failed testing.

wim leers’s picture

StatusFileSize
new26.86 KB
new1.09 KB

#45 contains a random fail. Easy fix.

wim leers’s picture

So, now that we have a simpler, green patch, we can focus on the remaining problem: context-aware blocks. See #41 for a full analysis.

First, I needed my understanding/analysis of context-aware blocks confirmed. Tim Plunkett confirmed in chat that:

  1. A block's context annotation indeed lists all contexts it may ever use:
    Tim Plunkett [21:26] those could be optional, those could include ones that are only used in certain code paths, but yes they would be in there
    Wim Leers [21:27] So a Block's `context` annotation lists all contexts it may *ever* consume, even if it does not consume them always? (edited)
    Wim Leers [21:27] (I want to make 100% sure I understand it correctly.)
    Tim Plunkett [21:28]  yes
    
  2. One can thus find a block's contexts by doing !empty($block->getPlugin()->getPluginDefinition()['context'])?
    Tim Plunkett [21:29] ContextAwarePluginInterface::getContextDefinitions does
       $definition = $this->getPluginDefinition();
       return !empty($definition['context']) ? $definition['context'] : array();
    Tim Plunkett [21:29] so if ($block->getPlugin() instanceof ContextAwarePluginInterface && $block->getPlugin()->getContextDefinitions())
    

    … so it can be done even more elegantly than I anticipated :)

  3. One thing I failed to mention in #41, but realized later, is the case of "configured contexts" or "static contexts" (not sure what the official term is, I think I've read both): if all contexts that a block could ever consume are specified in context_mapping, then the block effectively is being rendered in full isolation.
    Tim Plunkett [21:37]  it's basically like $used_contexts =  getContextMapping() + getContextDefinitions(). if everything in the definitions is in the mapping, then the mapping is the entire resulting set
    Tim Plunkett [21:37] see \Drupal\Core\Plugin\Context\ContextHandler::applyContextMapping
    

Conclusion: we can easily make sure that blocks which depend on contexts that aren't defined in context_mapping will get #create_placeholder = FALSE set, to prevent them from being rendered outside the main request.

wim leers’s picture

Status: Needs work » Needs review

I forgot to set #47 to NR. Such noob.

wim leers’s picture

Issue tags: -Needs manual testing
StatusFileSize
new26.99 KB
new2 KB

(Before reading this comment, please read #41.)


  1. #48 explains/confirms that we can in fact make context-aware blocks opt out.
  2. Drupal core only supports SingleFlush, and may support BigPipe (see #2469431: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts). Drupal core can only support those two render strategies/HTML delivery mechanisms, because they both don't require additional infrastructure. ESI does require additional infrastructure.
  3. As #41's section C explains, in both of those cases (SingleFlush & BigPipe), any and all rendering does happen in the original request context. Which means that Contexts work just fine.
  4. Therefore, there is in fact no need for Drupal 8 core to prevent Context-aware blocks from being placeholdered. That will only be necessary when ESI is used — which will happen on a tiny minority of Drupal sites. Therefore we can put the responsibility on the contrib ESI module to prevent Context-aware blocks from being placeholdered, by letting its hook_block_build_alter() set '#create_placeholder' => FALSE.
  5. The expanded test coverage in this patch already asserts that hook_block_build_alter() implementations can set or override #create_placeholder (but currently it only tests the TRUE case, for absolute peace of mind it should also test the FALSE case; done in this reroll).

Conclusion: this patch is ready; it doesn't need to do any further work to deal with Contexts/Context-aware blocks. Placeholdering of Context-aware blocks works fine for >99% of sites (no ESI), and for the remaining <1% (ESI) there is test coverage to ensure that the contrib ESI module can prevent Context-aware blocks from being placeholdered. The ability to serialize and unserialize Contexts is therefore only necessary for this <1% of sites, and can easily be an optional interface added in Drupal 8.x.0.


Let's explain why BigPipe is sufficient, and ESI is not a great fit for most sites anyway.

BigPipe will be sufficient for 99%, because ESI will only be actually useful on pretty advanced infrastructure setups: the setup must cache the ESI fragments in the ESI client (Varnish or something else), otherwise they'll end up bootstrapping Drupal for every ESI request. Not to mention that Varnish (and other, but not all) ESI clients in fact perform ESI requests serially, not in parallel. So your page that is cached in the ESI client and which just needs N blocks rendered in Drupal is then waiting for N Drupal bootstraps plus rendering, plus network overhead/latency.

That's the beauty of BigPipe: a single bootstrap, no network involved, no additional infrastructure required.

fabianx’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +needs profiling

#50 Two little corrections:

a) ESI is planned for core in 8.1.x or 8.2.x (but not for 8.0.0).

b) There is still the possibility of using a middleware near page cache level to achieve an ESI like effect in core (essentially smart cache at a higher up level).

That all only works because smart cache is sitting after routing or rather it wraps the controller callback, which means that nothing can be happening in-between in comparison to a 'real' request.


That all said, the only reason for that exercise and excursion into 'blocks-with-contexts' land was to ensure that we don't break things for those cases. (not solve all the problems in here).

It seems we don't and the alter hook allows to easily set TRUE / FALSE the placeholder flag, so that is great.


  1. +++ b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
    @@ -103,7 +104,18 @@ public function processAttachments(AttachmentsInterface $response) {
    +    try {
    +      $response = $this->renderPlaceholders($response);
    +    }
    +    catch (EnforcedResponseException $e) {
    +      return $e->getResponse();
    +    }
    

    Hmmmm.

    While this makes a lot of sense for the test case, I think the better fix is to ensure forms-in-blocks are not placeholderable by implementing the alter hook.

    On the other hand this will only become a problem with BigPipe and a very apparent problem then, so I think it is fine to leave here and to re-discuss #create_placeholder_options in the BigPipe issue, which should fail in the same test then.

  2. +++ b/core/modules/block/block.api.php
    @@ -127,6 +127,61 @@ function hook_block_view_BASE_BLOCK_ID_alter(array &$build, \Drupal\Core\Block\B
    +    $build['#contexts'][] = 'user';
    ...
    +  $build['#create_placeholder'] = TRUE;
    

    Love it!


I already reviewed this several times and it looks ready to me now.

Setting RTBC, but also adding 'Needs profiling'.

Could we get some base performance numbers though - to see what the impact of the additional indirection is?

We for sure can gain 100% of that back, but it would still be good to know.

wim leers’s picture

Issue tags: -needs profiling

For profiling, see #40. :)

fabianx’s picture

Ohhh, I totally missed #40.

That is great and exactly what I wanted to see.

Fantastic that we even save some time on cache hits and good that the overhead is not that much worse for an auto-placeholdered block.

Crell’s picture

Wow. WimLeers++!

catch’s picture

Status: Reviewed & tested by the community » Fixed

Have looked through this a couple of times and it looks good, also discussed the issue a bit with Wim in irc.

Committed/pushed to 8.0.x, thanks!

  • catch committed 18bb5ab on 8.0.x
    Issue #2543340 by Wim Leers, Fabianx: Convert BlockViewBuilder to use #...
wim leers’s picture

Status: Fixed » Closed (fixed)

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