Problem/Motivation

If you provide field based REST views you'll realize that they are quite slow, unless you have some caching for the entire result enabled.

Once you dive into it, you quickly realize, this is caused by having no row level field caching for REST views.

Proposed resolution

Remaining tasks

User interface changes

API changes

Data model changes

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

dawehner created an issue. See original summary.

dawehner’s picture

Assigned: dawehner » Unassigned
Status: Active » Needs review
FileSize
474.42 KB
616 bytes

This helps quite a lot, see the attached screenshot.

Let's see whether this breaks stuff

oriol_e9g’s picture

You also need to change the code comment

// Otherwise, pass this through the field advancedRender() method.

dawehner’s picture

Issue tags: +Needs tests

Fair, let's see first, whether this breaks anything.

We need maybe add some tests around escaping etc. While implementing that in custom code, I had quite some issues around that.

moshe weitzman’s picture

Priority: Normal » Major

Wow, quite a speedup for a single line. Nice work.

dawehner’s picture

Well, my actual usecase was a 120 seconds view down to 1.5 seconds, but yeah, this was too crazy for a core issue

xjm’s picture

@dawehner and I chatted briefly about this point in IRC. One concern we discussed was cache invalidation and how cache bubbling played out in the REST view. (I imagine @dawehner will post more about that but just wanted to add a note here.)

dawehner’s picture

Right, its all about cache invalidation. The simple usecase of a rest view, which just renders some of the fields of an entity, works fine, as each row has their cache tags. The advanced usecases are the once which cause troubles. Just imagine you embed a view inside the main view to render some fields of the current row entity. Then you would need to take into account cache tags for that referenced entity as well. All that complex bubbling logic is solved by render API, but sadly it's something we don't have in REST views.

One potential solution @jaesin and others talked about is to introduce intermediate objects, which carry around the metadata for the field rendering and finally extract the data out later, before we pass the data to the serializer to render everything.

Wim Leers’s picture

+++ b/core/modules/rest/src/Plugin/views/row/DataFieldRow.php
@@ -152,7 +152,7 @@ public function render($row) {
-        $value = $field->advancedRender($row);
+        $value = $this->view->style_plugin->getField($row->index, $id);

Explain this dark magic?!


One potential solution @jaesin and others talked about is to introduce intermediate objects, which carry around the metadata for the field rendering and finally extract the data out later, before we pass the data to the serializer to render everything.

This sounds sensible, but it's hard to picture how that works exactly — especially considering how complex Views can be. Not even having taken into account how the Serializers work. I fear even a PoC would be a huge undertaking though? And could this even be done without breaking BC?

dawehner’s picture

Explain this dark magic?!

getField() calls out \Drupal\views\Plugin\views\style\StylePluginBase::renderFields which does some render caching one day.

Wim Leers’s picture

Aha, so in calling that other function, we're able to rely on a cache hit rather than doing the work. Makes sense. Thanks.

Wim Leers’s picture

Title: Row level caching doesn't exist for REST views » Row-level caching doesn't exist for REST views
dawehner’s picture

Exactly, now the only problem is that we don't have any kind of bubbleable cacheable metadata, just the one on the row itself. I was thinking about adding an advanced option,
so you can get this single line, in case you want to and you know what you do.

xjm’s picture

Version: 8.0.x-dev » 8.1.x-dev
Issue tags: +Triaged core major

All the core committers agreed that this issue should be major priority. We weren't entirely agreed it was a bug though, and it might be an 8.1.x issue depending on the full solution. Moving it to 8.1.x for now; if it turns out to be backportable we can look into that.

dawehner’s picture

To be honest the only thing I see possible, is to make it configurable. There is no clear API layer where we could opt out of caching.

dawehner’s picture

Status: Needs review » Needs work

This patch so far is obviously not enough!

Wim Leers’s picture

#2 shows a significant perf win. What was the test setup?

dawehner’s picture

#2 shows a significant perf win. What was the test setup?

I don't remember, but I guess its like 50 nodes in a rest feed or so. The actual site here decreased there numbers from 3 minutes to 1,5 seconds, but this was rendering the entirety of the entire site out with thousand of nodes.

Wim Leers’s picture

The actual site here decreased there numbers from 3 minutes to 1,5 seconds, but this was rendering the entirety of the entire site out with thousand of nodes.

:O Wow!

dawehner’s picture

Here is the basic approach I haven chosen for the client code:

Inside a custom field plugin which does complex stuff:

      $data = new RestFragmentData($result);
      $data->addCacheTags(['foo', 'bar']);
      $build = [
        '#markup' => $data->jsonSerialize(),
      ];

      CacheableMetadata::createFromObject($data)->applyTo($build);

      return $build;

Inside DataFieldRow:

        $value = $this->view->style_plugin->getField($row->index, $id);

        // Render caching converts RestDataFragment objects to its encoded json.
        // In order to get the data back, try to decode the json array.

        // First reset the last json error.
        json_decode(1);

        $data = @json_decode($value, TRUE);
        if (json_last_error() === 0) {
          if (is_array($data)) {
            array_walk_recursive($data, function (&$val) {
              $val = Html::decodeEntities((string) $val);
            });
          }
          $value = $data;
        }
      }

      $output[$this->getFieldKeyAlias($id)] = $value;

