Problem/Motivation

There is a significant performance degradation when views renders multi-value fields where each delta creates a row of output. It's most prevalent with fields that have expensive renderings, like date fields, but even for plain old text fields it's very noticeable.

Sample setup:
1. Content type with a multi-value text field
2. Single node with 100 values for the text field
3. A view, configured to render fields and display nodes of that type. The multi-value field is the only field rendered, and it's configured such that each delta creates a row of output. So 100 rows are output.

In my testing with the above setup, it takes a page with that view ~2.5 seconds to load from a cold render cache (when I tested with date fields, it was far worse). With a warm render cache it's very fast as expected.

Here is a public profile of the entire page execution courtesy of Blackfire: https://blackfire.io/profiles/9c2c3db8-e59b-4602-a127-bc4641b887f5/graph

70% of the time was spent in views rendering. FieldPluginBase::advancedRender was called 100 times, which called out to field plugin's renderItem method 10,000 times. It seems to be rendering all 100 field deltas for each 100 rows of output.

In the blackfire profile, it helps to click the magnifying glass on advancedRender so you can see the call tree after that. Blackfire leaves out calls in the default display.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

bkosborne created an issue. See original summary.

bkosborne’s picture

Issue summary: View changes
bkosborne’s picture

Issue tags: +Performance, +VDC
mpdonadio’s picture

Thanks for posting the profiling info. This is very weird. Even if all the field values are being rendered for each row, I don't see why the extra calls aren't being cached (since views caching is on by default).

mradcliffe’s picture

\Drupal\views\Entity\Render\EntityFieldRenderer::buildFields

This doesn't seem as efficient as it could be in the case that the same entity spans multiple rows. $entities_by_bundles and $bundle_entities would have the same entity multiple times, and entity view display gets created for the same one each time.

dawehner’s picture

This doesn't seem as efficient as it could be in the case that the same entity spans multiple rows. $entities_by_bundles and $bundle_entities would have the same entity multiple times, and entity view display gets created for the same one each time.

That for itself is a good observation. Rendering fields is incredible hard :) I'm happy for concrete suggestions here :)

On top of that I'm wondering whether we can optimise some of the rendering of date fields. Could \Drupal\Core\Datetime\DateFormatter for example store some static caching of the loaded date formatter?

bkosborne’s picture

Thanks for posting the profiling info. This is very weird. Even if all the field values are being rendered for each row, I don't see why the extra calls aren't being cached (since views caching is on by default).

Is there such a thing as views caching aside from render caching though?

On top of that I'm wondering whether we can optimise some of the rendering of date fields. Could \Drupal\Core\Datetime\DateFormatter for example store some static caching of the loaded date formatter?

So looks like the bulk of the slowdown when using the DateTimePlainFormatter (like in my example) is that it tries to load a non-existent formatter 20,000 times, when it shouldn't even try to do that because the formatter type is "custom" (there will never be a formatter stored with that name).

Simply wrapping the code that does the formatter lookup in an if statement like so:

    ...

    if ($type != 'custom') {
      // If we have a non-custom date format use the provided date format pattern.
      if ($date_format = $this->dateFormat($type, $langcode)) {
        $format = $date_format->getPattern();
      }

      // Fall back to medium if a format was not found.
      if (empty($format)) {
        $format = $this->dateFormat('fallback', $langcode)->getPattern();
      }
    }

    ...

... reduces the the total render time to 9 seconds, down from 22 or so.

So that's an easy win and removes the date component from the slowdown here, at least for the plain formatter.

bkosborne’s picture

Here's a separate issue I filed to fix the above issue with the date formatter: #2846643: DateFormatter should not attempt to load a format from storage if the format is "custom"

mpdonadio’s picture

Two thoughts about \Drupal\views\Entity\Render\EntityFieldRenderer::buildFields before I start on client work

  /**
   * A list of indexes of rows whose fields have already been rendered.
   *
   * @var int[]
   */
  protected $processedRows = [];

PhpStorm says this is not used. Not sure if that matters here.

      $entities_by_bundles = [];
      $field = $this->view->field[current($field_ids)];
      foreach ($values as $result_row) {
        if ($entity = $field->getEntity($result_row)) {
          $entities_by_bundles[$entity->bundle()][$result_row->index] = $this->getEntityTranslation($entity, $result_row);
        }
      }

I think this is the problem. We are building up an array of all of the entities we need, and then looping over then below. In this particular case, the array is filled with the same entity, so it is getting rendered out multiple times.

