Problem/Motivation

Sparked from Fabianx's comment at #2378789-8: Views output cache is broken plus an IRC discussion with damiankloip, Fabianx and myself.

That issue had to make Views' output cache aware of:

  • $element['#attached']
  • $element['#post_render_cache']
  • $element['#cache']['tags']

Ideally, that wouldn't have been necessary; ideally Views would just have to call a method on the Renderer service to get the cacheable representation of a rendered render array. This issue is about adding that.

Proposed resolution

Add a Renderer::getCacheableRenderArray() method, to encapsulate which data is needed for caching a render array.

Remaining tasks

None.

User interface changes

None.

API changes

  • Added Renderer::getCacheableRenderArray()
  • Removed drupal_render_cache_get()
  • Removed drupal_render_cache_set()
  • Removed drupal_render_cid_create()

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Bug because: #2379741-2: Add Renderer::getCacheableRenderArray() to encapsulate which data is needed for caching a render array and have views use it.
Issue priority Major because it blocks #2381797: Add render_cache debug output, which is major.
Disruption Almost zero disruption: if there are any callers of drupal_render_cache_(get|set)() or drupal_render_cid_create(), it's going to be a handful at most. Disruption is hence very, very low, and does not outweigh the long-term benefits of this change. (Which includes e.g. adding a "max age" property to the #cache key of render arrays in a later D8 release, with zero API breakage or logic breakage thanks to this change.)

Comments

wim leers’s picture

Status: Active » Needs review
StatusFileSize
new10.63 KB

Here's a patch that adds this.

Note that I have one concern: Views output cache would ideally not exist; it should just use render caching like anything else. Which then leaves the question: what is a valid use case for this?

Fabianx wrote this in IRC, but it's not entirely clear what that means. It'd be great if you could explain that in some more detail, Fabian :)

Fabianx: WimLeers: I want to do my own caching
Fabianx: And I have a list of data ...
Fabianx: And I don't know what that list will contain before doing so.
fabianx’s picture

Title: Add Renderer::getCacheableData() to encapsulate which data is needed for caching a render array » Add Renderer::getCacheableData() to encapsulate which data is needed for caching a render array and have views use it
Category: Task » Bug report

So #pre_render, #cache is just a pattern, but it might not work for you, so you want custom caching.

The bigger impact however is if views hardcodes this:

I might want to add downstream-ttl as a render array #cache property and I need it propagated upstream.

Therefore I can now exchange the renderer service, but if views has this hardcoded, I can't have this work with views caching.

And that is why this is a bug.

fabianx’s picture

Postponing on #2378789: Views output cache is broken briefly ...

fabianx’s picture

Status: Needs review » Postponed
fabianx’s picture

Status: Postponed » Needs work

Code needs work to port views over to the new API.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new13.79 KB
new3.22 KB

Voilà.

damiankloip’s picture

+++ b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
@@ -235,27 +226,6 @@ public function postRender(&$output) { }
-    $this->view->element['#attached'] = drupal_merge_attached($this->view->element['#attached'], $this->storage['attachments']);
-    $this->view->element['#cache']['tags'] = Cache::mergeTags(isset($this->view->element['#cache']['tags']) ? $this->view->element['#cache']['tags'] : [], $this->storage['cacheTags']);
-    $this->view->element['#post_render_cache'] = NestedArray::mergeDeep(isset($this->view->element['#post_render_cache']) ? $this->view->element['#post_render_cache'] : [], $this->storage['postRenderCache']);

We are losing something here, we need to make sure this is ok. With this patch we will no longer be merging with any other items in $view->element.

damiankloip’s picture

StatusFileSize
new14.89 KB
new3.01 KB

I removed the 'output' key on the storage property. If we are not using other keys, I don't think we need to use that either now. Let's just simplify as much as we can?

We should also probably inject the renderer, and not use drupal_render directly anymore.

Just question in #7 I think.

Status: Needs review » Needs work

