Problem/Motivation

blocks #2450897: Cache Field views row output

this issue is kinda blocked on: #2469277: Changing #cache keys during #pre_render or anywhere else leads to cache redirect corruption

Anything that caches renderable arrays to be used by the render pipeline needs to properly support all bubbleable metadata (cache contexts/tags/max-age + assets), plus #markup plus #cache_redirect for bubbled cache contexts.

Therefore it MUST use a dedicated render cache API. If anything that deals with render arrays, uses $cache_backend->(get|set) directly (even with the $renderer->getCacheableRenderArray() call), then we are still in trouble as #cache_redirect would need to be re-implemented - otherwise a bubbled 'moon' context will be missing and the output won't be varied by 'moon' as necessary. The Views output cache suffers currently from that.

Anything that uses cacheable render arrays and wants to cache that MUST go through the Render (Cache) API and not use $cache_backend->(get|set) directly.

It must provide the original #cache data too so that what is currently $pre_bubbling_cid works correctly.

For most cases in core the #pre_render / #cache pattern is used to mitigate this effect, however there are use cases where that pattern is not enough, e.g.

  1. Multiple Cache-Get / Prepare and render remainder

    A number of rows can be prepared in fully, but only non-cached rows, should be prepared and rendered.

    The #pre_render / #cache pattern is not enough here as they don't have any multiple support, but assume each row is rendered in a single way.

  2. Something like views that according to its internal architecture can't use the #pre_render / #cache pattern (e.g. because it supports its own cache layer).

    This can also use the new renderCache API and has to deal less with the interna then.

Proposed resolution

An ideal API would decouple the render caching implementation and allow to do:

$cached_rows = $this->renderCache->getMultiple($render_cache_array_keyed_by_row_id);

// ...

foreach ($rendered_rows as $row_id => &$rendered_row) {
  $this->renderCache->set($rendered_row, $render_cache_array_keyed_by_row_id[$row_id]);
}

return $cached_rows + $rendered_rows;

A render_cache array is a render array with just the #cache, #attached and #markup property set.

This relates to the other issue of using multiple cache_get in the renderer that it will make it simpler and we can also optimize some other cases like BlockViewBuilder, etc. more in an easier way.

However using the render cache API is a MUST for modules dealing with render arrays - else it will break the caching.

This should also remove Renderer::getCacheableRenderArray() as that gave a wrong promise of solving the cacheSet/Get and using $renderCache is simpler than the $cache anyway.

Before:

$original_build = $build;
$renderer->render($build);
$data = $renderer->getCacheableRenderArray();
$cid = $this->computeACidUsingContextsAndKeys($data);
$this->cache->set($cid, $data);

and even with that much work the result is incorrect when something bubbles up and createCid($original_build) != createCid($build).

After:

$original_build = $build;
$renderer->render($build);
$this->renderCache->set($build, $original_build);

The original is needed to compute the pre-bubbling CID.

We cannot kill getCacheableRenderArray(), yet, because core needs to use it in HtmlRenderer::prepare(), which is a very very special case.

--

Thoughts: However $renderer->render($build); will already render cache the data, also it is unclear what the stack behaves like and what one would return.

The simplest API probably would be to use:

$this->renderCache->renderAndSet($build);
return $build;

With the understanding that $build afterwards can be used as if it was coming from cache. This is also very important for any placeholdering work we do.

This is different from calling ->render that $build would be replaced with the cacheableRenderArray() AND that no data would be bubbled on the stack, which it should not be.

Remaining tasks

None.

User interface changes

None.

API changes

RendererInterface::getCacheableRenderArray() removed; now at RenderCacheInterface::getCacheableRenderArray() instead. See #28 for rationale.

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Task, because not a bug, but refactoring to prevent significant amounts of duplication for #2450897: Cache Field views row output.
Issue priority Major because blocking a critical, and because it enhances the maintainability of Renderer significantly.
Prioritized changes The main goal of this issue is performance: it blocks #2450897: Cache Field views row output.
Disruption There is only one API change, it's a very, very rarely needed API. Two calls to it in core. Very likely zero in contrib/custom code. So, effective disruption is approximately zero.
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

catch’s picture

This makes sense to me to use for multiple get/set. #pre_render just doesn't work at all for that.

Fabianx’s picture

Issue summary: View changes

Removed page_cache example as its controversial.

Wim Leers’s picture

Fabianx’s picture

Issue summary: View changes
Wim Leers’s picture

Issue summary: View changes

First, adding formatting/structure to the IS, to make it more parseable. Then going to analyze it.

Fabianx’s picture

Issue summary: View changes

.

Fabianx’s picture

Issue summary: View changes

.

Fabianx’s picture