Not sure if we can fix it here, though. I think that in EntityViewDisplay::buildMultiple() we need to change the loop to see if a particular entity (or group?) has already been rendered, and if so reuse the results instead of just looping and rendering each entry.

?

bkosborne’s picture

Title: Terrible performance when rendering multi-value date fields in views » Terrible performance when rendering multi-value fields in views

Remove the "date" part from the title - I the main issue here is related to views, not so much the type of field

bkosborne’s picture

Version: 8.2.5 » 8.3.x-dev
dawehner’s picture

@bkosborne
Do you mind providing a profiling for the case without rendering a date field, but some other field?

bkosborne’s picture

Sure. I created a new content type with a multi-value plain text field. I created 100 text values for that field and configured views to render content of this type, outputting fields style, and outputting a row for each delta of the multi-value field. So, 100 rows output, each row outputting just the multi-value field instance. I also have dynamic page cache, internal page cache, and render cache disabled, to simulate cold cache start.

It takes ~2.5 seconds for the page to load.

Here's the profiling data in blackfire: https://blackfire.io/profiles/9c2c3db8-e59b-4602-a127-bc4641b887f5/graph

Note that blackfire seems to add some overhead, as it reports that it took the page 4.2 seconds to load. But in my tests in a browser it was ~2.5.

70% of the time was spent in views rendering. FieldPluginBase::advancedRender was called 100 times, which called out to field plugin's renderItem method 10,000 times. It seems to be rendering all 100 field deltas for each 100 rows of output.

In the blackfire profile, it helps to click the magnifying glass on advancedRender so you can see the call tree after that. Blackfire leaves out calls in the default display.

dawehner’s picture

Note that blackfire seems to add some overhead, as it reports that it took the page 4.2 seconds to load. But in my tests in a browser it was ~2.5.

Right, which is fair, as we want the additional information to be collected. It is all about the relative numbers.

70% of the time was spent in views rendering. FieldPluginBase::advancedRender was called 100 times, which called out to field plugin's renderItem method 10,000 times. It seems to be rendering all 100 field deltas for each 100 rows of output.

This is certainly wrong, no question.

Do you mind enhancing the issue summary with your latest results? This helps for new people to know what is going on.

bkosborne’s picture

Issue summary: View changes
bkosborne’s picture

Done. I stripped away most of the data related to the date fields and removed the screenshots, since it's much easier for people to just browse through the profiler that I linked to.

bkosborne’s picture

Trying to get some more attention on this. I'm going to aim to get some fresh profiling data again and see if I can figure things out once more. This is really killer for multi-day events =/

bkosborne’s picture

Here's fresh profiling data: https://blackfire.io/profiles/bf00c75c-161a-4705-acb2-f7805b5ea1cc/graph

- Drupal 8.3.2
- "Event" content type with one unlimited cardinality Date field (day and time)
- A view with a page display that renders fields. Output the title and the date field. Configure the date field NOT to display all fields in a single row (so you get a new row for each multi-value). Disable pager so it outputs everything
- Create a single event with 100 values for the date field.
- Observe that rendering the view from a cold cache takes ~10 second (depending on your env of course).

Here's some code you can run in drush php to populate your event node:

$dates = [];
for ($i = 1; $i <= 100; $i++) {
    $date = new \DateTime();
    $date->modify('+' . $i . 'days');
    $dates[] = $start_date->format(DATETIME_DATETIME_STORAGE_FORMAT);
}
$event = \Drupal::entityTypeManager()->getStorage('node')->create([
  'type' => 'event',
  'title' => 'Test',
  'field_date' => $dates
]);
$event->save();
bkosborne’s picture

Priority: Normal » Major

Seem appropriate to mark this as major, especially as more sites start implementing calendaring solutions?

bkosborne’s picture

For what it's worth, I spent time some looking at #1867518: Leverage entityDisplay to provide fast rendering for fields which is where much of this work for rendering fields was done. Looks like $processedRows was refactored away with this patch: https://www.drupal.org/node/1867518#comment-9786607 - Looks like the it just isn't relevant anymore and it was forgotten to remove it.

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

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should 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.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should 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.

drclaw’s picture

Came across this issue recently on a Multi-value text field with ~100 items. Render time was ~10 seconds for just the one field and it seemed a bit crazy so I dug in.

Was thinking that maybe this could be fixed by just running EntityField::prepareItemsByDelta() in EntityField::getItems() instead of in EntityField::renderItems(). The problem is that by the time the $items makes it to renderItems(), they have already been individually rendered so the damage is done before we pull out the specific items we're actually "rendering". With this change we filter the items by delta before they're actually rendered in advancedRender().

