Problem/Motivation

BEFORE COMMENTING ON THIS ISSUE, PLEASE READ THE ISSUE SUMMARY OF #2469431: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts!

A requirement for #2469431: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts is to have placeholders. This is a child issue of that one to bring the single most crucial concept for BigPipe to Drupal 8 core — quoting myself at #2469431-40: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts:

I think it's crucial that we first flesh out the "placeholders" part of this patch. If we agree that we that we can remove #post_render_cache in favor of the conceptually simpler "placeholders", then I think we could file an issue for doing just that and mark it as a blocker for this issue, but it would be a DX improvement regardless.

Fabianx & yched +1'd that.

Goal + requirements analysis

  1. Read the issue summary of #2469431: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts.
  2. We want BigPipe in Drupal 8 core, because of the enormous performance boost. If you haven't seen it yet, watch the first 10 minutes of this video for a demo — hopefully that will convince you. #2469431: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts is the issue for that.
  3. "BigPipe" is just a new "render strategy". Drupal 8's current render strategy is "SingleFlush": we build the entire response, then send it all at once (i.e. single flush). BigPipe is about sending the non-dynamic (typically cacheable) parts of the page first (first flush), and then rendering the expensive, dynamic (typically uncacheable) things afterwards, and every time another part is rendered, we send that part (second, third, etc. flushes). "ESI" would be another render strategy. And so on.
  4. What is key about these render strategies is that they want to be able to deliver portions of the page (fragments, if you will) in different ways. E.g. let's imagine we have a hyper-dynamic block that shows the request time, the IP address, the current user, and a the number of page views in the current session. In the case of the SingleFlush render strategy, we want that block to be delivered as part of the HTML that is sent. In the case of BigPipe, we don't want it to be part of the first flush, but want it in the second flush.
  5. Some fragments are known to be dynamic (e.g. this crazy block I used as an example, or just the "status messages" block), but others are not: due to complex access checking, it's possible that a certain block can be part of the first BigPipe flush on site X, but is so dynamic on site Y that it cannot be in the first BigPipe flush (and thus needs to be in one of those after).
  6. The ability to deliver portions of the page requires Drupal to be able to A) choose to render a portion as part of the first flush (non-dynamic) or delay it until a later time, B) know where these portions are, C) how to render them. Point A is what we call "auto-placeholdering": based on whether something is too dynamic or not, let Drupal automatically render it into a placeholder, with associated bubbleable metadata to be able to replace it at a later time (using one of the render strategies).

Why isn't #post_render_cache enough?

  1. Drupal 8 HEAD already has a concept/abstraction capable of replacing placeholders: #post_render_cache.
  2. But it is not able to know which the placeholders are, and is utterly incapable of "auto-placeholdering". Without auto-placeholdering, we suffer from two problems: A) we burden developers with needing to know to use #post_render_cache, which has a very painful DX, B) and for those that do use #post_render_cache, they will always use placeholders, not only when it is necessary (i.e. when the contents are indeed very dynamic/uncacheable).
  3. On top of that, many current usages of #post_render_cache are now misguided and adversely impact performance. Key example: entity_embed. This is a D8 contrib module that will very likely see enormous, highly prominent use in the Drupal 8 ecosystem. It uses #post_render_cache to be able to do entity access checking, because entity access checks could be highly dynamic — e.g. per user — and #post_render_cache was the only solution for that until recently. But, recently we fixed #2099137: Entity/field access and node grants not taken into account with core cache contexts, which is an analogous problem to the one that entity_embed faces. It was possible to fix that thanks to #2429257: Bubble cache contexts. And so, thanks to #2429257, an entire class of problems that D8 was only able to fix using #post_render_cache can now be fixed without it! Consequently, entity_embed will no longer have to render each embedded entity on every page load, which is an enormous performance improvement. So: all current #post_render_cache callbacks in D8 contrib MUST be re-evaluated. (The issue for Entity Embed: #2496363: Replace #post_render_cache with #lazy_builder and use cache context bubbling.)
  4. So, we in are a situation where we can see, smell and taste the sweet smell of an enormous performance win, but we are lacking the right concept. We have a concept close to it, but all usages of that concept already need to be re-evaluated anyway. It seems therefore acceptable to break BC and completely remove the old concept.