Title: Decouple cache implementation from the renderer and expose as renderCache service » [PP-1] Decouple cache implementation from the renderer and expose as renderCache service
Status: Active » Postponed
Issue tags: +blocker

We could also just expose the cache parts of the renderer as public API, but I think that makes the renderer too complex ...

And I am pretty sure this kinda blocks #2450897: Cache Field views row output.

While I was able to re-implement a ton of APIs in that issue, having access to cacheGet / cacheSet will make it _way_ simpler and cleaner ...

But this issue is kinda blocked on:

#2469277: Changing #cache keys during #pre_render or anywhere else leads to cache redirect corruption

Because having to specify $pre_bubbling_cid without means to create $pre_bubbling_cid is problematic and I think we don't want to expose createCacheId() as public ...

YesCT’s picture

Fabianx’s picture

Issue tags: +D8 Accelerate Dev Days

Tagging for Dev Days

plach’s picture

Title: [PP-1] Decouple cache implementation from the renderer and expose as renderCache service » Decouple cache implementation from the renderer and expose as renderCache service
Status: Postponed » Needs work
plach’s picture

Status: Needs work » Active
Fabianx’s picture

Assigned: Unassigned » Fabianx

Let me get a quick patch uploaded for that.

Fabianx’s picture

Assigned: Fabianx » Unassigned
Status: Active » Needs review
FileSize
37.93 KB

Quick first WIP :

- Test fixes for unit tests are still wrong. (just made them pass for now)
- BC for getCacheableRenderArray needs discussion, but likely should stay on renderer as BC

Overall works nicely though.

Wim Leers’s picture

  1. +++ b/core/lib/Drupal/Core/Render/RenderCache.php
    @@ -0,0 +1,309 @@
    +class RenderCache implements RenderCacheInterface {
    

    Just an idea, but… I think that in an ideal world, we wouldn't have a RenderCache service that is used directly by the Renderer service, but we'd instead have a service decorating the Renderer service, and in that way adding support for render caching.

    That'd mean Renderer would only have to care about rendering itself, not about render caching.

  2. +++ b/core/lib/Drupal/Core/Render/RenderCache.php
    @@ -0,0 +1,309 @@
    +  protected $cacheContexts;
    

    $cacheContextsManager

More complete review to follow.

Fabianx’s picture

#15

1. That would double the function calls to doRender(), not sure we want to afford that - it is very much our critical path and the caching part is already quite isolated ...

And in an ideal world we would have a CachedRendererDecorator, which uses the renderCache service as composition > inheritance / decoration, but I don't see that its worth it ...

2. Will fix, was straight c&p (or move & paste) from Renderer (which means core missed one rename ;)).

catch’s picture

Haven't reviewed in depth yet but general +1 to splitting this out, and what I did review was encouraging.

plach’s picture

Just a quick look to get some familiarity with this code.

  1. +++ b/core/lib/Drupal/Core/Render/RenderCache.php
    @@ -0,0 +1,309 @@
    +    $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render';
    

    Would it make sense to have a dedicated method for this?

  2. +++ b/core/lib/Drupal/Core/Render/RenderCache.php
    @@ -0,0 +1,309 @@
    +      // @see ::doRender()
    ...
    +    // @see ::doRender()
    

    These need to be updated.

  3. +++ b/core/lib/Drupal/Core/Render/RenderCacheInterface.php
    @@ -0,0 +1,78 @@
    + * Defines an interface for caching rendered data.
    

    This could be a great place to add some documentation about the key concepts of render caching: keys, contexts, tags and so on. Sorry, if they are already documented in a better place :)

  4. +++ b/core/lib/Drupal/Core/Render/RenderCacheInterface.php
    @@ -0,0 +1,78 @@
    +   * Given a render array and its rendered output (HTML string), return an array
    

    Is there any value in assuming we are caching HTML markup?

  5. +++ b/core/lib/Drupal/Core/Render/RenderCacheInterface.php
    @@ -0,0 +1,78 @@
    +   * 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.
    

    This paragraph is a bit confusing: it's not clear why Drupal would need additional metadata. As a consequence of a code change? Or new code installed? Maybe an example would help.

  6. +++ b/core/lib/Drupal/Core/Render/RenderCacheInterface.php
    @@ -0,0 +1,78 @@
    +   * Gets the cached, pre-rendered element of a renderable element from the cache.
    

    80 chars exceeded, what about:

    "Gets the cached, pre-rendered element of a renderable element from cache."

  7. +++ b/core/lib/Drupal/Core/Render/RenderCacheInterface.php
    @@ -0,0 +1,78 @@
    +   * @param array $pre_bubbling_elements
    +   *   A renderable array corresponding to the state (in particular, the
    +   *   cacheability metadata) of $elements prior to the beginning of its
    +   *   rendering process, and therefore before any bubbling of child
    +   *   information has taken place. Only the #cache property is used by this
    +   *   function, so the caller may omit all other properties and children from
    +   *   this array.
    

    Would it make sense to specify only pre-bubbling cacheability metadata then? I guess it would make the contract clearer.

  8. +++ b/core/lib/Drupal/Core/Render/RenderCacheInterface.php
    @@ -0,0 +1,78 @@
    +   *  Returns FALSE if no cache item could be created, NULL otherwise.
    +   *
    +   * @see ::cacheSet()
    +   */  ¶
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function cacheSet(array &$elements, array $pre_bubbling_elements);
    

    PHP doc mess :)