The last submitted patch, 8: 2379741-8.patch, failed testing.

damiankloip’s picture

Status: Needs work » Needs review
StatusFileSize
new16.4 KB
new1.51 KB

Whoops.

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -387,4 +398,84 @@ protected function processPostRenderCache(array &$elements) {
    +    if (!empty($cid) && $cache = \Drupal::cache($bin)->get($cid)) {
    ...
    +    \Drupal::cache($bin)->set($cid, $data, $expire, $data['#cache']['tags']);
    

    Should we inject the cache services? From an abstract point of view it would be cool to be able to render something without a cache dependency, so maybe make it optional?

  2. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -387,4 +398,84 @@ protected function processPostRenderCache(array &$elements) {
    +    if (!\Drupal::request()->isMethodSafe() || !$cid = drupal_render_cid_create($elements)) {
    

    At least this request() could be replaced with the stack again.

  3. +++ b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
    @@ -143,8 +176,8 @@ public function cacheSet($type) {
    -        $this->storage['output'] = drupal_render($this->view->display_handler->output);
    -        $this->gatherRenderMetadata($this->view->display_handler->output);
    +        $markup = $this->renderer->render($this->view->display_handler->output);
    
    @@ -178,16 +211,7 @@ public function cacheGet($type) {
    -            $this->restoreRenderMetadata();
    -            $this->view->display_handler->output = array(
    -              '#attached' => &$this->view->element['#attached'],
    -              '#cache' => [
    -                'tags' => &$this->view->element['#cache']['tags'],
    -              ],
    -              '#post_render_cache' => &$this->view->element['#post_render_cache'],
    -              '#markup' => $cache->data['output'],
    -            );
    +            $this->view->display_handler->output = $this->storage;
    

    Tis is really nice code now!

wim leers’s picture

Issue tags: +VDC
StatusFileSize
new18.91 KB
new2.67 KB

#7: I found that $view->element business highly confusing; I don't entirely understand how that works or is intended to work. Could you change that to match how you think it should work? :) Thanks!

#8 + #10: looking good :) Just one docblock update that didn't match the parameter order; fixed that.

#11:

  1. Perhaps we could inject the 'render' cache service but not any others? The problem is that we can only know at runtime which cache services we'll need, since any render array can choose to store its render cache items in another bin…
  2. Hah, looked over that one. Fixed :)
  3. :)
fabianx’s picture

The patch looks already really good!

wim leers’s picture

Issue tags: +D8 cacheability

I'm not yet happy with the method names (saveToCache() and getFromCache()). Suggestions?

I'm also not yet convinced getCacheableData() is the best method name, because you don't just get back "data", but you get back an actual valid render array that is the most minimal representation of the given rendered render array.

Thoughts?

+++ b/core/lib/Drupal/Core/Render/RendererInterface.php
--- /dev/null
+++ b/core/modules/system/tests/modules/theme_test/theme_test.libraries.yml

+++ b/core/modules/system/tests/modules/theme_test/theme_test.libraries.yml
--- /dev/null
+++ b/core/modules/system/tests/themes/test_basetheme/test_basetheme.libraries.yml

+++ b/core/modules/system/tests/themes/test_basetheme/test_basetheme.libraries.yml
--- /dev/null
+++ b/core/modules/system/tests/themes/test_subtheme/test_subtheme.libraries.yml

+++ b/core/modules/views/src/Plugin/views/cache/Time.php
--- /dev/null
+++ b/core/themes/stark/stark.libraries.yml

These hunk doesn't belong here.

wim leers’s picture

StatusFileSize
new16.93 KB
new0 bytes

Reroll after #2382503: Not possible to render self-contained render array while a render stack is active landed. Removed the faulty hunks mentioned in #14 (which btw have already been committed, hence no interdiff for those removals).

wim leers’s picture

I'm not yet happy with the method names (saveToCache() and getFromCache()). Suggestions?

These are protected methods anyway, so it actually doesn't matter at all. It's not important, and is sufficiently clear.

I'm also not yet convinced getCacheableData() is the best method name, because you don't just get back "data", but you get back an actual valid render array that is the most minimal representation of the given rendered render array.

More accurate names would be getCacheableRepresentation(&$markup, $elements) or getCacheableRenderArray(&$markup, $elements) or getMinimalRepresentation(&$markup, $elements).

I think that getCacheableRenderArray(&$markup, $elements) would be the best choice, and better than getCacheableData(&$markup, $elements), which is in the current patch. Do others agree?

fabianx’s picture

getCacheableRenderArray is great!

cacheSet / cacheGet is probably sufficient for the protected methods.

wim leers’s picture

StatusFileSize
new16.96 KB
new3.18 KB

Done.

wim leers’s picture

Title: Add Renderer::getCacheableData() to encapsulate which data is needed for caching a render array and have views use it » Add Renderer::getCacheableRenderArray() to encapsulate which data is needed for caching a render array and have views use it
StatusFileSize
new2.51 KB
new2.51 KB

I'd implemented the first part of #17, not the second part (cacheSet / cacheGet is probably sufficient for the protected methods.). Did that now.

Status: Needs review » Needs work

The last submitted patch, 19: 2379741-19.patch, failed testing.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new16.95 KB

interdiff == patch => facepalm

wim leers’s picture

Priority: Normal » Major
Issue tags: +blocker

This blocks #2381797: Add render_cache debug output, which is major. Hence bumping this too.

fabianx’s picture

+++ b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
@@ -235,27 +259,6 @@ public function postRender(&$output) { }
-    $this->view->element['#attached'] = drupal_merge_attached($this->view->element['#attached'], $this->storage['attachments']);
-    $this->view->element['#cache']['tags'] = Cache::mergeTags(isset($this->view->element['#cache']['tags']) ? $this->view->element['#cache']['tags'] : [], $this->storage['cacheTags']);
-    $this->view->element['#post_render_cache'] = NestedArray::mergeDeep(isset($this->view->element['#post_render_cache']) ? $this->view->element['#post_render_cache'] : [], $this->storage['postRenderCache']);

I would _love_ to RTBC this, but the question remains if there can be something already in $this->view->element[metadata-key] that is not cached.

In D7 RenderCache I circumvented this in two ways:

a) Either adding the whole element as x_render_cache_storage (and removing any #markup other children by using again the getCacheableRenderArray function).
b) Rendering whatever was in there early on the stack.

Needs feedback by Views maintainer I assume to see if there can be something in $this->view->element that was not cached before.

wim leers’s picture

I I would _love_ to RTBC this, but the question remains if there can be something already in $this->view->element[metadata-key] that is not cached.

… in which case that'd have to be merged, which is what HEAD does.

I tried to answer this in multiple ways:

  1. I debugged it, looking at all other occurrences in core where ->element is being modified. None of them are called on a Views output cache hit, hence there cannot be anything to merge.
  2. The stack trace makes this impossible: on a Views output cache hit, we start with a thin render array that has a #pre_render callback. This initializes the ViewExecutable $view object, then calls executeDisplay() on it, which then shortcuts to the output cache. The whole point of the output cache is to avoid executing the rendering logic for all the plugins, so it's only natural that there's no opportunity for them to alter $view->element.
  3. Imagine there were actual instances of the theoretical problem you describe. Then it'd not actually be output caching, it'd be output caching plus dynamic stuff. This is not what happens there.

So AFAICT, the theoretical question you raised (which is a good question) is not a problem.

wim leers’s picture

StatusFileSize
new17.85 KB
new6.04 KB

Yesterday I noticed that HtmlRenderer::prepare() could also benefit from this :)

And while working on that, I managed to clean this patch up further. The only thing that's been removed is the comment about ESI, where it's stated that cache backends can replace the actual content with ESI includes. That's been a lie for a very long time, because the cache backend receives the array by value, not by reference, as is dictated by CacheBackendInterface. In D7, cache_set() also worked by value, but in D7, it'd have been possible to replace cache.inc and change the signature, in which case the documented ESI approach would've worked indeed. This was added in 2009, and came with zero test coverage. ESI would probably not be implemented like this anyway nowadays.

fabianx’s picture

Status: Needs review » Reviewed & tested by the community

The only thing that's been removed is the comment about ESI, where it's stated that cache backends can replace the actual content with ESI includes. That's been a lie for a very long time, because the cache backend receives the array by value, not by reference, as is dictated by CacheBackendInterface. In D7, cache_set() also worked by value, but in D7, it'd have been possible to replace cache.inc and change the signature, in which case the documented ESI approach would've worked indeed.

I don't think this was ever used by anything, so i think this is fine

Thanks for finding further usages of this and thanks for checking that the "merge" before was no merge, because there was never any content.

RTBC :)

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Render/RendererInterface.php
    @@ -269,4 +269,23 @@ public function renderPlain(&$elements);
    +   * Gets a cacheable render array for a render array and its rendered output.
    +   *
    +   * Given a render array and its rendered output (HTML string), return an array
    +   * data structure that allows the render array and its associated metadata to
    +   * be cached reliably.
    +   *
    +   * If Drupal needs additional rendering metadata to be cached at some point,
    +   * consumers of this method will continue to work. Those who only cache
    +   * certain parts of a render array will cease to work.
    +   *
    

    Just in case you want to, you could document that this result can be safely serialized()/unseralized(), not sure whether this is implicit already with "cached"?

  2. +++ b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
    @@ -143,8 +176,8 @@ public function cacheSet($type) {
    +        $this->renderer->render($this->view->display_handler->output);
    +        $this->storage = $this->renderer->getCacheableRenderArray($this->view->display_handler->output);
    

    <3

wim leers’s picture

StatusFileSize
new17.87 KB
new852 bytes
  1. Good idea. Done.
  2. :)

Doc changes only, hence keeping at RTBC.

The last submitted patch, 25: 2379741-25.patch, failed testing.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs issue summary update
  1. To evaluate whether we should make this change during the beta, we need to update the issue summary according to https://www.drupal.org/core/beta-changes to explain what improvements it makes and what the disruption of the change would be. Can someone add Drupal 8 beta phase evaluation template to the issue summary.
  2. Do we need a D8 to D8 change record since we are removing drupal_render_cache_set and drupal_render_cache_get? These are the only runtime usages of drupal_render_cid_create() so perhaps we ought to be moving that too for consistency? And then there is drupal_render_cache_generate_placeholder() - there is no functional usage of this either but that is used in move places - drupal_render_cache_generate_placeholder () looks like followup material to me.
wim leers’s picture

Issue summary: View changes
Status: Needs work » Needs review
Issue tags: -Needs issue summary update
StatusFileSize
new33.65 KB
new19.28 KB
  1. Added beta evaluation.
    1. Added a CR for removing drupal_render_cache_(get|set)(). It's an API that I'd consider almost part of the internal Drupal API, very few people are using this, if any, but since it was a procedural API, it's indeed technically an API change.
    2. Also moved drupal_render_cid_create() for consistency, as requested. (This had zero calls to it, except for the Renderer service and tests (ab)using it.) Note that this pretty much doubled the patch size, but it's still a simple enough patch IMO.
    3. And indeed, drupal_render_cache_generate_placeholder() is then the last remnant of what logically belongs on the Renderer service, but that's for a follow-up issue. That we might want to keep around for BC. But, again, that's a discussion for a separate issue.
dawehner’s picture

  1. +++ b/core/includes/common.inc
    @@ -2369,79 +2369,6 @@ function show(&$element) {
    -  // Create the cache ID for the element.
    -  if (!\Drupal::request()->isMethodSafe() || !$cid = drupal_render_cid_create($elements)) {
    -    return FALSE;
    -  }
    
    +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -437,4 +459,102 @@ protected function processPostRenderCache(array &$elements) {
    +    if (!$this->requestStack->getCurrentRequest()->isMethodSafe() || !$cid = $this->createCacheID($elements)) {
    +      return FALSE;
    +    }
    ...
    +    // Create the cache ID for the element.
    +    if (!$this->requestStack->getCurrentRequest()->isMethodSafe() || !$cid = $this->createCacheID($elements)) {
    +      return FALSE;
    +    }
    

    I know this existed before, but can we document why we don't want to deal with it in case we use https? It feels like quite an detail which is not always necessarily right.

  2. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -437,4 +459,102 @@ protected function processPostRenderCache(array &$elements) {
    +    if (!empty($cid) && $cache = \Drupal::cache($bin)->get($cid)) {
    ...
    +    \Drupal::cache($bin)->set($cid, $data, $expire, $data['#cache']['tags']);
    

    I can haz the cache factory injected?

  3. +++ b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
    @@ -73,6 +74,38 @@
    +    return new static($configuration, $plugin_id, $plugin_definition, $container->get('renderer'));
    

    it would be nice to newline this.

  4. +++ b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
    @@ -143,8 +176,8 @@ public function cacheSet($type) {
           case 'output':
    -        $this->storage['output'] = drupal_render($this->view->display_handler->output);
    -        $this->gatherRenderMetadata($this->view->display_handler->output);
    +        $this->renderer->render($this->view->display_handler->output);
    +        $this->storage = $this->renderer->getCacheableRenderArray($this->view->display_handler->output);
             \Drupal::cache($this->outputBin)->set($this->generateOutputKey(), $this->storage, $this->cacheSetExpire($type), $this->getCacheTags());
    

    Meh, this conflicts with #2378815: DisplayPluginBase::elementPreRender() and View::preRenderViewElement() are called even when the Views output cache is being used, causing notice

  5. +++ b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
    @@ -180,16 +213,7 @@ public function cacheGet($type) {
    -            $this->view->display_handler->output = array(
    -              '#attached' => &$this->view->element['#attached'],
    ...
    -              '#post_render_cache' => &$this->view->element['#post_render_cache'],
    ...
    +            $this->view->display_handler->output = $this->storage;
     
    

    Do we think it is okay to no longer reference the view element entries?

wim leers’s picture

StatusFileSize
new34.4 KB
new4.91 KB
  1. Huh? This only cares about the HTTP method, not whether HTTPS or HTTP is used.
  2. Good point, done.
  3. Done.
  4. Nothing I can do about this.
  5. (Note to self: this was done in the interdiff in #3.)

    I think it's okay, because the modifying by reference only needs to happen in the "pre render" stage of rendering a view. The cited code is already in the "render" stage. It is ViewExecutable::render() that calls CachePluginBase::cacheGet(), and all logic in that function after the invocation of cacheGet() directly uses $this->display_handler->output, so it's safe.

    On further consideration; I don't think it's okay, because it's possible a Views "post render" callback modifies $view->element['#attached'] instead of $view->display_handler->output, because that used to be possible. So, restoring the references.

wim leers’s picture

StatusFileSize
new34.82 KB
new1.41 KB

Discussed point #32.1 with dawehner and catch in IRC, this is the conclusion.

Status: Needs review » Needs work

The last submitted patch, 34: 2379741-34.patch, failed testing.

The last submitted patch, 33: 2379741-33.patch, failed testing.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new34.85 KB
new2.82 KB

Fixed test failures (oops!) + 80 cols violations.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Alright, looks sane now.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 62c31b5 and pushed to 8.0.x. Thanks!

Thanks for adding the beta evaluation to the issue summary.

  • alexpott committed 62c31b5 on 8.0.x
    Issue #2379741 by Wim Leers, damiankloip: Add Renderer::...

Status: Fixed » Closed (fixed)

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