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.
Comment | File | Size | Author |
---|---|---|---|
#40 | views-multivalue_field_rendering_performance-2846485-23.patch | 963 bytes | joseph.olstad |
#23 | views-multivalue_field_rendering_performance-2846485-23.patch | 963 bytes | drclaw |
Comments
Comment #2
bkosborneComment #3
bkosborneComment #4
mpdonadioThanks 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).
Comment #5
mradcliffe\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.
Comment #6
dawehnerThat 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?Comment #7
bkosborneIs there such a thing as views caching aside from render caching though?
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:
... 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.
Comment #8
bkosborneHere'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"
Comment #9
mpdonadioTwo thoughts about \Drupal\views\Entity\Render\EntityFieldRenderer::buildFields before I start on client work
PhpStorm says this is not used. Not sure if that matters here.
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.
?
Comment #10
bkosborneRemove the "date" part from the title - I the main issue here is related to views, not so much the type of field
Comment #11
bkosborneComment #12
dawehner@bkosborne
Do you mind providing a profiling for the case without rendering a date field, but some other field?
Comment #13
bkosborneSure. 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'srenderItem
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.
Comment #14
dawehnerRight, which is fair, as we want the additional information to be collected. It is all about the relative numbers.
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.
Comment #15
bkosborneComment #16
bkosborneDone. 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.
Comment #17
bkosborneTrying 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 =/
Comment #18
bkosborneHere'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:Comment #19
bkosborneSeem appropriate to mark this as major, especially as more sites start implementing calendaring solutions?
Comment #20
bkosborneFor 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.
Comment #23
drclaw CreditAttribution: drclaw at Fuse Interactive commentedCame 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()
inEntityField::getItems()
instead of inEntityField::renderItems()
. The problem is that by the time the$items
makes it torenderItems()
, 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 inadvancedRender()
.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!
Comment #24
mpdonadioGo, TestBot, go!
Comment #25
drclaw CreditAttribution: drclaw at Fuse Interactive commentedAh, yes thanks for that! And it passed! :)
Comment #26
mpdonadioI 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.
Comment #28
logickal CreditAttribution: logickal at The Weather Company commentedWe 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.
Comment #31
joseph.olstadpatch still applies, is still great!
Comment #32
larowlanThe questions in #26 have not been answered yet
Comment #33
larowlanComment #34
joseph.olstadanswering #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.
Comment #35
gdaw CreditAttribution: gdaw as a volunteer commentedGood point Joseph, and besides this has matured very well and still works great. RTBC +1
Comment #36
alexpottI 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.
Comment #37
bkosborneComment #38
LendudeThis 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 ¯\_(ツ)_/¯
Comment #39
andypostAs a bug it could be backported
Comment #40
joseph.olstadcredit 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.
Comment #42
joseph.olstadtestbot issues
Comment #43
longwaveRe #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
Comment #44
alexpottCommitted 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.
Comment #47
xjm