Wim Leers’s picture

plach’s picture

Thanks, a couple of @see would be great, then :)

Wim Leers’s picture

Assigned: Unassigned » Wim Leers

I'm going to take a stab at addressing all feedback.

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
FileSize
39.06 KB
6.21 KB

#16:

  1. Good point RE: too expensive.
  2. Y, I know. I made the same remark there, but it landed anyway.

#18:

  1. Done.
  2. Per #19 + #20: referring to the existing docs.
  3. Not really, but OTOH everything in Renderer is already HTML-specific anyway. Many concepts, and every template.
  4. These are c/p'ed docs — they come from RendererInterface::getCacheableRenderArray(). Also not sure how to improve without making it super wordy.
  5. Fixed.
  6. Again c/p'd docs. +1 though, but Fabianx wanted to keep this more abstract, so that he wouldn't have to change the API again in #2469431: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts.
  7. Fixed :)

Review:

+++ b/core/lib/Drupal/Core/Render/RenderCache.php
@@ -0,0 +1,309 @@
+  public function cacheGet(array $elements) {
...
+  public function cacheSet(array &$elements, array $pre_bubbling_elements) {

I'd argue these should just be get() and set().

The "cache" bit is implied by the service name.

Fabianx’s picture

#22: Yes, just get and set. :)

plach’s picture

+++ b/core/lib/Drupal/Core/Render/RenderCache.php
@@ -14,6 +14,10 @@
+ * @see sec_caching

I was suggesting to put this on the interface, looks fine to me otherwise.

Fabianx’s picture

  1. +++ b/core/lib/Drupal/Core/Render/RenderCache.php
    @@ -14,6 +14,10 @@
      * Wraps the caching logic for the render caching system.
    + *
    + * @see sec_caching
    

    Yep, lets put that on the interface as well.

  2. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -194,8 +194,8 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    // @see \Drupal\Core\Render\RenderCacheInterface::cacheGet()
    +    // @see \Drupal\Core\Render\RenderCacheInterface::cacheSet()
    

    If we rename, need to rename here, too.

Not much found in review.

Things to address:

- a) Above points
- b) Rename cacheGet / cacheSet to just get / set. (oversight on my part)
- c) Discuss what to do with the unit tests:

Currently we couple renderer and renderCache together and test them together. That works, but is not how unit tests are usually structured ...

We could put c) to a follow-up if everyone agrees that the test coverage "purification" should not be blocking going ahead with Views-Row-Caching (critical).

Wim Leers’s picture

FileSize
39.46 KB
8.54 KB

Fixed #22's bottom remark, thanks to the +1 in #23.
Fixed #24.
Fixed #25.

RE: unit testing RenderCache (#25.c)): I think that these are inherently tightly coupled. I think our current test coverage is sufficient.


Another review.

  1. createCacheID() is a protected method. But at #2450897-23: Cache Field views row output.1, it is said that part of the reason we're doing this issue, is to allow other code to call this method (to avoid duplication). So… do we still want to make that method public, or…?
  2. Regardless of the original need for this issue (i.e. for the Views row caching issue), I think this is a sane improvement. It's two highly coupled classes, but that is fine. It makes both classes/parts more understandable, so I think it's an improvement in any case.
Fabianx’s picture

#26

1. If there is a cacheSet / cacheGet method the createCacheID does not need to be public.
2. Agree, it is independently useful.

If c) unit testing is acceptable, then all we need to do is IMHO to check again on the docs for getCacheableMetadata in the Interface of the renderer and we should be done here.

Wim Leers’s picture

FileSize
46.35 KB
8.71 KB

#27.2: LOL, why didn't I think of that? :)

all we need to do is […] check again on the docs for getCacheableMetadata in the Interface of the renderer and we should be done here.