I've attached a patch to show what I'm thinking. I'm sure there's probably some repercussions here but I figure at least this gets the ball rolling!

mpdonadio’s picture

Status: Active » Needs review

Go, TestBot, go!

drclaw’s picture

Ah, yes thanks for that! And it passed! :)

mpdonadio’s picture

I think there are two things to think about here, assuming this is faster (it probably is).

1. Do we have enough test coverage around this?

2. Is this a BC break (esp for contrib or custom modules) by changing what is available in a views hook by pushing something later in the process. This isn't 100% apparent to me; needs some additional debug/tracing.

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

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should 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.

logickal’s picture

We have a site that's using a view for slideshow management that went from performing fine to slow enough to cause gateway timeouts in our containerized infrastructure with the 8.7 update. This patch certainly seems to help, but I wonder if some other change to either the rendering or caching systems have exacerbated these performance problems... so far, I haven't found any smoking gun, but this at least mitigates the issue.

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

Drupal 8.6.x will not receive any further development aside from security fixes. Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.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: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should 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.

joseph.olstad’s picture

Version: 8.9.x-dev » 9.1.x-dev
Status: Needs review » Reviewed & tested by the community

patch still applies, is still great!

larowlan’s picture

Status: Reviewed & tested by the community » Needs work

The questions in #26 have not been answered yet

larowlan’s picture

Status: Needs work » Needs review
joseph.olstad’s picture

answering #26

getItems is called before renderItems , and proof is that renderItems takes $items as a parameter

so the nervousness mentioned in #26 is invalid.

"zero" reports of any issues with the patch after 2 years since that the question was posed. We need speed indeed!

Views especially benefits here because we cannot always rely on caching to keep our availability up and our latency low! Performance fixes such as this one actually improve security, lower latency, improves availability, more clients we can serve concurrently.

Win-win here.

gdaw’s picture

Status: Needs review » Reviewed & tested by the community

Good point Joseph, and besides this has matured very well and still works great. RTBC +1

alexpott’s picture

I think this change makes a lot of sense. I've looked at the code and can't see any reason not to limit the items prior to rendering each individual item. As this is the view rendering pipeline and therefore very complex I'm going to ping view maintainers and ask for a +1 - just in case I'm missing something.

bkosborne’s picture

Issue summary: View changes
Lendude’s picture

This looks good from a Views rendering pipeline perspective.

We have test coverage for prepareItemsByDelta in:
\Drupal\Tests\views\Kernel\Handler\FieldFieldTest::testComplexRender
\Drupal\Tests\views\Kernel\Handler\FieldFieldTest::testRevisionComplexRender
So we know calling that at a different moment doesn't break anything

And the whole rendering in the rest of \Drupal\Tests\views\Kernel\Handler\FieldFieldTest

So that seems covered.

The daily test is running against 8.5.x though, so we might want to make sure it is still green for 9.1.x. I tried re-queuing it, but when I choose 9.1 it still queues 8.5 ¯\_(ツ)_/¯

andypost’s picture

As a bug it could be backported

joseph.olstad’s picture

credit to @drclaw for patch 23
re-uploading patch 23 verbatim just to trigger tests again. No change made.

patch applies cleanly to all currently active branches , 8.8.x, 8.9.x, 9.0.x , 9.1.x

***EDIT*** something weird with the testbot, will wait and see what happens once they all run but it looks like I triggered everything on 9.1.x even though I selected other branches.

Status: Reviewed & tested by the community » Needs work
joseph.olstad’s picture

Status: Needs work » Reviewed & tested by the community

testbot issues

longwave’s picture

Re #40 there is something wrong with queuing issues against different branches, it happened to me in another issue. Reported at #3172023: Testing different branches other than the issue branch has stopped working

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 074e921 and pushed to 9.1.x. Thanks!

Committing this to 9.1.x only for now - views is complex and it wouldn't be the first time a patch that looks very non-disruptive proved otherwise so going to let this issue sit in 9.1.x for a bit to see if anything breaks. If nothing does we should re-open this for back port to 9.0.x and 8.9.x because the current behaviour to unnecessarily render rows and therby cause timeouts is a bug.

  • alexpott committed 074e921 on 9.1.x
    Issue #2846485 by joseph.olstad, drclaw, bkosborne, mpdonadio, Lendude:...

Status: Fixed » Closed (fixed)

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

xjm’s picture

Issue tags: +9.1.0 highlights