With RestFragmentData

class RestFragmentData implements MarkupInterface, CacheableDependencyInterface {

  use RefinableCacheableDependencyTrait;

  /**
   * The stored data.
   *
   * @var array
   */
  protected $data;

  /**
   * Creates a new RestFragmentData instance.
   *
   * @param array|string $data
   */
  public function __construct($data) {
    $this->data = $data;
  }

  /**
   * {@inheritdoc}
   */
  public function __toString() {
    return $this->jsonSerialize();
  }

  /**
   * {@inheritdoc}
   */
  public function jsonSerialize() {
    return json_encode($this->data);
  }

}

As we cannot enforce custom field plugins to do that crazy stuff, I don't see how this could be solved automatically without opting in as site builder.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.0-beta1 was released on March 2, 2016, which means new developments and disruptive changes should now be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Wim Leers’s picture

So, AFAICT, the core problem is that \Drupal\views\Plugin\views\style\StylePluginBase::renderFields() does:

          // Views may be rendered both inside and outside a render context:
          // - HTML views are rendered inside a render context: then we want to
          //   use ::render(), so that attachments and cacheability are bubbled.
          // - non-HTML views are rendered outside a render context: then we
          //   want to use ::renderPlain(), so that no bubbling happens
          if ($renderer->hasRenderContext()) {
            $renderer->render($data);
          }
          else {
            $renderer->renderPlain($data);
          }

(This was introduced in #2450897: Cache Field views row output.)

But OTOH, we did #2477157: rest_export Views display plugin does not set necessary cache metadata, which made this change to \Drupal\rest\Plugin\views\display\RestExport::buildResponse():


@@ -259,7 +276,16 @@ public function execute() {
     parent::execute();
 
     $output = $this->view->render();
-    return new Response(drupal_render_root($output), 200, array('Content-type' => $this->getMimeType()));
+
+    $header = [];
+    $header['Content-type'] = $this->getMimeType();
+
+    $response = new CacheableResponse($this->renderer->renderRoot($output), 200);
+    $cache_metadata = CacheableMetadata::createFromRenderArray($output);
+
+    $response->addCacheableDependency($cache_metadata);
+
+    return $response;
   }

… so I wonder if bubbling isn't actually simply … working? Let's find out which code is still relying on the else-case in the first quoted code.

dawehner’s picture

I don't get that patch, honestly. You at least have to combine it somehow with my previous patch, because well, there is no row level caching triggered at the moment. Yes, the metadata of each row bubble to the entire view, but this is not the point of this issue to be honest, see first sentence of the issue summary.

Status: Needs review » Needs work

The last submitted patch, 22: 2648268-22-lets_see_what_fails.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
1.38 KB
dawehner’s picture

To be clear, in the custom project I was working one we had a field plugin that returned raw data, and well it worked, so requiring cache ability metadata needs to be somehow supported and opt IN for this raw data.

Status: Needs review » Needs work

The last submitted patch, 25: 2648268-25-lets_see_what_fails.patch, failed testing.

Wim Leers’s picture

Title: Row-level caching doesn't exist for REST views » REST views: row-level caching doesn't exist, unlike for other types of views
dawehner’s picture

Let me clarify why I think this issue is quite problematic when we want to keep BC.
While the patch in #2 proves that the simple case actually doesn't break anything, here is the what happened on the client project while dealing with it.

The patch in #2 got implemented and boom nothing invalided anymore. We had though a custom field plugin which just rendered stuff and returned a string. This one was specifically targeted towards REST, so we haven't thought about cacheability yet at that point in time. When we introduce simply #2 we gonna hit that problem on other sites as well.

On top of that advancedRender also potentially renders via the theme() function, which includes the template used by field templates. This can be obviously be problematic
when someone has any kind of HTML in their field template.

Given to make this BC compatible we need the following steps IMHO:

  • Be able to skip field templates completly
  • Be able to opt out / opt in into row level caching. I would vote for opt IN for rest views.
Wim Leers’s picture

Issue tags: +D8 cacheability

Adding D8 cacheability tag based on #29.

I would vote for opt IN for rest views.

Everything else is opt out. Opt out is better, if it can be done without breaking BC. If that is not possible (it sounds like it is not), then opt in would be fine.

dawehner’s picture

Well in the case above opt OUT would have totally broken the site.

Wim Leers’s picture

Yep, that's what I thought. So then opt in it is.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Wim Leers’s picture

Category: Bug report » Feature request
Priority: Major » Normal

Almost a year has passed since this was last touched. Only 11 followers, half of which are the core developers on this issue. Nobody is facing this problem yet?

Deprioritizing.

The bigger problem is that you can currently only get ALL results for a REST view: there's no paging. See #2099281: [PP-1] REST views: pagination link relations and #2100637: REST views: add special handling for collections for that.

dawehner’s picture

The bigger problem is that you can currently only get ALL results for a REST view: there's no paging.

I honestly don't know who came up with that myth. You can just configure a view like any other view, and use ?page=1 and paginate. What doesn't work is having the appropriate link relations in the http header, which everyone would always use, right ;)

Wim Leers’s picture

Oh huh… I'd swear you told me that!

dawehner’s picture

Mh. Well at least #2099281: [PP-1] REST views: pagination link relations is certainly not major in that case.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.