You meant ::getCacheableRenderArray(), but yes, I'd agree that that is all that's left then.

  1. The name suggests it should only live on RenderCache.
  2. The usages are limited to: 1) the renderer doing render caching, 2) HtmlRenderer, with a very very exceptional edge case, 3) Views' CachePluginBase, which will go away as part of #2381277: Make Views use render caching and remove Views' own "output caching". So disruption of moving it to RenderCache would be very very minimal.

So I say: let's keep it only on RenderCache. Did that in this reroll. Up to you guys to judge now.

The last submitted patch, 26: decouple_cache-2466585-26.patch, failed testing.

Wim Leers’s picture

FileSize
45.89 KB
495 bytes

Dammit, both #26 and #28 contain an accidental change to UserData, causing those test failures.

The last submitted patch, 28: decouple_cache-2466585-28.patch, failed testing.

Fabianx’s picture

RTBC on all interdiffs since my original implementation.

Need someone else for final sign-off now, but in my opinion its ready now.

--

We still need a beta eval and added the API change tag for the getCacheableRenderArray() move.

Wim Leers’s picture

Issue summary: View changes
Issue tags: -Needs beta evaluation

Added beta evaluation. Updated the remaining tasks/UI changes/API changes sections too.

Status: Needs review » Needs work

The last submitted patch, 30: decouple_cache-2466585-30.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
47.6 KB
1.76 KB

Forgot to update one bit.

plach’s picture

  1. +++ b/core/lib/Drupal/Core/Render/RenderCache.php
    @@ -0,0 +1,310 @@
    +  public function get(array $elements) {
    

    It would be great if we could provide also a ::getMultiple() method, this should allow to make things even faster in #2450897: Cache Field views row output.

  2. +++ b/core/lib/Drupal/Core/Render/RenderCacheInterface.php
    @@ -0,0 +1,77 @@
    +   * @see ::get()
    +   */
    +  public function get(array $elements);
    

    Why is this pointing to itself?

  3. +++ b/core/lib/Drupal/Core/Render/RenderCacheInterface.php
    @@ -0,0 +1,77 @@
    +   * @see ::set()
    +   */
    +  public function cacheSet(array &$elements, array $pre_bubbling_elements);
    

    I thought we were going to rename this to set, weren't we?

Wim Leers’s picture

FileSize
47.57 KB
4.47 KB
  1. Out of scope. This makes it easier to add that, at least/at last :)
  2. LOL, pre-existing problem, but fixed.
  3. LOL! Fixed. Can't believe I simply missed that earlier :(

Sorry for those bad rerolls :(

plach’s picture

Status: Needs review » Reviewed & tested by the community

Wim and Fabian are happy with this, so who am I to make them sad?

plach’s picture

Issue tags: +VDC
plach’s picture

Priority: Major » Critical

This is actually blocking a critical

plach’s picture

Status: Reviewed & tested by the community » Needs review

I went ahead and applied #37 in #2450897-74: Cache Field views row output, but I have some troubles using the new API and make things work. I had to perform some adjustments. Please check whether I'm using it the intended way or I'm doing something wrong before setting this back to RTBC.

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

I am setting back to RTBC as I provided feedback in the other issue and this is also without that other issue useful in itself (albeit not critical) and also blocks BigPipe work.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committing this to unblock further work. We can still make any tweaks necessary if #2450897: Cache Field views row output finds it to be necessary. Committed 83c5370 and pushed to 8.0.x. Thanks!

Thanks for adding the beta evaluation to the issue summary.

diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index e1113c9..ac1bd6e 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -12,11 +12,9 @@
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Cache\CacheableMetadata;
-use Drupal\Core\Cache\CacheContextsManager;
 use Drupal\Core\Controller\ControllerResolverInterface;
 use Drupal\Core\Theme\ThemeManagerInterface;
 use Drupal\Component\Utility\SafeMarkup;
-use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
  * Turns a render array into a HTML string.

Fixed unused use statements on commit.

  • alexpott committed 83c5370 on 8.0.x
    Issue #2466585 by Wim Leers, Fabianx: Decouple cache implementation from...
fgm’s picture

Issue tags: +Needs change record

I would say this needs a change record : using the cache API directly will be the obvious choice for many.

Wim Leers’s picture

Issue tags: -Needs change record

@fgm: Except that only in the extremest of extreme edge cases, one would even be tempted to implement their own render caching. 99.9% of code should just use render caching, which already is thoroughly documented: https://www.drupal.org/developing/api/8/render/arrays/cacheability. Otherwise, I would agree.

fgm’s picture

Makes sense : I added a note about this on the Cache API intro at https://www.drupal.org/developing/api/8/cache so that devs looking at the Cache API can be made aware of this.

Wim Leers’s picture

Good call! :) Tried to clarify it further.

Status: Fixed » Closed (fixed)

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