Proposed resolution

  1. Introduce #lazy_builder callbacks.
    1. Receives the same arguments as #post_render_cache callbacks do today, and with actual documentable arguments instead of a single $context argument for the callback.
    2. But without the "build $build, do its own rendering of $build, generate placeholder markup, find the placeholder markup in $element['#markup'], bubble metadata from $build onto $element" misery — instead simply: "build $build and return it.
    3. In other words: 99% of #post_render_cache callback logic can be retained, you only have to delete code, and make the function signature sane :) See the change record.
  2. Introduce #create_placeholder
    1. When set to #create_placeholder = TRUE, and if a #lazy_builder is present, then it behaves like #post_render_cache does today.
    2. In a follow-up patch (see the parent issue), we will add auto-placeholdering. i.e. based on the bubbled cacheability metadata, automatically set #create_placeholder to TRUE.
  3. Which brings us to the last : to make your complex/advanced/potentially uncacheable render arrays available to this system, you only have to convert #pre_render callbacks to #lazy_builder callbacks, and the system will be able to optimize it automatically (i.e. choose when to lazily build this!).
    Plus, "lazy builder" makes a hell of a lot more sense than "pre render" when writing code that builds render arrays: rather than sounding like a strange edge casey thing, it sounds like something you indeed want to do as soon as you're building something complex (like for any software/code), so it's much more encouraging to use than #pre_render.
  4. Note: This ability to have the same callback be able to "build out" the render array (i.e. build the subtree) is very similar to #pre_render callbacks. But #pre_render callbacks don't expect to work in isolation; they are designed to "decorate" an existing render array. So, we initially called it #pre_render_cache, what we now call #lazy_builder. Rationale being: "it's a #pre_render callback that can run in isolation, much like #post_render_cache, which meant we could just map any #pre_render_cache callback to become a #post_render_cache callback". But after many discussions and iterations, we ended up with #lazy_builder, because that term best embodies 1) the fact that this builds a render array rather than decorates it (#pre_render), or merely runs at a certain stage (#post_render_cache), 2 the laziness (only build the render array (sub)tree when necessary, i.e. during regular rendering or afterwards).
  5. So, the Renderer, when it encounters an element with #create_placeholder === TRUE && isset(#lazy_builder), it replaces that element in its entirety with placeholder markup, plus bubbleable metadata that informs Drupal about the placeholder markup that it should replace. So, conceptually:
    $element = [
      '#lazy_builder' => ['Class::renderCommentForm', ['arg1', 'arg2'],
      '#create_placeholder' => TRUE,
    ];
    

    to:

    $element = [
      '#markup' => '<drupal-render-placeholder STUFF></drupal-render-placeholder>',
      '#attached' => [
        'placeholders' => [
          '<drupal-render-placeholder STUFF></drupal-render-placeholder>' => [
            '#lazy_builder' => ['Class::renderCommentForm', ['arg1', 'arg2'],
          ],
        ],
      ],
    ];
    
  6. Then, when the entire HTML page is rendered, we have HTML markup for the entire page, but with those placeholders still in there. And at that point, depending on how the site is configured, Drupal 8 would use either the SingleFlush or the BigPipe render strategy, to replace those placeholders. In this patch, that continues to be the SingleFlush strategy, because render strategies are handled by the parent issue.

See the new general recommendation.

Remaining tasks

None.

User interface changes

None.

API changes

  1. Added #lazy_builder callbacks, which are given only arguments that may contain only primitive types (string, int, bool, float, NULL), not objects.
  2. Added #create_placeholder, which allows a render array to be forced to be placeholdered.
    In a follow-up issue, automatic placeholdering will be introduced. (Basically: if max-age = 0 or high-cardinality cache contexts are present for the subtree, set #create_placeholder = TRUE.)
  3. Removed #post_render_cache (the above is its successor).
  4. Removed RendererInterface::generateCachePlaceholder() and drupal_render_cache_generate_placeholder() because they are now obsolete.

A new general recommendation

  • A #lazy_builder can build a render array from scratch. It is given only the arguments that you provide it, and that context may contain only primitive types (string, bool, int, float, NULL).
  • When specifying #cache, provide a #lazy_builder to build out the entire render array in case of a cache miss.
  • Together, the two above points allow Drupal to optimize: it is able to determine based on how dynamic the contents of the #builder callback are, whether the #lazy_builder (and thus the associated render array) should be rendered during the initial page load, or via BigPipe or ESI.

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Task because not technically a bug, but definitely a blocker to having BigPipe in Drupal 8 core.
Issue priority Major because blocks BigPipe.
Prioritized changes The main goal of this issue is performance, as well as significantly improving DX.
Disruption Disruptive for core/contributed and custom modules/themes because it will require a BC break: #post_render cache is removed. In favor of #lazy_builder (which is easy to transition to, thanks to similar semantics).
This is a hard BC break, but for excellent reasons: way easier to understand, BigPipe support, automatic placeholdering. Developers won't have to learn about #post_render_cache complexities, they are simply encouraged to use #lazy_builder instead of #pre_render, and Drupal 8 will automatically determine the best time to render the render array returned by the #lazy_builder callback.
Note that the most important D8 contrib users (entity_embed, masquerade) of #post_render_cache is already switching away from it, now that we have cache context bubbling. See #2496363: Replace #post_render_cache with #lazy_builder and use cache context bubbling + #2448699: Implement caching for the masquerade block and links with a 'masquerade' cache context.
CommentFileSizeAuthor
#97 interdiff.txt13.2 KBwim leers
#97 placeholders-2478483-97.patch159.87 KBwim leers
#88 interdiff.txt979 byteswim leers
#88 placeholders-2478483-88.patch158.79 KBwim leers
#87 interdiff.txt5.79 KBwim leers
#87 placeholders-2478483-87.patch158.8 KBwim leers
#84 interdiff.txt53.93 KBwim leers
#84 placeholders-2478483-84.patch156.59 KBwim leers
#80 interdiff.txt9.37 KBwim leers
#80 placeholders-2478483-80.patch151.03 KBwim leers
#76 interdiff.txt1.23 KBwim leers
#76 placeholders-2478483-76.patch143.95 KBwim leers
#75 interdiff.txt10.66 KBwim leers
#75 placeholders-2478483-75.patch143.44 KBwim leers
#74 interdiff.txt14.64 KBwim leers
#74 placeholders-2478483-74.patch142.82 KBwim leers
#72 interdiff.txt14.33 KBwim leers
#72 placeholders-2478483-72.patch139.6 KBwim leers
#63 interdiff.txt9.04 KBwim leers
#63 placeholders-2478483-63.patch134.52 KBwim leers
#55 interdiff.txt45.83 KBwim leers
#55 placeholders-2478483-55.patch134.29 KBwim leers
#54 failed-attempt2-to-stop-passing-render-array-to-builder-callbacks--interdiff.txt1.54 KBwim leers
#54 failed-attempt1-to-stop-passing-render-array-to-builder-callbacks--interdiff.txt15.65 KBwim leers
#54 interdiff.txt2.57 KBwim leers
#54 placeholders-2478483-54.patch133.63 KBwim leers
#51 interdiff.txt12 KBwim leers
#51 placeholders-2478483-51.patch133.66 KBwim leers
#44 interdiff.txt42.78 KBwim leers
#44 placeholders-2478483-44.patch131.69 KBwim leers
#40 interdiff.txt2 KBwim leers
#40 placeholders-2478483-40.patch131.9 KBwim leers
#39 interdiff.txt2 KBwim leers
#39 placeholders-2478483-39.patch131.9 KBwim leers
#38 interdiff.txt2.46 KBwim leers
#38 placeholders-2478483-38.patch133 KBwim leers
#37 interdiff.txt18.78 KBwim leers
#37 placeholders-2478483-37.patch133.44 KBwim leers
#36 interdiff.txt2.75 KBwim leers
#36 placeholders-2478483-36.patch123.58 KBwim leers
#35 interdiff.txt28.02 KBwim leers
#35 placeholders-2478483-35.patch122.16 KBwim leers
#33 interdiff.txt18.91 KBwim leers
#33 placeholders-2478483-33.patch112.81 KBwim leers
#31 interdiff.txt6.35 KBwim leers
#31 placeholders-2478483-31.patch98.22 KBwim leers
#30 interdiff.txt1.12 KBwim leers
#30 placeholders-2478483-30.patch93.74 KBwim leers
#28 interdiff-commits.txt88.58 KBwim leers
#28 interdiff.txt56.56 KBwim leers
#28 placeholders-2478483-28.patch94.32 KBwim leers
#25 interdiff.txt9.02 KBwim leers
#25 placeholders-2478483-25.patch46.67 KBwim leers
#24 interdiff.txt15.1 KBwim leers
#24 placeholders-2478483-24.patch38.27 KBwim leers
#22 interdiff.txt8.09 KBwim leers
#22 placeholders-2478483-22.patch24.65 KBwim leers
#21 interdiff.txt2.27 KBwim leers
#21 placeholders-2478483-21.patch32.65 KBwim leers
#17 interdiff.txt10.88 KBwim leers
#17 introduce_placeholders-2478483-17.patch33.93 KBwim leers
#8 introduce_placeholders-2478483-8.patch35.74 KBfabianx
#8 introduce_placeholders-2478483-8--conversions.txt19.31 KBfabianx
#8 introduce_placeholders-2478483-8--placeholder-only.patch16.69 KBfabianx

Comments

wim leers’s picture

Issue tags: +API change, +API addition
wim leers’s picture

Assigned: Unassigned » wim leers

Working on this.

fabianx’s picture

Assigned: wim leers » fabianx

We decided that I will roll an initial MVP patch for this and that we use a symmetric system, where the placeholdering is still set explicitly right now.

wim leers’s picture

Assigned: fabianx » Unassigned

Discussed with Fabianx; I understand what needs to happen, but Fabianx will take this on, as he knows much better what needs to happen, rather than merely understanding it :)

wim leers’s picture

Assigned: Unassigned » fabianx

LOL @ d.o

I unassigned myself, and d.o didn't even tell me that it was assigned to somebody else in the mean time!

wim leers’s picture

fabianx’s picture

Title: Remove #post_render_cache in favor of "placeholders" » Introduce placeholders to replace #post_render_cache

Here we go with a first patch, but first a more patch friendly title.

fabianx’s picture

Okay, first patch:

- The minimum placeholders support (--placeholders-only.patch)
- Converted all #post_render_cache usages in core to placeholders, including a hack to make BubbleableMetadata create a placeholder instead of a thing to show that it is equivalent and conversions are simple. (--conversions.txt)

As can be seen the DX improves in all cases and as long as you have a render array that is cacheable (e.g. no objects, etc.) you can easily create a placeholder by just doing:

$elements['#markup'] = 'my-unique-string';
$elements['#cache']['placeholders']['my-unique-string'] = [
  '#pre_render_cache' => [ 'myid' => '123' ],
];

even manually.

You can also use the old drupal_generate_cache_placeholder() if you want to as long as you combine it with #pre_render_cache.

Note:

#pre_render_cache itself is a concept that works as follows:

- #pre_render_cache is called only when #pre_render is not set. (we could change that however and have #pre_render callbacks check for that itself).

The reason is to support the following use-case:

- If you already have an entity and done the trouble of loading it, it would be better to stick it in directly, so you don't have to do the loading twice on a cold-cache.

Though entities are likely a bad example as those are cached statically per request anyway ...

So the idea was that you could have:

$render_array = [
  '#pre_render_cache' => [
    $function => $context,
  ],
  '#pre_render' => [
    $some_function,
  ],
  '#cache' => [
    'keys' => ['some-keys'],
  ],
  '#property' => $someObject,
];

and #pre_render_cache can be used to re-create #pre_render, #property, etc. but that #cache and #pre_render_cache are stored together in the render cache array.

fabianx’s picture

Self-Review with comments:

  1. +++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
    @@ -43,6 +50,7 @@ class BubbleableMetadata extends CacheableMetadata {
    +      $result->placeholders = NestedArray::mergeDeep($this->placeholders, $other->placeholders);
    

    This could use a normal (and faster) array_merge, because placeholders are unique as they are hash based.

  2. +++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
    @@ -57,6 +65,7 @@ public function merge(CacheableMetadata $other) {
    +    $build['#cache']['placeholders'] = $this->placeholders;
    

    I am still pondering whether #cache placeholders or #render_placeholders.

    Pro #render_placeholders:

    In this patch placeholders do not have much to do with caches or the cache system and are explicitly set with #cache_placeholder => TRUE and #pre_render_cache.

    Contra #render_placeholders:

    If we want to support non-explicit placeholdering, we need to support bubbling cases, which means we need to store the information that we placeholder somewhere, which is stored in the render cache layer.

    Also as not everything can be dynamically re-created, but could still be cached, the render cache system needs to be placeholder-aware.

  3. +++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
    @@ -134,7 +144,14 @@ public function getPostRenderCacheCallbacks() {
    +    $placeholder = \Drupal::service('renderer')->generateCachePlaceholder($callback, $context);
    +    $this->placeholders[$placeholder] = [
    +      '#pre_render_cache' => [
    +        $callback => $context,
    +      ],
    +      // @todo Ensure tests can pass.
    +      '#render_strategies' => ['single_flush' => 'single_flush'],
    +    ];
    

    This shows how a previous #post_render_cache can be converted to placeholders, if it uses the generateCachePlaceholder pattern ...

    #render_strategies are not needed here and only here as I use this patch on top of the BigPipe one for testing purposes, too.

  4. +++ b/core/lib/Drupal/Core/Render/Element/StatusMessages.php
    @@ -100,10 +88,8 @@ public static function generatePlaceholder(array $element) {
       public static function renderMessages(array $element, array $context) {
    -    $renderer = static::renderer();
    -
         // Render the messages.
    -    $messages = [
    +    $element['messages'] = [
           '#theme' => 'status_messages',
           // @todo Improve when https://www.drupal.org/node/2278383 lands.
           '#message_list' => drupal_get_messages($context['display']),
    
    @@ -113,24 +99,7 @@ public static function renderMessages(array $element, array $context) {
         ];
    -    $markup = $renderer->render($messages);
    -
    -    // Replace the placeholder.
    -    $callback = get_class() . '::renderMessages';
    -    $placeholder = $renderer->generateCachePlaceholder($callback, $context);
    -    $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']);
    -    $element = $renderer->mergeBubbleableMetadata($element, $messages);
     
         return $element;
    

    This is a good example to show that a conversion removes code and improves DX.

    In most cases using a subkey of $element and returning that is enough to convert a #post_render_cache callback to #pre_render_cache + #cache_placeholder.

    Its also pretty similar to how #pre_render itself works, just that the context is not in $elements, but in $context.

  5. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -105,6 +105,16 @@ public function renderPlain(&$elements) {
    +  public function renderPlaceholder($placeholder, &$elements, $is_root_call = FALSE) {
    +    $elements['#cache_no_placeholder'] = TRUE;
    +    return $this->render($elements, $is_root_call);
    +  }
    

    It is important to provide render strategies with means to render a placeholder without it being placeholder'ed again.

    As the logic to check $elements['#cache_placeholder'] == TRUE/FALSE got very complicated I used the cache_placeholder / cache_no_placeholder pattern instead.

    This is less evident in this patch, but more in the all-in-logic of the bigpipe patch.

    Basically:

    For every element need to specify that a placeholder should never be created - regardless if #cache_placeholder is TRUE or FALSE.

    It might be possible to just use one property though, where it being empty is 'auto-detect'.

  6. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -168,10 +178,16 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    if (isset($elements['#cache_placeholder']) && isset($elements['#pre_render_cache']) && !isset($elements['#cache_no_placeholder'])) {
    +      $elements = $this->createRenderCacheArrayPlaceholder($elements);
    +    }
    

    It is important to only create a placeholder if #pre_render_cache and #cache_placeholder are set.

    This allows the 'auto-detection' follow-up.

    Here also the check for #cache_no_placeholder is used.

  7. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -197,6 +213,16 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    // Ensure the item we want to render is loaded.
    +    if (!isset($elements['#pre_render']) && isset($elements['#pre_render_cache'])) {
    

    This is where #pre_render_cache is called if #pre_render is not yet set, so the #pre_render_cache can populate #pre_render.

    This was mainly to ease conversion of the BlockViewBuilder.

    It is likely not needed in general, lets discuss it.

  8. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -355,6 +382,11 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    // All data is now in $elements, so discard stack as cacheSet could change
    +    // this. @todo Simplify this by introducing a new helper.
    +    static::$stack->pop();
    +    static::$stack->push(new BubbleableMetadata());
    
    @@ -364,6 +396,14 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    // We've rendered this element (and its subtree!), now update the stack.
    +    $this->updateStack($elements);
    +
    

    By contract cacheSet can change $elements and the metadata on $elements, the same is true for later processPlaceholders calls (though those need to live in a loop similar to post render cache).

    So for cacheSet to be able to do whatever it wants, the stack is emptied (as all data is in $elements) and a new empty stack frame is pushed upon it.

    Then after the processing the stack is updated again.

    The reason is that updateStack does two things:

    1) It updates elements with the stack data.
    2) It updates the stack with the elements data.

    So the data is redundantly stored in both places, but if cacheSet was to change assets for whatever reason then that data would be out of sync.

    This change prevents this.

  9. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -485,6 +525,54 @@ protected function processPostRenderCache(array &$elements) {
    +   // @todo Use hash_hmac with the salt instead?
    +   $placeholder_keys[] = hash('sha1', serialize($render_cache_array));
    

    Probably should set an attribute in the $render_cache_array so that placeholders can be easily identified.

    Might need to use a hash_hmac so that no-one can 'guess' a placeholder. (Not sure what the security implications of that are, but was asked for the messages part.)

  10. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -485,6 +525,54 @@ protected function processPostRenderCache(array &$elements) {
    +   $placeholder_id = '<drupal-render-cache-placeholder data-placeholder-id="' . implode('-', $placeholder_keys) . '"></drupal-render-cache-placeholder>';
    +
    

    Uses the same markup for now, this is later replaced by the rendering strategies with their ESI tags, [divs], etc.

fabianx’s picture

Conversion comments:

  1. +++ b/core/modules/comment/src/CommentLinkBuilder.php
    @@ -202,16 +202,27 @@ public function buildCommentedEntityLinks(FieldableEntityInterface $entity, arra
    +          // @todo Consider using drupalSettings directly plus hook_js_settings_alter().
    +          $entity_links['comment__' . $field_name]['history'] = [
    +            '#pre_render_cache' => [
    +              'history_attach_timestamp' => ['node_id' => $entity->id()]
    +            ],
    +            '#cache_placeholder' => TRUE,
    +            // @todo Big Pipe does not yet support #attached.
    +            '#render_strategies' => ['single_flush'],
    +          ];
    

    Its entirely possible to do this without using placeholders at all, which is a little strange as those placeholders will always be empty.

    Similar to how a library can add empty data during hook_library_info_alter() this could just do:

    $elements['#attached']['drupalSettings']['history_attach_timestamp']['node_id'][$entity->id()];

    and then use hook_js_settings_alter() to add the specific things.

    Train of thought

    However, the placeholdered approach is more effective, because it allows caching in higher up levels and would lead to that this information is e.g. automatically retrieved via AJAX, while the page is cached in Varnish ...

  2. +++ b/core/modules/comment/src/CommentLinkBuilder.php
    @@ -202,16 +202,27 @@ public function buildCommentedEntityLinks(FieldableEntityInterface $entity, arra
    +          $entity_links['comment__' . $field_name]['attach_new_comments_link_metadata'] = [
    +            '#pre_render_cache' => [
    +              'comment.post_render_cache:attachNewCommentsLinkMetadata' => [
    +                'entity_type' => $entity->getEntityTypeId(),
    +                'entity_id' => $entity->id(),
    +                'field_name' => $field_name,
    +              ],
    +            ],
    +            '#cache_placeholder' => TRUE,
    +            // @todo Big Pipe does not yet support #attached.
    +            '#render_strategies' => ['single_flush'],
    +          ];
    

    This example shows how a conversion from #post_render_cache to #pre_render_cache can be almost done automatically.

  3. +++ b/core/modules/comment/src/CommentPostRenderCache.php
    @@ -112,19 +112,13 @@ public function renderForm(array $element, array $context) {
         $comment = $this->entityManager->getStorage('comment')->create($values);
    -    $form = $this->entityFormBuilder->getForm($comment);
    -    $markup = $this->renderer->render($form);
    -
    -    $callback = 'comment.post_render_cache:renderForm';
    -    $placeholder = $this->generatePlaceholder($callback, $context);
    -    $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']);
    -    $element = $this->renderer->mergeBubbleableMetadata($element, $form);
    +    $element['form'] = $this->entityFormBuilder->getForm($comment);
     
         return $element;
       }
    

    And for #post_render_cache placeholders we have really nice DX again :).

wim leers’s picture

Status: Needs review » Needs work

#8:

As can be seen the DX improves in all cases and as long as you have a render array that is cacheable (e.g. no objects, etc.) you can easily create a placeholder by just doing:

$elements['#markup'] = 'my-unique-string';
$elements['#cache']['placeholders']['my-unique-string'] = [
  '#pre_render_cache' => [ 'myid' => '123' ],
];

even manually.

Strong -1 on manually creating placeholders. It's too easy to create a not-actually-unique placeholder string.

#9.3:

Pro #render_placeholders:

In this patch placeholders do not have much to do with caches or the cache system and are explicitly set with #cache_placeholder => TRUE and #pre_render_cache.

Exactly. Plus, placeholders are not cacheability metadata, but bubbleable rendering metadata. Therefore this new metadata should not live under #cache. Basically, this should replace BubbleableMetadata::postRenderCachePlaceholders.

#9.5: interesting, so you're saying that that code exists to prevent placeholder recursion? Then 1) that is not at all clear from the code or comments, 2) *why* is it impossible to do recursion?

#9.8: I still don't understand this. Can you include a concrete example in the code comments, using a render as the concrete example?


Review of the conversion:

  1. +++ b/core/modules/comment/src/CommentViewBuilder.php
    @@ -143,14 +143,11 @@ public function buildComponents(array &$build, array $entities, array $displays,
    +          '#cache_placeholder' => TRUE,
    
    @@ -162,9 +159,14 @@ public function buildComponents(array &$build, array $entities, array $displays,
    +          '#cache_placeholder' => TRUE,
    ...
    +          '#render_strategies' => ['single_flush'],
    

    Seeing these, I think this DX would make more sense:

    '#placeholder' => TRUE,
    

    and then, optionally, one can specify which render strategy to use:

    '#placeholder' => 'single_flush',
    
  2. +++ b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestPostRenderCache.php
    @@ -32,12 +32,13 @@ public function process($text, $langcode) {
    +    // @todo Should we keep BC?
         $result->addPostRenderCacheCallback($callback, $context);
    

    I don't think so, I know of only one filter that actually uses this already (https://github.com/drupal-media/entity_embed)


Reviewing the "placeholders only" patch:

  1. --- a/core/lib/Drupal/Core/Render/BubbleableMetadata.php
    +++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
    

    This should still remove the post_render_cache stuff.

  2. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -197,6 +213,16 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    // Ensure the item we want to render is loaded.
    

    This comment is confusing.

  3. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -485,6 +525,54 @@ protected function processPostRenderCache(array &$elements) {
    +   $placeholder_id = '<drupal-render-cache-placeholder data-placeholder-id="' . implode('-', $placeholder_keys) . '"></drupal-render-cache-placeholder>';
    

    I think both "single flush" and "BigPipe" can use this markup, right?

    It's only for ESI that we need custom mark-up, I think?

fabianx’s picture

Per quick discussions we are gonna do:

- Use #render_placeholder and #render_placeholder_strategy_hints as names
- Store the placeholders in ['#attached']['placeholders'] as they are truly out-of-band data and you should not attach placeholders yourself, also the format is suitable for the #attached merging we use.

yched’s picture

['#attached']['placeholders']++, nice :-)

Had a cursory n00b look at the patch, I had the impression we're not completely consistent as to what we call "placeholder" in the var names, method names, code comments : the placeholder string, or the $element to render in its place, or the combination of both.
Probably no big deal, but precise naming could really help make the code easier to follow IMO :-)

+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -105,6 +105,16 @@ public function renderPlain(&$elements) {
+  public function renderPlaceholder($placeholder, &$elements, $is_root_call = FALSE) {
+    $elements['#cache_no_placeholder'] = TRUE;
+    return $this->render($elements, $is_root_call);
+  }

Looks like this step doesn't actually need the $placeholder param ?
(--> in the spirit of the above, accurate name is renderPlaceholderElement() ?)

fabianx’s picture

#13: Yes, maybe to be consistent ['#attached']['render_placeholders']

++ to renderPlaceholderElement
++ to using #render_placeholder => FALSE in above function

However the knowledge of which placeholder string this is rendering will be needed in the future, so I would like the API to support it.

The reason being that - once we have auto-placeholdering - when something takes 3s to create, but then it turns out to be not cacheable, we need to placeholder it, but that would mean to take 3s again. Therefore the BigPipe patch had a static for-this-request cache to store data about already generated PlaceholderElement markup.

fabianx’s picture

Assigned: fabianx » wim leers

Wim is continuing with this.

dawehner’s picture

... its hard to even understand if things aren't described what this issue is remotely about.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new33.93 KB
new10.88 KB

As a first step, only doing #12.

Will fail with 99% probability because testbot is currently broken.

Status: Needs review » Needs work

The last submitted patch, 17: introduce_placeholders-2478483-17.patch, failed testing.

hass’s picture

fabianx’s picture

#19: Yes, it is a part of it.

To really fix this, we will also need to render CSS and JS in its own "rendering context" instead of render it after all #post_render_cache / #placeholders had already been processed.

I am still working out the details on how this will work best.

wim leers’s picture

StatusFileSize
new32.65 KB
new2.27 KB

To get back into this, I tested all scenarios that are being updated in this patch. And found that the comment.module's node links don't work.

I'm removing the changes to the comment module's node links. Because that is broken in HEAD already. Being fixed at #2496399: Comment module's node links' front-end perf-optimizing drupalSettings are missing. This allows the patch to remain focused on the problem at hand, and not fixing already-broken things.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new24.65 KB
new8.09 KB

#17 moved ['#cache']['placeholders'] to ['#attached']['placeholders']. The unit test expectations weren't updated for that yet. This fixes that, which reduces the number of test failures to close to zero, but don't let yourself be fooled, this is nowhere near ready yet.

In the next reroll, removing #post_render_cache bubbling/support. I won't change the code restored in #21, but that won't hinder this patch, because that is effectively dead code at the moment anyway; once #2496399: Comment module's node links' front-end perf-optimizing drupalSettings are missing is fixed, we'll start seeing failures, and then we can fix it.

Status: Needs review » Needs work

The last submitted patch, 22: placeholders-2478483-22.patch, failed testing.

wim leers’s picture

StatusFileSize
new38.27 KB
new15.1 KB

Here's a first round of removing a fair bit of #post_render_cache. Notably BubbleableMetadata is now completely free of it.

Keeping at "needs work" because surely the number of failures will increase. Just posting several patches before posting a "needs review" patch, so that the individual interdiffs are manageable.

Also: the (get|add|set)Assets() -> (get|add|set)Attachments() change and the deprecation of the former can and should happen in a child issue. That also includes the bug in HEAD in addAssets(). It's a pre-existing problem that's just made more apparent now that #attached can contain placeholders.


Continuing this work now.

wim leers’s picture

StatusFileSize
new46.67 KB
new9.02 KB

In HEAD, #post_render_cache callbacks are able to completely wipe the rendered markup. It is up to the #post_render_cache callback itself to only replace the part that it must replace, i.e. the placeholders.

With this patch, doing that becomes impossible: because the return value of the (#pre_render_cache) callback now automatically replaces the placeholder in the rendered markup, and only the placeholder.

Next: making RendererPostRenderCacheTest pass. Then all Renderer unit test coverage is green again, and I'll move back to "needs review".

Crell’s picture

With this patch, doing that becomes impossible: because the return value of the (#pre_render_cache) callback now automatically replaces the placeholder in the rendered markup, and only the placeholder.

Are you saying that as a good thing or a bad thing? It sounds like a good thing to me, but I want to make sure we're on the same page about that. :-)

wim leers’s picture

#26: Good thing, absolutely :) It just made it quite mind-bending to update the tests to 1) pass, 2) still test what we want it to test :)

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new94.32 KB
new56.56 KB
new88.58 KB

Worked all day long to get RendererPostRenderCacheTest to pass as well, and now it does! :)


The next tricky thing was this: in HEAD, the results of a #post_render_cache callback can never ever be render cached. But with this patch, that changes — intentionally.
For example, imagine a block is rendering some static text and an entity. If the entity's access says that is can only be cached per user then we want to be able to render that entity as a placeholder, so that it can be replaced later. This means we can render cache the block, but have that block then at the last moment replace the placeholder with the entity. We're good so far, but here comes the new aspect.
We also want to be able to render cache that entity, on a per-user basis, so that we don't have to render it on every page load (we don't want to do this automatically though, we'll only do it if ['#cache']['keys'] is set, so no need to worry there).
In other words: we want the very render array that was rendered into a placeholder to also be render cacheable, so that when placeholders are replaced, that doesn't have to mean rendering from scratch every time.
Given the simple example of having 1000 pages and 1000 users: HEAD would create 1000*1000 variations. With this approach, we would create 1000+1000 variations. That's 500 times better (and thus assuming perfectly distributed requests, a 500 times better cache hit ratio).

The reason this was tricky: HEAD's test coverage relied on the fact that when the same element had both cache keys and a post-render cache callback, that the render cache item always contained the placeholder. Given the above, that's no longer true. But we can still achieve the same test coverage by doing something similar to the "block containing an entity" example: make the parent (block) have cache keys and the child (entity) use a placeholder, then test that the parent always contains a placeholder.


By using data providers and passing different ways of using placeholders (generated or manually created, and cacheable or uncacheable) to the various test scenarios, I was able to hugely simplify the unit test, and at the same time have better test coverage and already pave the way for auto-placeholdering's test coverage.

Thanks to this, this is now a net-negative patch ( 22 files changed, 588 insertions(+), 695 deletions(-)), which is a nice benefit/side-effect :)

In doing this work, I've also cleaned up the changes to Renderer, and the placeholder processing logic should be in a near-final form. Thanks to added code comments, it should now be significantly easier to understand. The rest will still undergo clean-up. And of course, documentation needs to be improved.


This is still not yet completely done/cleaned up, but all Renderer unit tests do pass now! More work to follow. This is not yet in a reviewable state. (Except for Fabianx, perhaps — see interdiff-commits.txt for the individual local commits that lead to this interdiff.) Keeping assigned to me.

Status: Needs review » Needs work

The last submitted patch, 28: placeholders-2478483-28.patch, failed testing.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new93.74 KB
new1.12 KB

Removing the exception that I've been using while rolling this patch should fix quite a few failures.

Now continuing the actual work again.

wim leers’s picture

StatusFileSize
new98.22 KB
new6.35 KB

Not in its final form yet, but here's an initial update to the Filter module.

The last submitted patch, 30: placeholders-2478483-30.patch, failed testing.

wim leers’s picture

StatusFileSize
new112.81 KB
new18.91 KB

Convert all remaining #post_render_cache uses and references… with the exception of the ones for comment links that are broken in HEAD; those will be migrated once #2496399: Comment module's node links' front-end perf-optimizing drupalSettings are missing lands.

This hence includes fixes for the last failing tests. This should be the first green patch!

Now we enter the final stages: the next patch will be a clean-up patch, to clean up oddities in the current patch. And then we begin the real finalization process: self-reviews and reviews will be done to get this patch to an RTBC state.

The last submitted patch, 31: placeholders-2478483-31.patch, failed testing.

wim leers’s picture

StatusFileSize
new122.16 KB
new28.02 KB

Most notably cleans up \Drupal\Tests\Core\Render\RendererPlaceholdersTest (formerly called \Drupal\Tests\Core\Render\RendererPostRenderCacheTest). But really cleans up all Renderer unit tests: not a single trace of #post_render_cache left in them!

wim leers’s picture

Issue summary: View changes
StatusFileSize
new123.58 KB
new2.75 KB

And now the removal of all references to #post_render_cache is done, with the exception of:

  1. The stuff in the comment module that is broken in HEAD and is blocked on #2496399: Comment module's node links' front-end perf-optimizing drupalSettings are missing. Added a remaining task for that to the IS.
  2. drupal_render_cache_generate_placeholder() in common.inc and RendererInterface

Next: addressing point 2, at which point the removal of all references will be complete for now.

wim leers’s picture

Issue summary: View changes
StatusFileSize
new133.44 KB
new18.78 KB

As promised, #36.2 now addressed: all references to #post_render_cache now removed!

Changes in this reroll:

  • Removed RendererInterface::generateCachePlaceholder() and drupal_render_cache_generate_placeholder() because they are now useless.
  • Cleaned up the Renderer's creation of placeholders and invoking of #pre_render_cache callbacks, to be more consistent with existing code.
  • Renamed public function createRenderCacheArrayPlaceholder() to protected function createPlaceholder(). As far as this patch is concerned, this does not need to be public. The auto-placeholdering/render strategy/BigPipe patch can still make this a public API, if truly necessary. No need to do that here.
  • RendererInterface docs updated
  • FilterProcessResult no longer uses RendererInterface::generateCachePlaceholder() and therefore no longer has a hidden service dependency.

This is now partially ready for review, now doing the work to make it completely ready for review :)

wim leers’s picture

Issue summary: View changes
StatusFileSize
new133 KB
new2.46 KB

Based on discussions with @Fabianx, we concluded that "render placeholder" (#render_placeholder) is too ambiguous. It's not clear if it means
"render a placeholder, as in replace it", or
"render a placeholder, as in generate placeholder markup"

Hence the following improvements in this reroll:

  • #render_placeholder -> #create_placeholder
  • protected function Renderer::processPlaceholders() -> protected function Renderer::replacePlaceholders()
  • removed #render_placeholder_strategy_hints, because the concept of "render strategies" are completely out of scope here; it's up to that issue to add them
wim leers’s picture

StatusFileSize
new131.9 KB
new2 KB

Minimizing the changes in StatusMessages. There were a bunch of unnecessary changes in there.

wim leers’s picture

Issue summary: View changes
StatusFileSize
new131.9 KB
new2 KB

In this reroll: #pre_render_cache is renamed to #builder.

Rationale: see the relevant portions of an IRC discussion with @Fabianx below:

16:29:09 WimLeers: Here's a bigger thought.
16:29:49 WimLeers: Even though "#post_render_cache" was a Renderer concept, it was understandable for Filters as well, because the name was abundantly clear: it's for things that must NOT be render cached.
16:29:56 WimLeers: That clarity is lost as we move to #pre_render_cache
16:30:38 WimLeers: Because we'll have to document that "if a filter wants to do something very dynamic, it'll have to call FilterProcessResult::createPlaceholder($callback, $context) and make sure that $callback is a #pre_render_cache callback.
16:30:39 WimLeers: "
16:30:41 WimLeers: That's quite the WTF.
16:30:51 WimLeers: Furthermore, #post_render_cache ran at a predictable time
16:30:58 WimLeers: but #pre_render_cache does not, that's its entire purpose!
16:31:13 WimLeers: it may run BEFORE render cache (and thus its results may be render cached) of AFTER (results not render cached)
16:31:17 WimLeers: So the name is quite confusing
16:31:25 WimLeers: So what about introducing a new/better name?
16:32:50 WimLeers: I suggest #builder callback.
16:32:53 WimLeers: Then the docs could be:
16:32:56 WimLeers: "A #builder is able to add any additional children and #properties to a render array. And to allow Drupal to optimize (i.e. do as little work as possible), #builder callbacks may only be given primitive type parameters."
16:33:23 WimLeers: That is much more logical, and doesn't impose a lot of (advanced) Renderer knowledge on the developer.
16:33:26 WimLeers: s/impose/require
16:33:56 WimLeers: And, equally important: "builder" does not indicate at all whether it will run before or after render caching. Implying that Drupal will run it when necessary.
16:34:13 WimLeers: i.e. no confusion/doubt is caused in the developer, hence (s)he doesn't even start to think when it runs
16:34:27 WimLeers: because it'll Just Work as expected, and only the most advanced developers will have to make changes
16:34:39 WimLeers: *will have to do the work to learn how it works
16:34:44 WimLeers: *how it is called
16:35:03 WimLeers: Not doing this just yet, but I'm already at least half convinced myself
16:36:14 WimLeers: And as a final bonus: this also makes #builder much more attractive than #pre_render
16:36:19 WimLeers: Because #builder doesn't sound scary.
16:36:57 WimLeers: We can then just give the following advice: "When specifying #cache, keep your render array as tiny as possible, and provide a #builder to build out the entire render array in case of a cache miss."
16:37:13 WimLeers: So then the total docs become:

- "A #builder is able to add any additional children and #properties to a render array. And to allow Drupal to optimize (i.e. do as little work as possible), #builder callbacks may only be given primitive type parameters."

- "When specifying #cache, keep your render array as tiny as possible, and provide a #builder to build out the entire render array in case of a cache miss."
16:37:15 WimLeers: Very comprehensible.
16:48:29 Fabianx-screen: Almost
16:48:39 Fabianx-screen: But I like it very much.
…
16:49:25 Fabianx-screen: The most important thing about #pre_render_cache + #builder is that you don't get any additional render array properties.
16:49:28 Fabianx-screen: e.g.
16:50:05 Fabianx-screen: [ '#builder' => [ 'func' => ['foo'] ], '#myproperty' ] does not work.
…
16:50:24 Fabianx-screen: As all parameters should be in #builder itself.
…
16:50:58 Fabianx-screen: Nothing additional.
16:51:03 Fabianx-screen: Just #builder + #cache
…
16:51:09 Fabianx-screen: No other properties are saved.
…
16:51:37 Fabianx-screen: #builder is fine, just your docs do not imply that important part.
wim leers’s picture

Issue summary: View changes

Added beta evaluation.

yched’s picture

How does #builder play (or clashes) with #after_build ?

wim leers’s picture

#42: #after_build is a pure Form API concept; it exists only in FormBuilder, not in Renderer. There's no clashing or playing.

First, FormBuilder does its thing, including executing #after_build callbacks. It returns a render array. That's the controller result. The MainContentViewSubscriber detects a render array and uses the Renderer to render the form (and blocks etc) and turns it into a page.

wim leers’s picture

StatusFileSize
new131.69 KB
new42.78 KB

LOL, the patch + interdiff in #40 were the same as those in #39. So ignore the files in #40, but do read the comment.

effulgentsia’s picture

#after_build is a pure Form API concept; it exists only in FormBuilder, not in Renderer. There's no clashing.

But $form and form element arrays are also render arrays, so there might be a conceptual clash between #builder and the "build" terminology of FAPI: e.g. #after_build. What about renaming to #generator or similar?

effulgentsia’s picture

Or even, #lazy_builder / #lazy_generator?

berdir’s picture

Haven't reviewed yet, just want to point out that entity_embed is not the only contrib module that uses #post_render_cache. Poll uses it as well since poll forms and especially the results (and whether the form or the results should be displayed) is very much per-user. And payment users it as well, to display a form in a field, very similar to comment.module.

And masquerade seems to use it as well.

wim leers’s picture

Issue summary: View changes

#47: true. Masquerade is also switching away from it though. Sounds like Poll will indeed continue to use #post_render_cache in the form of placeholders.

I'm merely stressing in the IS that quite a few of the early D8 contrib modules can be made simpler, because they no longer have to use placeholders, they can just use cache contexts in quite a few cases.

Good feedback though, thanks :)

fabianx’s picture

I like #lazy_builder, it also works nice with the plan to have a PreparableLazyBuilderInterface (where you have prepare() and build() functions, so we can support placeholders with e.g. preloading of entities).

xano’s picture

I use #post_render_cache in contrib to display a form in a field formatter.

I have no time to go through the entire patch today (and maybe not tomorrow). Could someone post a small example (or point it out in the patch) of how calling code should implement this change?

wim leers’s picture

StatusFileSize
new133.66 KB
new12 KB

I had a call with @effulgentsia yesterday, walking him through the patch. Remarks he had:

  • A #builder callback is for building a render array, whereas a #pre_render callback is for decorating an existing render array. The docs should stress this difference.
  • Consequently, there should also be only a single #builder callback. Otherwise, two #builder callbacks might generate the same child, and we wouldn't know how to merge them. So, if a #builder still wants to spread responsibilities across multiple functions, it should either invoke hooks, or use multiple decorating #pre_render callbacks.
  • Also consequently, if a #builder callback is encountered but there are children (Element::children()), then a helpful exception should be thrown.

All implemented.

yched’s picture

FYI, #2496039: Formatter's #attached assets are not carried over by Views is introducing another mention of #post_render_cache (not an actual use, just an isset check), will need to be adjusted depending of which patch gets in first.

Also, comment #9 over there has a question/suggestion for @Wim / @Fabianx :-)

andypost’s picture

quickly skimming a patch I'm wondering:

- reason for #create_placeholder = true - is there a case then it false? better DX would be not require people to provide TRUE each time

- name #builder- post_render_builder looks more descriptive

Overall it looks like basic replacement for post render cache so why break all 8.x code?
Maybe there's other way to preserve BC or build that on top of existing "post_render_cache"?

  1. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -105,12 +107,44 @@ public function renderPlain(&$elements) {
    +    // Make sure the same placeholder is not created again; that'd lead to an
    +    // infinite loop.
    +    $placeholder_elements['#create_placeholder'] = FALSE;
    ...
    +    // A #builder callback may create new placeholders (i.e. recursive
    +    // placeholders), but not at the root level, because that would again lead
    +    // to an infinite loop.
    +    if (isset($placeholder_elements['#create_placeholder']) && $placeholder_elements['#create_placeholder'] === TRUE) {
    +      throw new \Exception('Endless placeholdering loop detected. #builder callbacks called during the placeholder phase cannot set #create_placeholder on the root of the given element');
    +    }
    

    explains the need in the key but maybe there's other way to check recursion?

  2. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -205,15 +239,50 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    // First validate the usage of #builder; both of the next if-statements use
    +    // it if available.
    +    if (isset($elements['#builder'])) {
    +      if (count($elements['#builder']) > 1) {
    +        throw new \DomainException('Only a single #builder callback can be specified.');
    +      }
    +      if (Element::children($elements)) {
    +        throw new \DomainException('When a #builder callback is specified, no children can exist; all children must be generated by the #builder callback.');
    +      }
    +    }
    ...
         // Make any final changes to the element before it is rendered. This means
         // that the $element or the children can be altered or corrected before the
         // element is rendered into the final text.
    +    // #builder is recommended, but #pre_render is still supported: we don't
    +    // want to break backwards compatibility.
    +    if (isset($elements['#builder'])) {
    +      if (count($elements['#builder']) > 1) {
    +        throw new \DomainException('Only a single #builder callback can be specified.');
    +      }
    +      if (Element::children($elements)) {
    +        throw new \DomainException('When a #builder callback is specified, no children can exist; all children must be generated by the #builder callback.');
    +      }
    +      foreach ($elements['#builder'] as $callable => $context) {
    +        if (is_string($callable) && strpos($callable, '::') === FALSE) {
    +          $callable = $this->controllerResolver->getControllerFromDefinition($callable);
    +        }
    +        $elements = call_user_func($callable, $elements, $context);
    +      }
    

    so #builder checked twice?
    looks copy/paste...

  3. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -205,15 +239,50 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    // If instructed to create a placeholder, and a #builder callback are
    +    // present (without such a callback, it would be impossible to replace the
    +    // placeholder), replace the current element with a placeholder.
    +    if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === TRUE && isset($elements['#builder'])) {
    +      $elements = $this->createPlaceholder($elements);
    +    }
    

    looks #builder is a requirement to create placeholder, so why not relay on it presence?

  4. +++ b/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php
    @@ -206,14 +206,11 @@ public function viewElements(FieldItemListInterface $items) {
    -            $placeholder = drupal_render_cache_generate_placeholder($callback, $context);
                 $output['comment_form'] = array(
    -              '#post_render_cache' => array(
    -                $callback => array(
    -                  $context,
    -                ),
    +              '#builder' => array(
    +                 $callback => $context,
                   ),
    -              '#markup' => $placeholder,
    +              '#create_placeholder' => TRUE,
                 );
    

    Example code shows that all implementations could be updated directly - for @Xano

wim leers’s picture

StatusFileSize
new133.63 KB
new2.57 KB
new15.65 KB
new1.54 KB

On that same call with @effulgentsia, we also discussed whether #builder callbacks should even receive $elements. I unfortunately don't remember the details/our joint conclusion of that. The main (but only!) trickiness is how to deal with the #cache set on the original element, but that's easy enough to merge onto the render array returned by the #builder callback!

So, I tried (failed-attempt1-to-stop-passing-render-array-to-builder-callbacks--interdiff.txt), and that's how I found out the hard way why this doesn't work: because restoring #cache means restoring #cache[keys], which means that it is impossible to render cache the results, because the cache keys will only be available *after* the render cache has been checked.

So, instead, here's improved documentation.

Having done written #51, and then having written the above made me realize something: because we still expect the original #cache to be persisted, in its current shape, #builder is only half-assed: because it persists #cache, it actually has part of the "decorating" aspect of #pre_render. And it is only for that bit that we're doing this.
So, let's make this both simpler and more consistent: if the render array built by #builder should be cacheable, then #cache should just be set by #builder. Period.

I then spent considerable timing doing this, only to notice that this too was flawed (failed-attempt2-to-stop-passing-render-array-to-builder-callbacks--interdiff.txt): if we always call #builder, then we still cannot actually have render cache hits! Sigh :(

Conclusion: #pre_render is decorating an existing render array, and #builder is building a render array. But #builder does start with #cache set (and nothing else), precisely to allow for render cache hits to still happen.

(Also fixed a small bit of duplication introduced in #51.)

wim leers’s picture

StatusFileSize
new134.29 KB
new45.83 KB

Per #45, #46, #49: renaming #builder to #lazy_builder.

Other feedback: thanks! Will address all of it in my next comments!

dawehner’s picture

Should there be some example documentation somewhere in the patch to explain how to use #builder callbacks?

  1. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -105,12 +107,44 @@ public function renderPlain(&$elements) {
    +    $placeholder_elements = $elements['#attached']['placeholders'][$placeholder];
    

    I'm curious is #attached really the right place for placeholder storage?

  2. +++ b/core/modules/comment/src/CommentPostRenderCache.php
    @@ -89,7 +89,7 @@ public function __construct(EntityManagerInterface $entity_manager, EntityFormBu
    -   * #post_render_cache callback; replaces placeholder with comment form.
    +   * #builder callback; replaces placeholder with comment form.
    

    I like the term #builder here

  3. +++ b/core/modules/filter/src/FilterProcessResult.php
    @@ -116,4 +120,47 @@ public function setProcessedText($processed_text) {
    +  /**
    +   * Creates a placeholder.
    +   *
    ...
    +  public function createPlaceholder($callback, array &$context) {
    

    It would be great to explain why we need a custom placeholder for filters ...

wim leers’s picture

#50: answered by the CR that I just created: https://www.drupal.org/node/2498803

fabianx’s picture

Train of thought

We have three cases of render arrays to distinguish:

  • a) "Input Render Arrays": Only: #builder + #cache => Builder in the ideal case is a service and can fully construct render arrays with all context being injected and hence dependencies are specified
  • b) "Process Render Arrays" #any-properties-you-want, used for actually rendering
  • c) "Output Render Arrays / Render Cache Arrays": [ '#markup' ] + [ '#attached' ] + [ '#cache' ] // Pure value array (similar to a value object / DTO (Data Transport Object))

I like that builder works in the way that it just returns the build render array, it does not even need to be able to change #cache in anyway.

If it _really_ needs a different cache layer, it should just use a child with a cache key, the same for more placeholders.

--

The advantage of one builder callback is:

- Can use services with injected dependencies and hence be called e.g. from a Middleware if no dependency on e.g. current request
- Service can have an interfaces to make multiple preparation possible (e.g. ->prepare() and ->build() on a list of items with the same builder)


#53:

Why is the API change needed?

Post Render Cache means you can do _everything_ you want to markup and that is a problem, because there is a world after Drupal, too.

e.g. if you want to cache things per role in Varnish and you have a post render cache that adds all unique user information, then that is pretty much impossible to resolve.

In essence you could still do anything that post render cache did in a custom response subscriber, but we encourage you to not do it.

While Post Render cache was mainly used to create placeholders in core, it can theoretically be used for anything and that is why its not possible to keep it around as else we lie over our goals.

Also with placeholders you give core much more possibilities to run the "placeholder callback" at the best suitable time.

See the demo at the beginning here:

https://events.drupal.org/losangeles2015/sessions/making-drupal-fly-fast...

wim leers’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

#52: thanks for the note. Posted a review/suggestion on that issue, which would remove that mention of #post_render_cache.


#53: Basically all your questions point to the fact that the IS is severely lacking. The IS should be answering all of those questions. So, instead of answering your questions directly, I've updated the IS. You'll find the necessary answers there. It's a relatively long read, but I promise it will be worth it :)

  1. Agreed that that is ugly/confusing, I also don't like it. Hoping to get rid of that.
  2. Yep, copy/paste error, already fixed in #54 :) I noticed it too!
  3. Explained in the IS: auto-placeholdering — if not placeholdered (!isset(#create_placeholer) || #create_placeholder == FALSE), this would run early during the rendering (same phase as #pre_render callbacks — since they're conceptually similar), if placeholdered (#create_placeholder = TRUE), this would run after the rendering of the HTML, using SingleFlush or BigPipe.
  4. Yep, already answered @Xano too. See the CR.
effulgentsia’s picture

Priority: Major » Critical

Raising to critical per #1744302: [meta] Resolve known performance regressions in Drupal 8's guideline of Over ~10ms savings with warm caches and would have to be deferred to 9.x. The necessity of the BC break of #post_render_cache satisfies the latter part, and #2469431: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts is very likely to achieve the former part but requires this issue first.

The last submitted patch, 51: placeholders-2478483-51.patch, failed testing.

wim leers’s picture

StatusFileSize
new134.52 KB
new9.04 KB

#56:

  1. See #12 + #13, where this direction was chosen. Personally, I think it makes a whole lot of sense. Asset libraries are attachments similar reasons: they are metadata about the HTML that somehow needs to make it into the HTML at the last moment. The same applies to placeholders.
  2. :) Since renamed to #lazy_builder! Also see IS update and recent comments about that for more rationale.
  3. Done.

#53.1: I actually already had a way to get rid of that, because during the code was refactored to be clearer/more consistent/simpler, and in doing so, I made it impossible to hit the infinite recursion case!
Details: when rendering the placeholder, #create_placeholder === FALSE always, which means we don't create a placeholder, then we execute the #lazy_builder, which even if it sets #create_placeholder at the root level is simply ineffective.


Also cleaned up/improved other exceptions + associated tests.

wim leers’s picture

There are a few more small changes lined up, but that's just further clean-up. I will do so tomorrow.

This is basically ready for review now!

effulgentsia’s picture

This patch looks great to me. Noticed some minor nits, but holding off on those pending #64. My only non-minor nits:

A #lazy_builder is given a render array with only #cache

Why? Why not remove that arg and only pass $context?

#lazy_builder callbacks may only be given primitive type parameters.

Where is that enforced? If nowhere yet, can we add an exception?

wim leers’s picture

#65

  • Feel free to post nits already! I don't have many nits lined up, only small bits of refactoring.
  • Answered by my notes in #54 — short version: because without #cache being already set (and specifically, #cache[key]s), it's impossible for the contents of what is placeholdered itself to be cached. (And yes, that's super valuable: imagine the entire page being cached across users with roles A and B, but one block being per-user — it's valuable to be able to render cache that block per user, so that it doesn't need to be re-rendered for every page load — what matters is that the block didn't infect the entire page!)
  • Nowhere yet, just like it wasn't enforced for #post_render_cache either.

Status: Needs review » Needs work

The last submitted patch, 63: placeholders-2478483-63.patch, failed testing.

effulgentsia’s picture

#66.2: Per top of #58, can't we document and enforce that the builder callback is not allowed to return an element with #cache['keys'] set at the root level, and that instead that property may only be set on the original declaration? I'm still unclear on why the callback itself needs the original element declaration passed in. Meanwhile, I think cache tags and contexts would work just fine when set either (or both) in the original declaration and/or within the callback: can't the renderer merge those without needing to pass them to the builder callback?

#66.3: Yes, but this issue is about breaking the BC of #post_render_cache. Let's not have to break it twice.

effulgentsia’s picture

#66.1:

  1. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -205,15 +232,61 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +      foreach ($elements['#lazy_builder'] as $callable => $context) {
    

    Since this is required to be single, what about changing it from array to #lazy_builder as the callable and #lazy_builder_context for the context? Or maybe even drop the "context" terminology and turn it into #lazy_builder_arguments and use call_user_func_array instead of call_user_func?

  2. +++ b/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php
    @@ -197,7 +197,7 @@ public function viewElements(FieldItemListInterface $items) {
                 $callback = 'comment.post_render_cache:renderForm';
    

    After applying this patch, the codebase still has a bunch of "post_render_cache" terminology left in it, like here. Let's clean that up.

  3. +++ b/core/modules/views/src/Tests/Plugin/CacheTest.php
    @@ -288,7 +288,7 @@ function testHeaderStorage() {
    +    $this->assertEqual(['non-existing-placeholder-just-for-testing-purposes' => ['#lazy_builder' => ['views_test_data_pre_render_cache' => ['foo' => 'bar']]]], $output['#attached']['placeholders']);
    

    "pre_render_cache" is leftover terminology from earlier versions of this patch. Let's clean that up.

effulgentsia’s picture

+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -460,36 +518,85 @@ protected function bubbleStack() {
+    // Generate placeholder markup.
+    $attributes = new Attribute();
+    $callback = array_keys($placeholder_render_array['#lazy_builder'])[0];
+    $args = $placeholder_render_array['#lazy_builder'][$callback];
+    // Also generate a unique placeholder token if it doesn't exist already,
+    // to prevent collisions.
+    $args += ['placeholder_token' => Crypt::randomBytesBase64(55)];
+
+    $value = $callback;
+    $query = UrlHelper::buildQuery($args);
+    if ($query) {
+      $value .= '?' . $query;
     }
+    $attributes['callback'] = $value;
+    $placeholder_markup = '<drupal-render-cache-placeholder' . $attributes . '></drupal-render-cache-placeholder>';

Why do we need/want the args (other than the uniqueness token) in the placeholder markup? AFAICT, we don't later parse it from there, so looks to just be extra useless bytes. If there's a reason, such as supporting replacement from a reverse proxy, or to aid debugging, please add a comment about that.

wim leers’s picture

Issue summary: View changes

Applied @Fabianx's IS improvement suggestions.

wim leers’s picture

Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new139.6 KB
new14.33 KB

#68
RE #66.2: answering this in my next comment.
RE #66.3: fair! Done. (Fun fact: PHP calls these "scalars", not "primitives", and strangely, NULL is not considered a scalar… so I ended up with is_null() || is_scalar().)


#69:
  1. I'm fine with that. The thing I dislike most about the current patch is how we use the same data structure pattern as all other callbacks (yay), but that being a mismatch with what you can do (booh!). So strongly agree that a solution for that would be great. Pinging @Fabianx to hear what he thinks about that.
    Not sure about "arguments" instead of "context". I discussed that with Fabian:
    18:14:10 WimLeers|focus: Are we certain about s/$context/$args/?
    18:14:24 WimLeers|focus: The problem is that the arguments still need to be manually extracted
    18:14:29 WimLeers|focus: though I guess you can do extract($args)
    18:14:38 WimLeers|focus: but then $context may actually be a better name
    18:14:58 WimLeers|focus: not sure
    18:15:01 WimLeers|focus: Thoughts?
    18:15:48 Fabianx-screen: $context
    18:16:18 WimLeers|focus: $context is better, you say?
    18:16:42 Fabianx-screen: yes
    18:16:51 Fabianx-screen: extract($content)
    18:16:55 WimLeers|focus: ack
    18:16:55 Fabianx-screen: extract($context)
    18:16:57 WimLeers|focus: y
    18:16:58 Fabianx-screen: makes more sense.
    18:17:00 WimLeers|focus: indeed
    

    If we would be able to use call_user_func(), then it'd make sense, yes, but how could we do that, without using reflection?
    Not yet addressing this, needs more discussion first. Added to IS to make sure we don't forget.

  2. Agreed! Done.
  3. Done.

Over at #2451411-65: Add libraries-override to themes' *.info.yml, @alexpott said that we shouldn't use SafeMarkup::format() for exception messages (despite many in core doing so), but use sprintf() instead. So changed that too.

Status: Needs review » Needs work

The last submitted patch, 72: placeholders-2478483-72.patch, failed testing.

wim leers’s picture

StatusFileSize
new142.82 KB
new14.64 KB

#68, RE #66.2:

  • Yes, cache tags/contexts set either on the original or within the callback would work fine; if it weren't, bubbling would be broken.
  • The reason is that the current patch is designed in such a way to have zero extra code for calling #lazy_builder callbacks when replacing placeholders. That is: #attached[placeholders][<placeholdermarkup>] just contains a super thin render array: one with #cache + #lazy_builder, nothing else. So, replacing the placeholder is a matter of simply rendering that super thin render array!
    Because rendering that super thin render array (with #cache + #lazy_builder) means that the cache keys are already present, we can just call the renderer with that render array, and render caching works.
    I failed to get it to work in #54, but I've now found a way to make it work (I actually was very close in #54, I was probably too tired to see it back then). So, yay! :)

Thoughts on these changes? I like it a lot :) This was my biggest frustration/concern with the patch, so very glad to have that out of the way.


Next:

  1. fixing the few test failures introduced in #72 (thanks to the strict checking of what's in $context introduced there)
  2. doing the remaining bits of refactoring (as I said in #66) + fixing my own remaining nitpicks
  3. get #2496399: Comment module's node links' front-end perf-optimizing drupalSettings are missing — the only blocker — green
wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new143.44 KB
new10.66 KB

#74:

  1. I can't reproduce #72's failures with #74.
  2. Done. (The <p>overridden</p> string used for testing placeholders didn't make sense anymore, because it's now impossible to just simply/stupidly override all markup. And I missed bubblingPreRenderCache, should've renamed that as part of addressing #69.2.)
wim leers’s picture

StatusFileSize
new143.95 KB
new1.23 KB

@Fabianx remarked that the way Renderer::renderPlaceholder() is currently implemented makes it impossible to be used by the BigPipe render strategy, because BigPipe will not be able to merge the bubbleable metadata from the rendered placeholder with that of the rest of the HTML (which makes total sense) — it will e.g. have to load additional assets by including some JS to load those additional assets without loading the already-loaded assets again (i.e. something like Drupal.loadAssetLibrary(), which takes drupalSettings.ajaxPageState into account).

But! That's not something we need to solve here. We can just keep that method protected for now, and make it public (and a public API) as part of #2469431: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts. That helps keep the scope of this issue well-defined.

wim leers’s picture

Title: Introduce placeholders to replace #post_render_cache » Introduce placeholders (#lazy_builder) to replace #post_render_cache
Issue summary: View changes
  • IS updated. Now fully up-to-date again.
  • CR updated. Now ready.
yched’s picture

Very minor and silly question about that sample in the IS :

  '#markup' => '<drupal-render-cache-placeholder STUFF></drupal-render-cache-placeholder>',
  '#attached' => [
    'placeholders' => [
      '<drupal-render-cache-placeholder STUFF></drupal-render-cache-placeholder>' => (...)
    ],
  ],

Why not use a shorter snippet with a self-closing tag (<drupal-render-cache-placeholder STUFF/>) ?

wim leers’s picture

#78: because self-closing tags are HTML5, not HTML4, and PHP's DOMDocument is HTML4-only. That's the only reason.

PHP--

wim leers’s picture

StatusFileSize
new151.03 KB
new9.37 KB

Per #2496399-5: Comment module's node links' front-end perf-optimizing drupalSettings are missing, not blocking this issue on that, and just merging the solution into this issue, since there'd be a lot of overlap anyway (and several of the changes needed there are obsolete with this issue! :)).

Which means we can solve this in a simpler, less silly way than that other issue was doing it. Because the sole remaining #post_render_cache function (CommentLazyBuilders.php::attachNewCommentsLinkMetadata()) and #post_render_cache attachments (in CommentLinkBuilder::buildCommentedEntityLinks()) is a pretty curious case, and is the reason it was broken without anybody noticing. That is, it's a recursive post-render cache callback. i.e. when a node is rendered and its links are built, that happens via #post_render_cache. But then HEAD's broken code, which is called by that #post_render_cache callback, adds two more! This is pointless, because obviously it already is a placeholder.
So the solution is very simple: don't attach additional #post_render_cache callbacks (which were only attaching drupalSettings), but simply attach those drupalSettings directly. This is fine, because we're already rendering a placeholder! :)

Next: addressing #69.1.

Status: Needs review » Needs work

The last submitted patch, 80: placeholders-2478483-80.patch, failed testing.

The last submitted patch, 80: placeholders-2478483-80.patch, failed testing.

wim leers’s picture

Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new156.59 KB
new53.93 KB

No remaining tasks! I consider this ready for final review :)


I discussed #69.1 with both @effulgentsia & @Fabianx. We agreed that it'd be better to change

            $callback = 'some_callback';
            $context = array(
              'foo' => 'x',
              'bar' => 'y',
            );
            $output['comment_form'] = array(
              '#lazy_builder' => array(
                 $callback => $context,
              ),
              '#create_placeholder' => TRUE,
            );

and function some_callback(array $context) {…}
to:

            $callback = 'some_callback';
            $args = array(
              'x',
              'y',
            );
            $output['comment_form'] = array(
              '#lazy_builder' => [$callback, $args],
              '#create_placeholder' => TRUE,
            );

and function some_callback($foo, $bar) {…}

The syntax then actually matches the architecture: the syntax only allows you to have a single #lazy_builder callback. And the syntax is identical to the one we have elsewhere for dealing with callbacks: an array containing two values: the first being the callback, the second being the arguments.

This means that not only does the syntax now match one's expectations, it also means we can finally properly document these callbacks. Adding such documentation is a significant part of the changes you'll see in this interdiff.

And finally, thanks to this change, I needed to/was able to get rid of the "placeholder_token" stuff that was being generated, and generate a token automatically based on the hash of the serialized thin render array that is retained. (Just like @Fabianx's patch in #17.) Which means that a placeholder's markup depends solely on the thin render array associated with it (#cache keys + #lazy_builder callback + #lazy_builder arguments). Therefore, if the same combination of those 3 things exists multiple times on the page, they'll be de-duplicated automatically, and all such placeholders will be replaced in one go. Without having to think about it.
Hence you'll see a huge amount of simplification in StatusMessages. (To convince yourself: place multiple "status messages" blocks, note how they all work, and each have the same placeholder markup.)


IS & CR updated.

Status: Needs review » Needs work

The last submitted patch, 84: placeholders-2478483-84.patch, failed testing.

fabianx’s picture

Quick interdiff review:

  1. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -271,11 +270,13 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +      if (count($elements['#lazy_builder'][1]) !== count(array_filter($elements['#lazy_builder'][1], function($v) { return is_null($v) || is_scalar($v); }))) {
             throw new \DomainException("A #lazy_builder callback's context may only contain scalar values or NULL.");
           }
    

    +1000 to asserting that!

  2. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -609,19 +610,10 @@ protected function createPlaceholder(array $element) {
    +    $attributes['token'] = hash('sha1', serialize($placeholder_render_array));
    +    $placeholder_markup = '<drupal-render-placeholder' . $attributes . '></drupal-render-placeholder>';
    

    I thought about this again and hash() is enough.

    A hash_hmac is not needed here, because if you have access to write markup like <drupal-render-placeholder yourself, you can do way worse things with <script than ever with using a placeholder markup, where there should be none.

    And collisions can only occur if someone re-uses the exact same markup in a help text or so, but that should be okay as they should change the token in that case ...

  3. +++ b/core/modules/filter/src/FilterProcessResult.php
    @@ -133,32 +130,25 @@ public function setProcessedText($processed_text) {
    +    $query = UrlHelper::buildQuery($args);
    ...
    +    $attributes['token'] = hash('sha1', serialize([$callback, $args]));
    

    Are we sure we don't want to use arguments here as well?

  4. +++ b/core/modules/filter/src/FilterProcessResult.php
    @@ -133,32 +130,25 @@ public function setProcessedText($processed_text) {
    +    $attributes['callback'] = $callback . ($query ?: '?' . $query);
    

    This ternary operator looks wrong to me ...

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new158.8 KB
new5.79 KB

Fixed test failures.

wim leers’s picture

StatusFileSize
new158.79 KB
new979 bytes

#86:

  1. :)
  2. Yep!
  3. Indeed, let's just adopt the same pattern as Renderer::createPlaceholder(). That was my intention, but I forgot.
  4. Fixed by 3.
fabianx’s picture

Status: Needs review » Reviewed & tested by the community

RTBC, This looks really fantastic now!

( Justification for RTBC: Wim has touched every single line I had in my original patch, so there is nothing left that was not verified by him, it also had several reviews in between by other people, so I feel like I can RTBC this even though I initially worked on this.)

The last submitted patch, 24: placeholders-2478483-24.patch, failed testing.

The last submitted patch, 21: placeholders-2478483-21.patch, failed testing.

The last submitted patch, 25: placeholders-2478483-25.patch, failed testing.

The last submitted patch, 74: placeholders-2478483-74.patch, failed testing.

catch’s picture

Status: Reviewed & tested by the community » Needs review

Overall this is fantastic, I found a few minor issues and had a few questions:

  1. +++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
    @@ -72,82 +63,82 @@ public function applyTo(array &$build) {
    +   *
    +   * @deprecated Use ::getAttachments() instead.
        */
    -  public function getPostRenderCacheCallbacks() {
    

    Should this have a 'to be removed by' note on it?

  2. +++ b/core/lib/Drupal/Core/Render/Element/StatusMessages.php
    @@ -55,82 +41,42 @@ public function getInfo() {
         return $element;
    

    dreditor cut off the bit of the function I was trying to paste, but nice diff here.

  3. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -103,14 +104,67 @@ public function renderPlain(&$elements) {
    +    $args = $placeholder_elements['#lazy_builder'][1];
    +    if (is_string($callable) && strpos($callable, '::') === FALSE) {
    +      $callable = $this->controllerResolver->getControllerFromDefinition($callable);
    +    }
    

    Feels like it'd be useful to move this logic into a helper on ControllerResolver - sure have this pattern elsewhere too, but just a niggle and off-topic here really.

  4. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -103,14 +104,67 @@ public function renderPlain(&$elements) {
    +
    +    // Merge the original cacheability metadata (#cache), plus cache keys.
    +    CacheableMetadata::createFromRenderArray($placeholder_elements)
    +      ->merge(CacheableMetadata::createFromRenderArray($build))
    +      ->applyTo($build);
    +    if (isset($placeholder_elements['#cache']['keys'])) {
    +      $build['#cache']['keys'] = $placeholder_elements['#cache']['keys'];
    +    }
    

    Nit: We're replacing cache keys rather than merging them - i.e. we don't support that being part of $build. Comment suggests we merge the cache keys in a similar way.

  5. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -103,14 +104,67 @@ public function renderPlain(&$elements) {
    +    // bubbleable metadata with the main element's.
    

    ubernit: should this be elements' (plural possessive) to match the variable name?

  6. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -103,14 +104,67 @@ public function renderPlain(&$elements) {
         // but if exceptions aren't caught here, the stack will be left in an
    

    Slightly off-topic again, but for any strategy other than single-flush, isn't this going to cause problems? If we've already sent most of the page before that exception gets thrown then it'll be too late - unless we somehow force the entire page to re-render via js or something? Or throwing 404/403 exceptions randomly might need to not happen any more.

  7. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -211,9 +265,60 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    if (isset($elements['#lazy_builder'])) {
    +      // @todo Convert to assertions once https://www.drupal.org/node/2408013
    +      //   lands.
    +      if (!is_array($elements['#lazy_builder'])) {
    

    +1.

  8. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -211,9 +265,60 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +      }
    +    }
    +    // If instructed to create a placeholder, and a #lazy_builder callback are
    +    // present (without such a callback, it would be impossible to replace the
    +    // placeholder), replace the current element with a placeholder.
    +    if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === TRUE && isset($elements['#lazy_builder'])) {
    

    nit: is present.

    If a #lazy_builder callback isn't present, should this also throw an error?

  9. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -211,9 +265,60 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    if (isset($elements['#lazy_builder'])) {
    

    It's not clear how the comment relates to the code here - there's no reference to pre_render - or is it just that this runs before #pre_render.

    Also isn't #pre_render kept not just for backwards compatibility but also for different use cases?

  10. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -460,36 +550,76 @@ protected function bubbleStack() {
    +  /**
    +   * Turns this element into a placeholder.
    +   *
    +   * Placeholdering allows us to avoid "poor cacheability infection": this maps
    +   * the current render array to one that only has #markup and #attached, and
    +   * #attached contains a placeholder with this element's prior cacheability
    +   * metadata. In other words: this placeholder is perfectly cacheable, the
    +   * placeholder replacement logic effectively cordons off poor cacheability.
    +   *
    

    So 'poor cacheability infection'. We've had this concept informally for a long time, but I think this is the first case of trying to put a name to it, at least in core.

    I'm not sure about using infection terminology though - for a start it sounds like 'cache poisoning' which is a different concept. Infection suggests that the cache context is never wanted (i.e. a bacteria or a virus) - whereas cache contexts specifically protect us against infection/poisoning - that's the point of them.

    Would 'cache context tainting' or 'cache context contamination' be any better?

    Tainting/contamination suggests the context is OK in the correct context (no pun intended).

    You don't want lead in water but you do want it around nuclear waste. You don't want sugar in petrol but you do want it in cakes. User and route cache contexts are fine as long as they're isolated.

  11. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -460,36 +550,76 @@ protected function bubbleStack() {
    +    $attributes = new Attribute();
    +    $attributes['callback'] = $placeholder_render_array['#lazy_builder'][0];
    +    $attributes['arguments'] = UrlHelper::buildQuery($placeholder_render_array['#lazy_builder'][1]);
    +    $attributes['token'] = hash('sha1', serialize($placeholder_render_array));
    

    1. Why are the arguments in the markup?
    2. Why put them as query parameters?

    I think I know where this is leading, but at minimum it needs a good comment explaining, or possibly remove from here and add back later.

  12. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -460,36 +550,76 @@ protected function bubbleStack() {
    +    $placeholder_markup = '<drupal-render-placeholder' . $attributes . '></drupal-render-placeholder>';
    

    Do we need to be concerned at all about people adding <drupal_render_placehoder></drupal_render_placeholder> in text fields at all? Is there any test coverage that deals with an invalid render placeholder?

  13. +++ b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
    @@ -208,64 +207,30 @@ protected function assertRenderCacheItem($cid, $data) {
    +    if ($use_animal_as_array_key) {
    

    Is this like SplObjectStorage? ;)

fabianx’s picture

Status: Needs review » Needs work

Missed that in final review:

+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -103,14 +104,67 @@ public function renderPlain(&$elements) {
+    // Get the render array for the given placeholder
...
+
+    // Build the render array for the given placeholder.
+    $callable = $placeholder_elements['#lazy_builder'][0];
+    $args = $placeholder_elements['#lazy_builder'][1];
+    if (is_string($callable) && strpos($callable, '::') === FALSE) {
+      $callable = $this->controllerResolver->getControllerFromDefinition($callable);
+    }
+    $build = call_user_func_array($callable, $args);
+
+    // Merge the original cacheability metadata (#cache), plus cache keys.
+    CacheableMetadata::createFromRenderArray($placeholder_elements)
+      ->merge(CacheableMetadata::createFromRenderArray($build))
+      ->applyTo($build);
+    if (isset($placeholder_elements['#cache']['keys'])) {
+      $build['#cache']['keys'] = $placeholder_elements['#cache']['keys'];
+    }
+
+    // Render the placeholder into markup.

So why do we duplicate all of that from the renderer?

All that should be needed here is:

$build = $elements['#attached']['placeholders'][$placeholder];
$markup = $this->renderPlain($build);
fabianx’s picture

Some more nits, two the isset() checks can be follow-up as adding the check is not an API change.

  1. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -211,9 +265,60 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    if (isset($elements['#lazy_builder'])) {
    

    Lets check for && !isset($elements['#lazy_builder_built']);

  2. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -211,9 +265,60 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    if (isset($elements['#lazy_builder'])) {
    

    Lets check for && !isset($elements['#lazy_builder_built']);

  3. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -211,9 +265,60 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +      $elements['#built'] = TRUE;
    

    Lets use #lazy_builder_built please.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new159.87 KB
new13.2 KB
  1. Done.
  2. :)
  3. +1 on principle. But indeed off-topic.
  4. Comment clarified.
  5. :) Fixed!
  6. Pre-existing problem/untouched code, so yes, out of scope. But, yes, if the logic to build block X decides to throw a NotFoundHttpException to force the page to 404, we're gonna have a problem. But shouldn't only the controller be able to do that? So not 100% certain this is really a problem.
  7. :)
  8. Fixed nit, exception added, including test coverage. Death to silent failures!
  9. Improved comments.
  10. Hah — thanks for that lovely language feedback! Changed "infection" to "contamination".
    1. For when debugging the rendering process, and/or when looking at render cache entries. No other reason.
    2. Because that's an easy way to encode them into values that are acceptable in HTML attributes.

    Comment added to document this.

  11. No, zero worry about either case. Because the placeholder markup itself is not interpreted or parsed in any way. It is solely markup, completely meaningless. It could just as well be <as976876dsflakjsdfasdfjkl>. The only way a placeholder is going to be replaced, is if there's associated #attached[placeholders] metadata.

    Similarly, there is no such thing as "invalid" placeholders: they're just markup, there's nothing to be valid or invalid. The only way placeholders can be invalid, is if they're invalid HTML. And we have test coverage for that. See \Drupal\Tests\Core\Render\RendererPlaceholdersTest::testCacheableParent().

  12. Eh… no idea what you mean. But this is solely there to accommodate testRenderChildrenPlaceholdersDifferentArguments() — a pretty crazy complex test.

In addressing this feedback, I saw room for improvement in two areas:

  1. simplify the logic in Renderer::renderPlaceholder(), and move it to Renderer::render(), just like we had it in prior iterations — EDIT: this fixes #95
  2. optimize the merging of 'placeholders' attachments: #2471228: Optimize merging of attachments optimized that, and we can do much better than the default in this case: we can just merge at the top level: anything below #attached[placeholders][<unique placeholder markup>] is required to be the same anyway (i.e. "if same placeholder, then the same data to replace that placeholder")

And I also fixed a few nitpicks/cleaned up a few small things.

EDIT: (after noticing I was cross-posting) also addressed #96. #96.1/#96.2: no need for that: once a #lazy_builder is executed, it is removed from the render array. #96.3: done.

fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC, Interdiff looks great!

catch’s picture

Sorry #13 was a bad dad joke - SplObjectStorage lets you use objects as array keys, that parameter lets you use animals as array keys... Not actual feedback.

Interdiff and answers all look good, but getting late here so I'll look again and one more once-over tomorrow.

catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.0.x, thanks!

  • catch committed 4509204 on 8.0.x
    Issue #2478483 by Wim Leers, Fabianx: Introduce placeholders (#...
dawehner’s picture

Congratulations!!

wim leers’s picture

Hurray! :) Coming back from vacation and seeing a big critical has landed, wonderful :)

Next step: #2499157: [meta] Auto-placeholdering.

Status: Fixed » Closed (fixed)

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