Proposed commit message

Issue #1867518 by plach, dawehner, epari.siva, Fabianx, yched, Wim Leers, marcvangend, iMiksu: Leverage entityDisplay to provide fast rendering for fields

Problem/Motivation

Rendering entity fields in views works like the following:

For reach row, and for each entity field execute the following code:

$entity->$field_name->view();

This creates a lot of temporary objects, as some benchmarks here and in #2342045: Standard views base fields need to use same rendering as Field UI fields, for formatting, access checking, and translation consistency has shown.

Proposed resolution

Based on #1852966: Rework entity display settings around EntityDisplay config entity views could use the entityDisplay object to render with better performance.

Basically, the idea is a View should :

  • run its query
  • load the corresponding entities and group them by entity type
  • for each entity type
    • create a runtime EntityViewDisplay object
    • render each field for all entities in one pass via EntityViewDisplay::buildFieldMultiple() and store the result
  • for each row/entity and each field in the row, pick the field renderable array in the $build array.

Remaining tasks

  • Validate the current solution
  • Write a patch
  • Reviews

User interface changes

None

API changes

Additional methods EntityViewDisplay::buildField() and EntityViewDisplay::buildFieldMultiple() that instantiate field formatters on demand without needing an EVD configuration entity.

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Task becausethe functionality is not broken
Issue priority Critical because of the performance improvements introduced.
Disruption Disruptive for contributed/custom classes extending \Drupal\views\Plugin\views\field\Field::getItems().
CommentFileSizeAuthor
#264 views-entity_display-1867518-264.patch84.57 KBplach
#264 views-entity_display-1867518-264.interdiff.txt4.7 KBplach
#261 views-entity_display-1867518-261.patch83.29 KBplach
#261 views-entity_display-1867518-261.interdiff.txt10.91 KBplach
#256 views-entity_display-1867518-255.patch82.85 KBplach
#255 views-entity_display-1867518-255.interdiff.txt2.28 KBplach
#254 views-entity_display-1867518-254.patch82.27 KBplach
#254 views-entity_display-1867518-254.interdiff.txt2.33 KBplach
#250 views-entity_display-1867518-249.patch79.94 KBplach
#250 views-entity_display-1867518-249.interdiff.txt2.84 KBplach
#248 interdiff.txt1.45 KBdawehner
#248 1867518-247.patch81.45 KBdawehner
#240 interdiff.txt4.49 KBdawehner
#240 1867518-240.patch81.29 KBdawehner
#240 Screen Shot 2015-04-28 at 16.58.23.png35.27 KBdawehner
#233 views-entity_display-1867518-229.interdiff.txt5.76 KBplach
#229 views-entity_display-1867518-229.patch78.54 KBiMiksu
#229 1867518-229-interdiff.txt2.79 KBiMiksu
#223 views-entity_display-1867518-223.patch75.1 KBplach
#223 views-entity_display-1867518-223.interdiff.txt1.9 KBplach
#223 views-entity_display-1867518-223-fail.patch75.04 KBplach
#221 views-entity_display-1867518-221.patch74.46 KBplach
#219 views-entity_display-1867518-219.patch74.5 KBplach
#219 views-entity_display-1867518-219.interdiff.txt837 bytesplach
#218 evd_heavy_test.yml18.58 KBplach
#216 views-entity_display-1867518-216.patch74.44 KBplach
#216 views-entity_display-1867518-216.interdiff.txt7.87 KBplach
#203 views-entity_display-1867518-202.patch74.34 KBplach
#203 views-entity_display-1867518-202.interdiff.txt11.9 KBplach
#200 views-entity_display-1867518-199.patch81.32 KBplach
#200 views-entity_display-1867518-199.interdiff.txt9.93 KBplach
#199 views-entity_display-1867518-198.patch81.59 KBplach
#199 views-entity_display-1867518-198.interdiff.txt5.78 KBplach
#2 drupal-1867518-2.patch5.82 KBdawehner
#24 views-entity_display-1867518-24.patch12.75 KBplach
#28 views-entity_display-1867518-28.interdiff.txt7.59 KBplach
#28 views-entity_display-1867518-28.patch12.82 KBplach
#31 views-entity_display-1867518-31.interdiff.txt7.44 KBplach
#31 views-entity_display-1867518-31.patch13.35 KBplach
#39 views-entity_display-1867518-39.interdiff.txt10.78 KBplach
#39 views-entity_display-1867518-39.patch17.22 KBplach
#40 views-entity_display-1867518-40.interdiff.txt2.48 KBplach
#40 views-entity_display-1867518-40.patch17.22 KBplach
#41 views-entity_display-1867518-41.patch17 KBplach
#45 views-entity_display-1867518-44.interdiff.txt4.26 KBplach
#45 views-entity_display-1867518-44.patch19.29 KBplach
#47 views-entity_display-1867518-47.interdiff.txt10.87 KBplach
#47 views-entity_display-1867518-47.patch21.91 KBplach
#47 drupal_8x.sql_.gz2.49 MBplach
#48 views-entity_display-1867518-48.interdiff.txt2.41 KBplach
#48 views-entity_display-1867518-48.patch24.32 KBplach
#54 views-entity_display-1867518-54.patch37.75 KBplach
#56 views-entity_display-1867518-56.interdiff.txt3.38 KBplach
#56 views-entity_display-1867518-56.patch38.82 KBplach
#58 1867518-58.patch50.05 KBdawehner
#58 interdiff.txt11.49 KBdawehner
#60 1867518-60.patch58.93 KBdawehner
#60 interdiff.txt9.58 KBdawehner
#64 1867518-64.patch59.7 KBdawehner
#64 interdiff.txt789 bytesdawehner
#66 1867518-66.patch71.26 KBdawehner
#66 interdiff.txt20.68 KBdawehner
#68 1867518-68.patch71.45 KBdawehner
#68 interdiff.txt863 bytesdawehner
#73 interdiff.txt2.77 KBdawehner
#73 1867518-73.patch71.68 KBdawehner
#86 1867518-84.patch75.78 KBdawehner
#86 interdiff.txt5.52 KBdawehner
#89 views-entity_display-1867518-89.interdiff.txt5.91 KBplach
#89 views-entity_display-1867518-89.patch78.43 KBplach
#93 views-entity_display-1867518-93.interdiff.txt4.49 KBplach
#93 views-entity_display-1867518-93.patch78.83 KBplach
#104 1867518-104.patch82.82 KBdawehner
#104 interdiff.txt25.62 KBdawehner
#111 leverage_entitydisplay-1867518-111.patch82.92 KBsiva_epari
#121 views-entity_display-1867518-121.interdiff.txt20.39 KBplach
#121 views-entity_display-1867518-121.patch85.13 KBplach
#125 views-entity_display-1867518-124.interdiff.txt1.64 KBplach
#125 views-entity_display-1867518-124.patch86.22 KBplach
#127 views-entity_display-1867518-127.interdiff.txt8.02 KBplach
#127 views-entity_display-1867518-127.patch85.87 KBplach
#127 test_view.yml22.96 KBplach
#127 drupal_8x.sql_.gz1.86 MBplach
#128 views-entity_display-1867518-127.profile.txt6.31 KBplach
#129 views-entity_display-1867518-129.interdiff.txt2.66 KBplach
#129 views-entity_display-1867518-129.patch86.11 KBplach
#134 views-entity_display-1867518-133.interdiff.txt2.35 KBplach
#134 views-entity_display-1867518-133.patch86.33 KBplach
#137 views-entity_display-1867518-137.interdiff.txt1.66 KBplach
#137 views-entity_display-1867518-137.patch86.23 KBplach
#140 views-entity_display-1867518-140.interdiff.txt1.9 KBplach
#140 views-entity_display-1867518-140.patch86.28 KBplach
#142 views-entity_display-1867518-142.interdiff.txt700 bytesplach
#142 views-entity_display-1867518-142.patch86.96 KBplach
#158 1867518_views-entity_display-158.patch87.67 KByched
#158 interdiff.txt2.61 KByched
#160 views.view_.fields_test.yml12.41 KBmarcvangend
#160 1867518-158-profiler-diff.png164.93 KBmarcvangend
#162 1867518_views-EVD-162.patch87.41 KByched
#162 interdiff.txt4.87 KByched
#164 1867518_views-EVD-163.patch87.37 KByched
#164 interdiff.txt1.41 KByched
#168 interdiff.txt5.22 KByched
#169 1867518_views-EVD-168.patch88.7 KByched
#171 1867518_views-EVD-171.patch88.85 KByched
#171 interdiff.txt2 KByched
#172 1867518_views-EVD-172.patch88.5 KByched
#172 interdiff.txt4.14 KByched
#175 1867518_views-EVD-175.patch87.99 KByched
#175 interdiff.txt4.22 KByched
#181 1867518-views_EVD-181.patch81.34 KByched
#181 interdiff.txt10.25 KByched
#183 1867518-views_EVD-182-skip_EVD.patch88.05 KByched
#183 1867518-views_EVD-182-use_EVD.patch81.34 KByched
#183 interdiff-skip_EVD-to-use_EVD.txt11.53 KByched
#189 1867518-views_EVD-189-use_EVD.patch82.6 KByched
#189 interdiff.txt5.18 KByched
#197 XHProf__Hierarchical_Profiler_Report 2.png108.11 KBplach
#197 XHProf__Hierarchical_Profiler_Report 3.png107.17 KBplach
#197 XHProf__Hierarchical_Profiler_Report.png109.04 KBplach
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

yched’s picture

#1852966: Rework entity display settings around EntityDisplay config entity should hopefully get in soon.

I'm not sure where this would fit in the views rendering flow, but algorithmically this would be something like this :

After query results :

// Split results by bundle, create one display object per bundle:
foreach bundle in the result set {
  // create a $display object: 
  $displays[$bundle] = entity_create('entity_display', array(
    'targetEntityType' => $entity_type, 
    'bundle' => $bundle,
    'viewMode' => arbitrary (view name + display name ?)
  );
  // Configure display options for the fields in the view:
  foreach field {
    $displays[$bundle]->setComponent($field_name, $options);
  }
}
// Run prepare_view on all result entities :
// $entities should be keyed by entity ID, and $displays by bundle name.
field_attach_prepare_view($entity_type, $entities, $displays);

Once per row :

// Build render arrays for all fields in the entity.
$output[$entity->id()] = field_attach_view($entity_type, $entity, $displays[$entity->bundle()]);

Once per field :

// Grab the correct render array
$field_output = $output[$entity->id()][$field_name];

Also, one drawback is this works once per field. If the field is present more than once in the view (e.g first time with 1st value, 2nd time with other values), then the non-first occurrences need to be displayed with the good old field_view_field().

dawehner’s picture

FileSize
5.82 KB

Thank you for that help, it has been really helpful.

Also, one drawback is this works once per field. If the field is present more than once in the view (e.g first time with 1st value, 2nd time with other values), then the non-first occurrences need to be displayed with the good old field_view_field().

So currently it prepares the entity by removing items and then render it. Wouldn't it possible to render first and then remove the items from the render array?

The current patch just tracks some work, though at least for a single field this already outputs as expected.

yched’s picture

From a quick look at the patch:

$displays[$bundle]->setComponent($this->definition['field_name'], $this->options['settings']);

Should rather be something like :

$displays[$bundle]->setComponent($this->definition['field_name'], array(
  'label' => 'hidden',
  'type' => $this->options['type'],
  'settings' => $this->options['settings'],
));

(basically, the array you currently pass to field_view_field())

So currently it prepares the entity by removing items and then render it. Wouldn't it possible to render first and then remove the items from the render array?

I don't think I get the question actually :-).
field_attach_prepare_view() lets field types and formatters massage the field values before doing the rendering entity by entity? This is where stuff like "load the term/file corresponding to the tid/fid" happens (on multiple entities because it's more efficient for loading stuff). It usually adds rather than remove stuff.

yched’s picture

(yched) Also, one drawback is this works once per field. If the field is present more than once in the view (e.g first time with 1st value, 2nd time with other values), then the non-first occurrences need to be displayed with the good old field_view_field().
(dawehner) So currently it prepares the entity by removing items and then render it. Wouldn't it possible to render first and then remove the items from the render array?

Ah, sorry, I think I get what you meant now. Like instead of rendering 1st value and "other values" separately, could we render all values once, and dispatch sub render array pieces accordingly ?
That wouldn't work, because the two occurrences of the field might be configured to use a different formatter, with different settings, etc.. Rendering needs to happen separately.

However, instead of resorting to field_view_field() for fields that are present more than once in the view, I guess another solution would be to open up new entity_display objects for those, and run f_a_prepare_view() / f_a_view() on each separately.
Like : open N entity_display objects, and put the Nth occurrence of each field in the Nth display. Thus, each display has each field at most once, and you get as much display objects as the field that is most repeated.
Code will probably be a bit more complex :-(, but should still be way faster than individual f_v_f() calls.

entity_display and f_a_*view() can only handle a given field once per entity. The ability to have a field rendered more than once (that goes along with the ability to limit the display to some deltas) is only offered by Views, not by the regular "Manage Display" flow.
It would be interesting to alleviate that limitation on the Field UI / Field API side, and I'm keeping an eye towards that in the EntityDisplay work, but it's still highly hypothetical :-).

dawehner’s picture

Good point. So we might should figure out first which fields could be rendered together and then run the actual rendering.

This could interfere with the grouping functionality but we already workaround in the code.
I guess for that special case we need proper testing first.

yched’s picture

"figure out first which fields could be rendered together": Maybe - dunno if it's really worth the trouble though :-)

dawehner’s picture

Did some work, though it's currently totally in the work, so don't expect anything.

field_attach_prepare_view() lets field types and formatters massage the field values before doing the rendering entity by entity? This is where stuff like "load the term/file corresponding to the tid/fid" happens (on multiple entities because it's more efficient for loading stuff). It usually adds rather than remove stuff.

Just to summarize: There are multiple use cases to have the same field rendered multiple times in a view:

  1. You want to render certain pieces with another formatter/another formatter setting
  2. You want to render single items separately but with the same rendered output
  3. You want to render separately with different formatter settings

So in the end we could group the fields in the following schema, assuming that field_B and field_C exists (2, 3) times. This doesn't seem to be too complicated but yeah let's discuss that whether it actually happens often, but I think it would be better to have a single code not special cases in the code.

view:display:field_0
  - field_A_0
  - field_B_0
  - field_C_0
view:display:field_1
  - field_B_1
  - field_C_1
view:display:field_2
  - field_C_2

so we end up having the least required rendering. Do you think this could work?

yched’s picture

Not sure I fully get the differences between the use cases you mention, but other than that, pretty much agreed :-)

I think it would be better to have a single code not special cases in the code.

Yup, +1.

view:display:field_0
  - field_A_0
  - field_B_0
(etc)

This describes the various entity_display objects, right ? If so, yes, that should work.
What are the "view:display:field_0, view:display:field_1, ..." strings ?

dawehner’s picture

Some additional work I did showed some more problems:

  • You have to take relationships into account, so you theoretical have to load them and render the fields of them.
  • If fields use groupby they might have total different "entity" per field.

It seems to be that we should first try to just get the non-groupby part working and then maybe even write a total different codepath for it, as it might make it simpler to split them up.

yched’s picture

- relationships: right, the field values that get displayed in a row do not necessarily belong to the same entity, so the steps described in #1 probably have to be wrapped into:
"foreach relation on which there are fields, separately load the entities / build the displays / field_attach_[prepare]_view() them"...
Although probably less convoluted, I guess the current code has to account at least for related entity loading, since field_view_field() currently requires an $entity ?
- I'm less familiar with groupby and its implications :-/

yched’s picture

Issue summary: View changes

Bumping, @msonnabaum cried at the perf of field-based Views in #2346763: Improve views field rendering performance by caching entity display objects.

Patch over there brings valid improvements that make sense for the generic case of "some code needs to do individual calls to $item->view() repeatedly on the same Field with different values", but I think the plan we had for view here still stands and brings more improvements.

Updated the issue summary with a brief version of the actual plan :-)

yched’s picture

Status: Postponed » Active

It is a fact that this never made it to the top of anyone's todo list so far :-), but other than that I don't think there's any reason for it being marked "postponed" ?

dawehner’s picture

Assigned: dawehner » Unassigned

I hate the assigned field.

Wim Leers’s picture

Priority: Normal » Major
Issue tags: +D8 cacheability, +Performance

If this can actually improve performance, it'd be major at least. Tentatively marking it as such.

Besides performance, I think this could significantly unify/simplify the Views render pipeline.

moshe weitzman’s picture

Took me a few readings, and a chat with Wim, but I now understand whats being proposed here. Sounds like a nice refactor to me. Don't think I have enough command of Views and Entity render systems to bring it home, but I'm happy to review patches.

yched’s picture

Yes, that's probably why this issue has been sitting here so long. It requires pretty intimate knowledge of the Views and Entity rendering pipeline, and never reached the top of the todo list any of the interested parties :-)

catch’s picture

This sounds great to me. I'd probably leave it major even if it doesn't improve performance much, standardizing Views rendering would be really valuable by itself.

jhodgdon’s picture

Just wanted to point out:
#2394883: Language setup for entity and field based rendering in views is independent, confusing UI, lacking test coverage
Marking related... not sure they're the same but there's definitely some commonality. On that other issue, there's been some discussion about how to unify the language display settings for Field and Entity based views, and I think it relates to this discussion? Maybe anyway.

Wim Leers’s picture

#18: great catch; seems like there is commonality indeed!

dawehner’s picture

Assigned: Unassigned » plach

Assign to plach as he is working on it.

Wim Leers’s picture

#21 To clarify: #2450151: Don't try to render all fields (including hidden ones) for single entity display is an additional optimization, not an alternative to this issue, right?

plach’s picture

Yes, additional :)

plach’s picture

Status: Active » Needs review
FileSize
12.75 KB

Here is a first draft. It addresses only the basic scenarios: fields displayed multiple times and relationships still to do. This is roughly based on #2, but the amount of lines changed is so high that I'm not posting an interdiff.

Let's see if we can get at least some green test ;)

Wim Leers’s picture

Looking great already :)

Initial review.

  1. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -114,6 +114,27 @@ class Field extends FieldPluginBase implements CacheablePluginInterface, MultiIt
    +   * All fields being rendered.
    

    Actually a static cache for ::getRenderableFieldNames().

  2. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -700,6 +721,174 @@ public function renderItems($items) {
    +   * Renders all fields of all rows using entity display objects.
    

    Could use an @see to \Drupal\Core\Entity\Entity\EntityViewDisplay.

  3. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -700,6 +721,174 @@ public function renderItems($items) {
    +        foreach ($field_names as $field_name) {
    +          if ($entity->hasField($field_name)) {
    +            foreach ($entity->get($field_name) as $item) {
    +              $item->_attributes = array();
    +            }
    +          }
    +        }
    

    This didn't exist before. What is this doing exactly? It's resetting $item->_attributes for every field that we're going to render. Why?

  4. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -700,6 +721,174 @@ public function renderItems($items) {
    +        $bundle = $entity->bundle();
    +        if (!isset($bundles[$bundle])) {
    +          $bundles[$bundle] = $bundle;
    +          $bundle_fields[$bundle] = array_intersect($field_names, array_keys($entity->getFieldDefinitions()));
    +        }
    

    This determines the fields to render per bundle, so we can reuse the EntityViewDisplay objects across rows whose associated main entity use the same bundle.

    So, in the optimal case, this allows us to build only a single EntityViewDisplay for the entire view, if all rows in the view have entities of the same bundle.

  5. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -700,6 +721,174 @@ public function renderItems($items) {
    +  protected function getRenderableFieldNames() {
    

    Would getViewFieldToEntityFieldMapping() be clearer? That unambiguously communicates what this is about.

  6. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -700,6 +721,174 @@ public function renderItems($items) {
    +   *   A view mode name.
    

    A custom view mode name.

  7. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -700,6 +721,174 @@ public function renderItems($items) {
    +   * @param string[] $bundles
    

    Is this parameter really necessary? AFAICT it can be derived by doing array_keys($bundle_fields?

  8. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -700,6 +721,174 @@ public function renderItems($items) {
    +   *   nested array is keyed by Views field plugin name.
    

    s/by/by a/

  9. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -700,6 +721,174 @@ public function renderItems($items) {
    +    $displays = array();
    

    s/array()/[]/

    :)

  10. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -700,6 +721,174 @@ public function renderItems($items) {
    +    // Create on entity display object per bundle, so you can render entities
    +    // in different bundles at the same time.
    

    Not really "render", but *prepare*.

  11. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -700,6 +721,174 @@ public function renderItems($items) {
    +      // @todo: Here you would sort of iterate over all of the fields.
    +      foreach ($bundle_fields[$bundle] as $view_field_name => $field_name) {
    

    Isn't this already doing that? :)

Status: Needs review » Needs work

The last submitted patch, 24: views-entity_display-1867518-24.patch, failed testing.

dawehner’s picture

plach’s picture

Status: Needs work » Needs review
FileSize
7.59 KB
12.82 KB

Thanks for the review!

3: It's required to call entity prepare view, see the follow-up above
4: Yep :)
5: Mmh, I'm not very convinced about that: actually the important thing is that it returns a list of fields to act on, the fact that they are keyed by field plugin name is only an optimization

Test failures still to be addressed.

Status: Needs review » Needs work

The last submitted patch, 28: views-entity_display-1867518-28.patch, failed testing.

yched’s picture

Sad that we're kind of forced into this by the threat of a massive perf regression, but psyched to see this move forward :-)
@plach+++++++++++++

Couple remarks after a cursory look.

  1. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -700,6 +721,170 @@ public function renderItems($items) {
    +  protected function preRenderEntityFields(array &$values) {
    

    Nitpick : it's called by preRender(), but if it does the rendering, shouldn't it be called renderEntityFields() rather than preRender...() ?
    You cannot pre-render a Field API field, you can only render it ;-)

  2. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -700,6 +721,170 @@ public function renderItems($items) {
    +        // FIXME One problem with using process_entity before rendering is that
    +        //   this might interfere with other instances of this field on the
    +        //   view.
    +        $entity = $this->getEntity($row);
    +        $entity = $entity ? $this->process_entity($row, $entity) : FALSE;
    

    Yeah, I guess that is the trickiest part of the task here, one arbitrary field handler is in charge of rendering all the other fields in the view, probably leads to a weird unbalance.

    Since we tend to move to "all displayed components of a ContentEntity view are fields", I wonder if it means the logic of "render all the fields in the view" could move one level up to the row handler ?
    (I'm not really familiar with the views pipeline, maybe this doesn't really make sense)

  3. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -709,43 +894,37 @@ public function renderItems($items) {
    +        foreach (Element::children($build) as $count) {
    

    for consistency with what we generally do with field items and rendered items, can we s/$count/$delta ?

  4. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -709,43 +894,37 @@ public function renderItems($items) {
    +          // FieldItemListInterface::view() adds an #access property to the
    +          // render array that determines whether or not the current user is
    +          // allowed to view the field in the context of the current entity. We
    +          // need to respect this parameter when we pull out the children of the
    +          // field array for rendering.
    +          if (isset($build['#access'])) {
    +            $items[$count]['rendered']['#access'] = $build['#access'];
    +          }
    

    Comment needs to be adjusted, we don't call FieldItemList::view() anymore but EVD::build().

    It does set an #access property on $build (i.e $output[$entity_id][$field_name]) that we need to push down to each delta, so the code doesn't look like it needs to change.

  5. +++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
    @@ -316,8 +316,7 @@ public function buildComponents(array &$build, array $entities, array $displays,
    -    // Invoke hook_entity_prepare_view().
    -    $this->moduleHandler()->invokeAll('entity_prepare_view', array($this->entityTypeId, $entities, $displays, $view_mode));
    +    $this->prepareView($entities, $displays, $view_mode);
    
    @@ -329,6 +328,14 @@ public function buildComponents(array &$build, array $entities, array $displays,
    +  public function prepareView($entities, $displays, $view_mode) {
    +    // Invoke hook_entity_prepare_view().
    +    $this->moduleHandler()->invokeAll('entity_prepare_view', array($this->entityTypeId, $entities, $displays, $view_mode));
    +  }
    
    +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -700,6 +721,170 @@ public function renderItems($items) {
    +      $view_builder->prepareView($entities, $displays, $this->getCustomViewMode());
    

    No biggie, but it feels a bit weird that we need an EntityViewBuilder just for that ?

    More importantly : I guess "per-field views" didn't call hook_entity_prepare_view() before, and now they do. Not sure of the exact consequences ?

plach’s picture

Status: Needs work » Needs review
FileSize
7.44 KB
13.35 KB

This should fix some failures and (partially) address #30.

@yched:

2: I cannot vouch on that too, I can only say it will involve many changes. @Daniel, what do you think?
5: I think we need entity prepare view when displaying fields using their native template, otherwise for instance RDF won't work. I didn't test this yet, though.

Status: Needs review » Needs work

The last submitted patch, 31: views-entity_display-1867518-31.patch, failed testing.

Fabianx’s picture

  1. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -709,43 +900,44 @@ public function renderItems($items) {
    -    if (!$entity) {
    -      return array();
    ...
     
    ...
    +    if ($entity && isset($output[$values->index][$field_name])) {
    +      $build = $output[$values->index][$field_name];
    

    Can we leave the early return please.

    That makes the function nicer to read.

  2. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -709,43 +900,44 @@ public function renderItems($items) {
    +    $entity = $this->getEntity($values);
    +    $output = $this->getFieldOutput();
    +    $field_name = $this->definition['field_name'];
    ...
    +    // Make sure entity fields are actually rendered for the current row.
    +    if (!isset($output[$values->index])) {
    +      $this->renderEntityFields([$values]);
    +      $output = $this->getFieldOutput();
    ...
    -    $render_array = $entity->get($this->definition['field_name'])->view($display);
    

    I can see why its needed, but I think I would prefer a function that makes the caching of the EntityDisplay implicit rather than explicit, e.g.

    $output = $this->renderField($entity, [$values]);

    which then takes all the logic of rendering the entity and fields.

    And if nothing from this _field_ is needed here (e.g. if another class can access the field information needed to build the display), this could even be another class that is called in this function.

    $output = $this->entityRenderer->renderFields($entity,[$values]);
    return $output

    The advantage is that all the logic is encapsulated and theoretically changeable.

  3. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -709,43 +900,44 @@ public function renderItems($items) {
    +        foreach (Element::children($build) as $delta) {
    +          $items[$delta]['rendered'] = $build[$delta];
    

    Thanks for using $delta, using $count made me look three times, before I got that an index or delta was meant :).

Overall looks really nice already. I think most logic should live in another class, which caches the information in the view's output and lazy draws the fields.

This makes it also easier to re-factor it so that the logic lives earlier in the chain later.

dawehner’s picture

  1. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -700,6 +721,170 @@ public function renderItems($items) {
    +   */
    +  protected function preRenderEntityFields(array &$values) {
    +    // We need to prepare field output just once.
    

    I agree with fabianx that it would be great to collect this logic into its own class. ... I think it would also make it easier to understand, this already gigantic file,
    but I disagree with putting that onto the row plugin, as @yched suggested. Its not the responsibility and would increase the coupling. I guess we simply want something new.

  2. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -700,6 +721,170 @@ public function renderItems($items) {
    +          if (!isset($bundles[$bundle])) {
    +            $bundle_field_names[$bundle] = array_intersect($field_names, array_keys($entity->getFieldDefinitions()));
    +          }
    

    I honestly don't get, why we need this in the first place. Why do we need the intersect?

  3. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -700,6 +721,170 @@ public function renderItems($items) {
    +        if ($field instanceof self) {
    

    can we have static here?

yched’s picture

I agree with fabianx that it would be great to collect this logic into its own class. ... I think it would also make it easier to understand, this already gigantic file, but I disagree with putting that onto the row plugin, as @yched suggested

Sure, suggesting the row handler was just a shot in the dark, since the logic here is about "collect data and do the rendering for all the rows, then assign the right pieces row by row"

Wherever out of the field handler is fine by me :-)

dawehner’s picture

Manually testing the patch worked fine (beside rendering the default translation field)
After that I tried to do some profiling, so i tested HEAD, #2342045: Standard views base fields need to use same rendering as Field UI fields, for formatting, access checking, and translation consistency as well as this patch on top of #2342045: Standard views base fields need to use same rendering as Field UI fields, for formatting, access checking, and translation consistency

HEAD: https://blackfire.io/profiles/fcbb35e6-a092-47ca-b665-f9758287e253/graph (840ms)
PATCH (2342045): https://blackfire.io/profiles/cd3fe95c-db3d-4425-aea7-f27b3973690c/graph (1.32 s)
PATCH + PLACH (this issue): https://blackfire.io/profiles/0134b0c0-475c-4c82-aa30-ca9425edc8d0/graph (1.17s)

Notes:
From these 1.17 seconds we call spend 138ms in BubbleableMetadata::merge() and have ~6000 calls to Container::get() (40ms exclusive),
1300 calls to ModuleHandler::alter() (26ms exclusive) and 1100 calls to inheritCacheability (25ms exclusive). I hope we can solve those, as this should resolve a big
gap (28% 330ms -> 8% 100ms)

Fabianx’s picture

Unfortunately the gap cannot be changed that way, because in the 850 ms already 94 ms of BubbleableMetadata::merge() are spent, so its "only" 40 ms more due to PATCH (2342045) ...

Still nice to see that this gives a 150 ms improvement in total already now, so its then only a 330 ms regression and no longer a 480 ms. :)

I think we could remove the sort and unique from the merging functions and only do it on cache set or when added to the final render array that is returned as thats where the debug should be shown anyway ... Not sure what that would save though ...

dawehner’s picture

Sadly also the Container::get() call exists before, its 40ms vs 30ms. In case someone is interested where its coming from: http://nbviewer.ipython.org/gist/dawehner/c2b71ce137550a9330e1

plach’s picture

Status: Needs work » Needs review
FileSize
10.78 KB
17.22 KB

This should fix the last failures and address #33.1 and #34.3.

I'm fine with #33.2 but I'd like to keep it as the last change, once we are sure everything else is working correctly.

@#34.2: $field_names holds all fields used in the view, but only some of them may belong to the bundle; OTOH only some of the bundle fields may be used in the view, hence we need the intersection of the two sets.

plach’s picture

I'm not a fond of early returns, but I guess Fabian was meaning this actually.

plach’s picture

FileSize
17 KB

Wrong patch, sorry.

dawehner’s picture

dawehner’s picture

Given

Over ~100ms or more savings with cold caches and would have to be deferred to a minor version

from #1744302: [meta] Resolve known performance regressions in Drupal 8 and my test scenario is not totally crazy, I think this should be critical.

Fabianx’s picture

plach’s picture

This should perform better.

dawehner’s picture

And indeed it was a bit better, for example 31 is 1.06 vs 44 1.0

plach’s picture

This should support also relationships.

@Fabian: Daniel and I are seeing different numbers while profiling this, could you do a couple of runs too?

I'd be interested in a comparison between HEAD, HEAD + 2342045, and HEAD + 2342045 + this. Attached you can find a test db dump.

plach’s picture

This fixes a failure we got when using the default_langcode field.

larowlan’s picture

Issue tags: +Needs tests

Code looks great, and the optimization makes sense. But I agree with @dawehner at #34 that there is a lot of logic in here - I think we need unit tests for all the new logic (sorry).

  1. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -114,6 +114,35 @@ class Field extends FieldPluginBase implements CacheablePluginInterface, MultiIt
    +
    

    nit, extra line

  2. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -698,6 +727,225 @@ public function renderItems($items) {
    +  public function preRender(&$values) {
    ...
    +  protected function renderEntityFields(array $values) {
    ...
    +  protected function getRenderableFieldNames() {
    ...
    +  public function getRenderField() {
    ...
    +  protected function getCustomViewMode($entity_type_id) {
    ...
    +  protected function getBundleDisplays($entity_type_id, $bundle_field_names) {
    ...
    +  public function getFieldOutput() {
    

    This is fairly complex code, and looks to be unit-testable. Can we add some unit-tests for it?

  3. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -698,6 +727,225 @@ public function renderItems($items) {
    +      // Skip this row if has already been processed.
    

    nit: if » if it

  4. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -698,6 +727,225 @@ public function renderItems($items) {
    +      // We store a field name for each relationship toi make it easier
    

    nit: toi » to

  5. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -698,6 +727,225 @@ public function renderItems($items) {
    +        // We use self here, so theoretically you could inherit this class for a
    +        // particular field.
    

    Looks like we're using static and not self?

  6. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -698,6 +727,225 @@ public function renderItems($items) {
    +    // Create on entity display object per bundle, so you can prepare entities
    

    nit: on » one

Fabianx’s picture

Here is my numbers for #48 with DB from #47:

### FINAL REPORT

=== 8.0.x..8.0.x compared (5503b2bf8227d..5503b8086e8a7):

ct  : 362,577|362,577|0|0.0%
wt  : 474,454|473,945|-509|-0.1%
mu  : 26,757,672|26,757,672|0|0.0%
pmu : 27,284,952|27,284,952|0|0.0%

=== 8.0.x..perf-entity-display compared (5503b2bf8227d..5503b8442aaee):

ct  : 362,577|370,127|7,550|2.1%
wt  : 474,454|476,966|2,512|0.5%
mu  : 26,757,672|27,128,480|370,808|1.4%
pmu : 27,284,952|27,656,600|371,648|1.4%

=== 8.0.x..perf-views-base-fields compared (5503b2bf8227d..5503b8cb68f48):

ct  : 362,577|581,631|219,054|60.4%
wt  : 474,454|744,188|269,734|56.9%
mu  : 26,757,672|27,459,856|702,184|2.6%
pmu : 27,284,952|28,000,800|715,848|2.6%

=== 8.0.x..perf-views-base-fields-and-entity-display compared (5503b2bf8227d..5503b93a1f5a9):

ct  : 362,577|558,823|196,246|54.1%
wt  : 474,454|682,880|208,426|43.9%
mu  : 26,757,672|28,754,032|1,996,360|7.5%
pmu : 27,284,952|29,277,736|1,992,784|7.3%

=== SUM: 8_0_x-summary..perf-entity-display-summary compared (5503b954c1ae1..5503b95b1cb5e):

ct  : 36,261,178|37,016,274|755,096|2.1%
wt  : 48,134,912|48,470,531|335,619|0.7%
mu  : 2,676,372,072|2,713,376,488|37,004,416|1.4%
pmu : 2,729,108,328|2,766,201,824|37,093,496|1.4%

### XHPROF-LIB REPORT

+-------------------------------------------+------------+------------+------------+------------+------------+
| namespace                                 |        min |        max |       mean |     median |       95th |
+-------------------------------------------+------------+------------+------------+------------+------------+
| Calls                                     |            |            |            |            |            |
|                                           |            |            |            |            |            |
| perf-vbf-and-entity-display |    558,823 |    562,445 |    558,859 |    558,823 |    558,823 |
| perf-views-base-fields                    |    581,631 |    585,157 |    581,666 |    581,631 |    581,631 |
| perf-entity-display                       |    370,127 |    373,701 |    370,163 |    370,127 |    370,127 |
| 8_0_x                                     |    362,577 |    366,055 |    362,612 |    362,577 |    362,577 |
|                                           |            |            |            |            |            |
| Wall time                                 |            |            |            |            |            |
|                                           |            |            |            |            |            |
| perf-vbf-and-entity-display |    682,880 |    746,898 |    697,731 |    697,906 |    703,545 |
| perf-views-base-fields                    |    744,188 |    790,561 |    756,503 |    756,479 |    764,560 |
| perf-entity-display                       |    476,966 |    555,423 |    484,705 |    484,519 |    489,298 |
| 8_0_x                                     |    473,945 |    534,991 |    481,349 |    480,558 |    487,271 |
|                                           |            |            |            |            |            |
| Memory usage                              |            |            |            |            |            |
|                                           |            |            |            |            |            |
| perf-vbf-and-entity-display | 28,754,032 | 29,061,672 | 28,759,016 | 28,754,032 | 28,754,344 |
| perf-views-base-fields                    | 27,459,312 | 27,929,608 | 27,464,759 | 27,459,856 | 27,459,856 |
| perf-entity-display                       | 27,128,416 | 27,468,352 | 27,133,765 | 27,128,480 | 27,128,480 |
| 8_0_x                                     | 26,757,672 | 27,185,432 | 26,763,721 | 26,757,672 | 26,757,882 |
|                                           |            |            |            |            |            |
| Peak memory usage                         |            |            |            |            |            |
|                                           |            |            |            |            |            |
| perf-vbf-and-entity-display | 29,277,736 | 29,588,096 | 29,282,958 | 29,277,736 | 29,278,504 |
| perf-views-base-fields                    | 27,998,504 | 28,472,960 | 28,005,273 | 28,000,800 | 28,000,800 |
| perf-entity-display                       | 27,656,480 | 28,003,496 | 27,662,018 | 27,656,600 | 27,656,600 |
| 8_0_x                                     | 27,284,952 | 27,715,704 | 27,291,083 | 27,284,952 | 27,285,108 |
|                                           |            |            |            |            |            |
+-------------------------------------------+------------+------------+------------+------------+------------+
plach’s picture

@Fabianx:

Thanks, those are more or less the same numbers I'm seeing locally. Just wanted to outline that from my measurements #48 is slightly faster than #31, so things didn't get worse in the process.

The test view above is probably one of the worst scenarios for the comparison with HEAD: it's a table view with 9 base fields and a single configurable field, this explains why this patch is slightly slower than HEAD. However if we were displaying 10 configurable fields this would be faster than HEAD.

Basically this provides a 9% ~ 12% performance improvement on views displaying ~ 10 fields, which should be a common scenario. However callgraphs clearly show that we should look for more improvements on the render side, hence I think the only hope we have to reduce the performance penalty introduced by #2342045: Standard views base fields need to use same rendering as Field UI fields, for formatting, access checking, and translation consistency is #2450897: Cache Field views row output.

plach’s picture

Working on test coverage now.

dawehner’s picture

@plach @fabianx
Well, I guess the difference is simply caused by the more recent optimizations of @plach


=== 8.0.x..perf-views-base-fields compared (5503b2bf8227d..5503b8cb68f48):


ct  : 362,577|581,631|219,054|60.4%
wt  : 474,454|744,188|269,734|56.9%
mu  : 26,757,672|27,459,856|702,184|2.6%
pmu : 27,284,952|28,000,800|715,848|2.6%

=== 8.0.x..perf-views-base-fields-and-entity-display compared (5503b2bf8227d..5503b93a1f5a9):


ct  : 362,577|558,823|196,246|54.1%
wt  : 474,454|682,880|208,426|43.9%
mu  : 26,757,672|28,754,032|1,996,360|7.5%
pmu : 27,284,952|29,277,736|1,992,784|7.3%

Mh, these numbers are still bad :(

plach’s picture

FileSize
37.75 KB

This should address #33.2, tests still to be added. Too many line changes, no interdiff.

Status: Needs review » Needs work

The last submitted patch, 54: views-entity_display-1867518-54.patch, failed testing.

plach’s picture

Status: Needs work » Needs review
FileSize
3.38 KB
38.82 KB

This should fix test failures and address #49. I'm not sure we need unit tests for this code: we have many tests covering exactly most of the cases it handles, only relationships were not well covered. I'm afraid we would be basically duplicating the existing tests.

@Daniel:
What do you think?

Status: Needs review » Needs work

The last submitted patch, 56: views-entity_display-1867518-56.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
50.05 KB
11.49 KB

Well, I think we have all kind of test coverage for that already ... the problems of that code would not be covered by a unit test, but rather
a configuration with multiple fields, revisions and what not, so its rather an integration complexity than an inner complexity.

Added some test coverage about such a complex configuration and it seems to be somehow the case that its not working as expected.
It doesn't render something at all.

Let's first though fix the other broken test, interesting that this every actually worked.

Status: Needs review » Needs work

The last submitted patch, 58: 1867518-58.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
58.93 KB
9.58 KB

In order to fix that I decided to move the delta handling part of processEntity into the rendering layer of views.
This allows us to not have conflicting fields.

On top of that I wrote a unit test for the delta method, this is pretty useful and covered one potential bug.

dawehner’s picture

Issue summary: View changes

Worked on the issue summary.

Status: Needs review » Needs work

The last submitted patch, 60: 1867518-60.patch, failed testing.

plach’s picture

Assigned: plach » Unassigned
dawehner’s picture

Assigned: Unassigned » plach
Status: Needs work » Needs review
FileSize
59.7 KB
789 bytes

let's fix that quickly. Working on revision tests.

dawehner’s picture

Assigned: plach » Unassigned

sorry plach

dawehner’s picture

FileSize
71.26 KB
20.68 KB

Added a test coverage for relationships and ... oh wonder, it did not actually worked before. I'm glad that we have test coverage now.

Status: Needs review » Needs work

The last submitted patch, 66: 1867518-66.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
71.45 KB
863 bytes

This should fix things.

dawehner’s picture

Issue summary: View changes
Issue tags: -Needs tests

I think we have now quite a bit of a test coverage.

YesCT’s picture

does this need another performance analysis,
or just reviews?
change record?

dawehner’s picture

This nearly changed just internal code, so I think we don't need a change record here.

The changes since #53 (last time fabian checked it), have been mostly adding test coverage, so I think the tags are up to date.
We need some form of review.

plach’s picture

Status: Needs review » Needs work

Reviewed the interdiffs since my last patch:

  1. +++ b/core/modules/views/src/Tests/ViewResultAssertionTrait.php
    @@ -109,7 +109,15 @@ protected function assertIdenticalResultsetHelper($view, $expected_result, $colu
    +          $row[$expected_column] = $value[$expected_column];
    

    Don't we need to cast this to string too?

  2. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -690,6 +691,74 @@ public function renderItems($items) {
    +   * Therefore this method filters out limits the items, reorders the items
    +   * as well as takes into account an offset.
    

    This sentence doesn't look correct to me.

  3. +++ b/core/modules/views/tests/src/Unit/Plugin/field/FieldTest.php
    @@ -553,6 +555,82 @@ public function testQueryWithGroupByForConfigField() {
    +  public function providerTestPrepareItemsByDelta() {
    

    Missing PHP doc.

  4. +++ b/core/modules/views/src/Tests/Handler/FieldFieldTest.php
    @@ -320,7 +376,72 @@ public function testRevisionRender() {
    +   * Tests the output of a revision view with base fields and configurable fields.
    

    Exceeded 80 chars limit.

  5. +++ b/core/modules/views/src/Tests/ViewResultAssertionTrait.php
    @@ -112,8 +112,11 @@ protected function assertIdenticalResultsetHelper($view, $expected_result, $colu
    +        // This case is about fields with mulitple values.
    

    Typo

Daniel and I did pretty distinct work: I focused on the refactoring and he focused mostly on test coverage. I guess once both of us is happy with the respective changes either of the two could RTBC the patch. However a third person performing a final review would be welcome.

It would be good if @Fabian could provide updated numbers to confirm I did not introduce a performance regression.

dawehner’s picture

Status: Needs work » Needs review
FileSize
2.77 KB
71.68 KB

Thank you for your review!

Don't we need to cast this to string too?

We absolutely, have to, see https://www.drupal.org/files/issues/interdiff_11083.txt

This sentence doesn't look correct to me.

Fair, let's try to rewrite it.

Missing PHP doc.

If we really would just be less strict with this rules, but here is your one line documentation :)

Fabianx’s picture

Assigned: Unassigned » Fabianx

Reviewing today or tomorrow ...

plach’s picture

Fabianx’s picture

Assigned: Fabianx » Unassigned
Related issues: -#2450897: Cache Field views row output

I found nothing in my review today.

I will review and hopefully RTBC again tomorrow.

Fabianx’s picture

plach’s picture

Thanks Fabian, if you could profile this again to ensure I didn't introduce any significant regression in the last iterations it would be great.

Fabianx’s picture

Here we go:

### FINAL REPORT

=== 8.0.x..8.0.x compared (550f82638311b..550f838143987):

ct  : 600,174|600,174|0|0.0%
wt  : 764,153|763,582|-571|-0.1%
mu  : 27,275,888|27,275,888|0|0.0%
pmu : 27,819,448|27,819,448|0|0.0%

=== 8.0.x..issue-1867518 compared (550f82638311b..550f84122f50a):

ct  : 600,174|565,627|-34,547|-5.8%
wt  : 764,153|687,814|-76,339|-10.0%
mu  : 27,275,888|28,435,448|1,159,560|4.3%
pmu : 27,819,448|28,964,488|1,145,040|4.1%

=== SUM: 8_0_x-summary..issue-1867518-summary compared (550f8440b2d3e..550f84474de28):

ct  : 60,020,803|56,566,199|-3,454,604|-5.8%
wt  : 77,588,946|70,259,099|-7,329,847|-9.4%
mu  : 2,728,032,936|2,843,973,168|115,940,232|4.2%
pmu : 2,782,455,912|2,896,887,072|114,431,160|4.1%

---
ct = function calls, wt = wall time, cpu = cpu time used, mu = memory usage, pmu = peak memory usage

### XHPROF-LIB REPORT

+-------------------+------------+------------+------------+------------+------------+
| namespace         |        min |        max |       mean |     median |       95th |
+-------------------+------------+------------+------------+------------+------------+
| Calls             |            |            |            |            |            |
|                   |            |            |            |            |            |
| issue-1867518     |    565,627 |    569,126 |    565,662 |    565,627 |    565,627 |
| 8_0_x             |    600,174 |    603,577 |    600,208 |    600,174 |    600,174 |
|                   |            |            |            |            |            |
| Wall time         |            |            |            |            |            |
|                   |            |            |            |            |            |
| issue-1867518     |    687,814 |    744,665 |    702,591 |    702,014 |    712,984 |
| 8_0_x             |    763,582 |    815,454 |    775,889 |    775,099 |    785,034 |
|                   |            |            |            |            |            |
| Memory usage      |            |            |            |            |            |
|                   |            |            |            |            |            |
| issue-1867518     | 28,434,728 | 28,740,888 | 28,439,732 | 28,435,448 | 28,435,448 |
| 8_0_x             | 27,275,496 | 27,660,272 | 27,280,329 | 27,275,888 | 27,275,888 |
|                   |            |            |            |            |            |
| Peak memory usage |            |            |            |            |            |
|                   |            |            |            |            |            |
| issue-1867518     | 28,963,840 | 29,270,640 | 28,968,871 | 28,964,488 | 28,964,488 |
| 8_0_x             | 27,819,448 | 28,207,856 | 27,824,559 | 27,819,448 | 27,820,824 |
|                   |            |            |            |            |            |
+-------------------+------------+------------+------------+------------+------------+
dawehner’s picture

Thank you @Fabianx

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Reviewed again, performance is good (10% faster compared to the 50% slower regression we introduced earlier)!

Test coverage seems to cover all tricky cases.

RTBC

Wim Leers’s picture

Is the 4% increase in memory usage expected? Why?

dawehner’s picture

... I have some idea for additional test coverage

alexpott’s picture

Status: Reviewed & tested by the community » Needs review

Let's get some understanding around #82

yched’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
    @@ -329,6 +328,14 @@ public function buildComponents(array &$build, array $entities, array $displays,
    +  public function prepareView($entities, $displays, $view_mode) {
    +    // Invoke hook_entity_prepare_view().
    +    $this->moduleHandler()->invokeAll('entity_prepare_view', array($this->entityTypeId, $entities, $displays, $view_mode));
    +  }
    

    Still not a fan of splitting this part of buildComponents() into a new public API on EVB. Two public methods, one being called by the other, confuses the external facing API of EVB.

    - Can't EntityFieldRenderer leverage EVB::buildComponents() directly ?
    - If not, can't EntityFieldRenderer simply repeat the invokeAll('entity_prepare_view') ?

  2. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,358 @@
    +  protected function getCustomViewMode($entity_type_id) {
    +    return 'views:' . $this->view->storage->id() . ':' . $this->view->current_display . ':' . $entity_type_id;
    +  }
    

    Do we really need a dedicated view_mode name here ?

    EntityViewDisplay skips its init() when the view_mode === EntityDisplayBase::CUSTOM_MODE ('_custom'), and i think we do need to skip init() here :

    - it is not actually needed in our case here, and thus costs needless CPU cycles

    - it forces base fields that do not specify display options back to "hidden" (that's intended for "regular" view modes - but here it would mean that for example 'nid' could never be displayed even in a View ?)

dawehner’s picture

FileSize
75.78 KB
5.52 KB

MH, so this configuration: Use the same field with different field formatter configurations breaks. This feature is for example needed for a filemime formatter which uses the filename on the storage side, so you can easily end up with twice the same entity field name.

Fabianx’s picture

Status: Needs review » Needs work

OMG, yes. That is what I had the whole time on my mind and just could not grasp.

Yes, the same field formatter with different configurations is a must to support.

The last submitted patch, 86: 1867518-84.patch, failed testing.

plach’s picture

Status: Needs work » Needs review
FileSize
5.91 KB
78.43 KB

Just a reroll for now.

@Wim:

This was not exactly trivial due to the introduction of cache contexts. Could you check the interdiff and see whether it still makes sense? The entity field renderer now proxies the call to the entity translation renderers responsible of altering the view query. Is that enough?

Status: Needs review » Needs work

The last submitted patch, 89: views-entity_display-1867518-89.patch, failed testing.

dawehner’s picture

+++ b/core/modules/views/src/Plugin/views/field/Field.php
@@ -774,10 +774,12 @@ public function preRender(&$values) {
+      if (!empty($this->view->field)) {
+        foreach ($this->view->field as $field) {

Here is a logic question: How can $this->view->field can be empty if FIeld.php itself is an instance?

plach’s picture

Not sure how, but it happens. Maybe it's called before populatiing the array...

plach’s picture

Status: Needs work » Needs review
FileSize
4.49 KB
78.83 KB

This should be green and address #85.2. Tomorrow I will post some details, now too late, I almost fell asleep while running tests ;)

Status: Needs review » Needs work

The last submitted patch, 93: views-entity_display-1867518-93.patch, failed testing.

dawehner’s picture

I guess you lost your color vision.

Wim Leers’s picture

#89: Well, to be fair, it's a rather complex combination of 2 levels of base classes and traits, plus layers of indirection. That's the true source of complexity here. So, HEAD is actually wrong: RendererBase::getCacheContexts() should return the empty array. DefaultLanguageRenderer and ConfigurableLanguageRenderer then also return the empty array, which is correct, since they both use non-request-dependent entity translations. Only TranslationLanguageRenderer depends on the request context.
AFAICT that's exactly what you changed it into, so, yes, the #89 interdiff is correct :)

I guess you lost your color vision.

LOL :)

Wim Leers’s picture

@plach: I split that cache context stuff off into a separate issue, since it really is a problem of its own in HEAD. Also helps simplify this patch a bit: #2462851: Improve Views entity row renderer plugins' cache contexts.

dawehner’s picture

We (plach and dawehner) talked for a while this morning about this issue and had an idea to approach the issue dramatically different.
As we see in the current patch, we add a hell lot of complexity to avoid creating entity display objects as much as possible.

We have been wondering, whether its worth the effort, and we could instead just initialize the formatter for each field formatter,
call ::view() and ::access() on it for each row and be done. By doing that we bypass the entity display API,
which is not a good fit in the first place, for our usecase.

This itself should not only reduce the complexity A LOT, but it also would potential improve the performance as well.
Any first thoughts about it?

plach’s picture

Just a clarification: calling FieldItemsList::view() still involves building display objects, so we would need to more or less inline EVD::buildMultiple() instead.

Wim Leers’s picture

I think that would also make it more difficult to enable render caching in the future, because then you're definitely going to avoid using EntityViewBuilder forever.
Of course, unless I'm mistaken, #2450897: Cache Field views row output is going to have its own render caching anyway… so I don't think it really matters.


I think the root cause of the trickiness here is that EntityViewDisplay is a very strange beast. If you could create an EntityViewDisplay object and then give it the entities that need to be rendered and not have to talk to the view builder also, that'd help a lot, I think.
EntityFormDisplay does work that way, IIRC.
I had a discussion about this with @swentel and others about all this a year ago, at DDD Szeged. We opened an issue, but I'm unable to find it right now.

dawehner’s picture

I'm experimenting on #2463959: dawehner testing issue

I think the root cause of the trickiness here is that EntityViewDisplay is a very strange beast. If you could create an EntityViewDisplay object and then give it the entities that need to be rendered and not have to talk to the view builder also, that'd help a lot, I think.

Well even if an entity display can render itself properly,
I still think the problem is simply that it is not a good fit for views at all. Especially EntityViewDisplay just doesn't work together with the model or rendering arbitrary fields, across bundles and multiple instances, with relationships and everything. As plach said earlier this morning, every change will result in an exponential growth of the complexity.

Fabianx’s picture

Assigned: Unassigned » yched

Assigning to yched for feedback.

Wim Leers’s picture

Especially EntityViewDisplay just doesn't work together with the model or rendering arbitrary fields, across bundles and multiple instances, with relationships and everything.

Completely true. Views has more advanced needs than EVD is designed for.

+1 to requiring @yched's feedback.

dawehner’s picture

Status: Needs work » Needs review
FileSize
82.82 KB
25.62 KB
Wim Leers’s picture

"Before" == #93/earlier iterations of this patch, "after" == latest patch, right?


+++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
@@ -121,113 +132,159 @@ public function preRender(array $values) {
+//    $entities = [];
+//    $translations = [];
+//    $bundle_field_names = [];

etc.

There's a lot commented out there :)

dawehner’s picture

Yeah that is an interdiff to #93 and yeah, the old code is still commented out.

Opened a follow up to discuss getting rid of views-view-field as template

yched’s picture

If you could create an EntityViewDisplay object and then give it the entities that need to be rendered and not have to talk to the view builder also, that'd help a lot, I think.

Well, "create an EntityViewDisplay object and then give it the entities that need to be rendered" is basically what the current patch does, no EVB is involved in the process ? (well, except for calling hook_entity_prepare_view(), which, as noted in #85, is not something I think we need an EVB for)

But yeah, an EVD is about a bundle, i.e rendering the same collection of fields with the same settings on a series of entities (of the same bundle). Using multiple ones for rendering a list of entities spanning several bundles belongs to the layer above (e.g EntityViewBuilder does that). Relationships are very much a Views thing, so EVD cannot handle that.

Aside from reducing the callstack, the big promise of using EVD::buildMultiple($entities) was to use FormatterInterface::prepareView($entities) once per field for all rendered rows, instead of running it row by row so far. At this point, prepareView() is mostly for multiple-loading of referenced entities for ER / File formatters (it makes a big difference there, though).

Then, having EVD being the one unified way to run formatters of fields in an entity also meant better maintainability by avoiding duplicated logic, and ansuring the code flow is the same (same hooks invoked, etc... IIRC, ensuring that the same hooks run was the main reason why D7 Views used field_attach_view() in single-field mode, which is basically what D8 HEAD still does, in a post-field_attach_view() codebase).

But well, if not using EVDs and writing separate logic means 10% more efficient code, I guess that's hard to argue against :-)
Slightly worried about having to maintain two separate code flows, though. The idea in the long run was to try to unify Views rendering and non-Views entity rendering, not split them apart :-/

dawehner’s picture

Slightly worried about having to maintain two separate code flows, though. The idea in the long run was to try to unify Views rendering and non-Views entity rendering, not split them apart :-/

Well the solution would be the following: Get rid of the idea to couple EVD with bundles, but instead just allow to pass in a couple of components, which have the used field names as part of it, but don't assume to not be able to render the same field twice etc.
One EVD per entity type thought would be enough then.

effulgentsia’s picture

I agree that with EVD as currently in HEAD (coupled to bundles, and each field only appearing once), it's not a good fit for the Views use case, where you typically have the same field display settings for a given Views display even though the entities being rendered are in different bundles. What's critical about this issue is not to use EVD, but to speed up the way Views renders multiple fields for multiple entities. So, I suggest retitling and resummarizing this issue for that.

I think one or several followups to either refactor EVD to have fewer of those limitations, or to split out the EVD::buildMultiple() implementation into something that can be used by both EVD and Views, in order to centralize the logic referenced in #107 and #100, would be good though.

Wim Leers’s picture

Issue tags: +Needs reroll

#2462851: Improve Views entity row renderer plugins' cache contexts landed! That should make this issue a tiny bit smaller.

siva_epari’s picture

Patch rerolled.

yched’s picture

Well the solution would be the following: Get rid of the idea to couple EVD with bundles, but instead just allow to pass in a couple of components, which have the used field names as part of it, but don't assume to not be able to render the same field twice etc.

1) Allowing EVDs to render the same field twice :

It is a long-standing wish of mine, but there are non minor challenges:
- Impacts how you work with entity templates. You can do {{ content.field_foo }} because you know that by convention the $content contains one entry per field, named after the field. If the field appears more than once, then who decides how each is named, and how do I know it as a tpl author ?
- Related, not sure how that would work with in-place editing
- Would need a complete rethink of the "Manage display" UI screen, to move from the current "fixed list of possible pieces of content (one per field), you can hide some" to something more like Views' "add a new piece of content"
- Rendering the same field twice is only really interesting if you can do #1234624: Limit the number of fields to display, which was deemed "too late" a while ago

My gut feeling says "doubtful at this point".

2) Allowing EVDs to render entities across bundles :

Yeah, EVDs are currently modeled as "the config entities for Field UI's "Manage Fields" screen *on a specific bundle*". The config entity ID is keyed by entity type and bundle.

Maybe we could untie that strong "link to a specific bundle" for runtime, non-config EVDs - meaning, if you don't have a $this->bundle, you can be used for runtime rendering, but you can't be saved in config.

Then we'd need to change the way EVDs collect the (bundle specific) field definitions they pass to each formatter.
Currently that's done "statically" in EntityDisplayBase::getFieldDefinitions() based on $this->targetEntityType / $this->bundle.
It would need to happen dynamically based on each specific $entity being rendered.

That one seems doable, I could try to work on that in Dev Days next week.

Question is : is working on 2) worhtwhile for Views if 1) stays out of reach ? I.e, would 2) alone alleviate enough of the complexity / CPU overhead that Views could switch back to using EVDs ?

Meanwhile, agreed that it's safer for this critical here to pursue the "separate code" approach from the latest patches. We'll need to closely track the code flow and hook invocations though.

dawehner’s picture

Question is : is working on 2) worhtwhile for Views if 1) stays out of reach ? I.e, would 2) alone alleviate enough of the complexity / CPU overhead that Views could switch back to using EVDs ?

I'm pretty sure that would simplify the code quite a bit and so reduce the amount of overhead in the first place, but even for now, we would have to figure out,
how we deal with fields rendered multiple times.

Here is a question: Would it make sense for $entity->foo->view() to bypass the entity display machinery as well?

yched’s picture

@dawehner: $entity->field->view() uses EVD for the same reason field_view_field() uses field_attach_view() in D7: to ensure the same logic and hooks apply

And the big drawback of single-field view() is that you lose the single prepareView() across multiple entities.

But this makes me think : if we find we have to duplicate field rendering code, maybe it would be best if it was duplicated between EVD and $entity->field->view() rather than between EVD and Views ? Keeps the duplicate code a bit closer, and optimises $entity->field->view() for everyone instead of just Views ?

Then, Views would use
- EVDs (across bundles if we can do #112.2) on entities containing the first instance of each field, and for relationships (rendering a field with a relationship is really rendering a different entity, you can't reuse the same EVD). There you benefit of multiple processing
- one-off $entity->field->view() for fields that appear for a 2nd/3rd/Nth time

Thoughts ?

dawehner’s picture

Thoughts ?

Sounds more sane indeed, but we really have to do profiling in order to keep things quick.
The current approach of the patch is great in that sense, because it also for example doesn't create formatter instances all the time.

Fabianx’s picture

With this post I am trying to take a step back again and get back to the "drawing board" first. (Note: This includes views row #cache as a concept just to ensure all edge cases are properly supported).

Current State (As I understand it)

I have a number of rows and each row has one or more fields associated with it, all rows are configured to render in a certain way, however single rows can be rendered differently.

How I would implement it (naively)

So lets say I am trying to re-implement a 'views light', but I skip row style and field style plugins for now, however I still have different entities in my input data.

So my exemplary input could look like this:

$this->rows = [
  [ 'entity_article' => ['article', 10], 'entity_user' => ['user', 42] ],
  [ 'entity_article' => ['article', 20], 'entity_user' => ['user', 23] ],
];

While my configuration per row could look like this:

$this->fields = [
  0 => [ 'entity' => 'entity_article', 'field name' => 'title', 'field config' => [...]],
  1 => [ 'entity' => 'entity_user', 'field name' => 'name', 'field config' => [...]],
  2 => [ 'entity' => 'entity_article', 'field name' => 'body', 'field config' => [...]],
  3 => [ 'entity' => 'entity_article', 'field name' => 'title', 'field config' => [..., 'formatter' => 'summary_trimmed']],
];

So pseudo-code I would write to render that could be like:

function render() {
  $rendered_rows = [];

  foreach ($this->rows as $index => $row) {
    $rendered_rows[$index] = $this->renderRow($row);
  }
}

And in renderRow:

function renderRow($row) {
  $rendered_fields = [];
  foreach ($this->fields as $index => $field) {
    $rendered_fields[$index] = $this->renderField($field, $row);
  }

  return implode('|', $renderer_fields); // This is the row style formatter, omitted for brevity.
}

And in renderField:

function renderField($field, $row) {
  $entity_name = $field['entity'];

  $entity = entity_load($row[$entity_name][0], $row[$entity_name][1]);
  $field_name = $field['field name'];
  $field_config = $field['field configuration'];

  return '<div class="field">' . $entity->get($field_name)->view($field_config) . '</div>';
}

Adding row caching

Adding row caching is for this simplified example pretty trivial:

function getCachedRows($rows) {
  $cid_map = [];
  foreach ($rows as $index => $row) {
     $cid_map[$index] = $this->createRowCacheID($row);
  }

  if (empty($cid_map)) {
    return [];
  }

  $cids = array_filter(array_values($cid_map)); // Filter out non-cacheable rows, which have $cid == FALSE / NULL

  $cached_data = $this->cache->getMultiple($cids);

  $return = [];
  foreach ($cid_map as $index => $cid) {
    if (isset($cached_data[$cid])) {
      $return[$index] = $cached_data[$cid]->data;
    }
  }

  return $return;
}

function render() {
  $rendered_rows = $this->getCachedRows($this->rows);

  foreach ($this->rows as $index => $row) {
    if (!isset($rendered_rows[$index])) {
      $rendered_rows[$index] = $this->renderRow($row);
    }
  }

  $this->setCachedRows($this->rows, $rendered_rows); // Optimization to not cache already cached data omitted for brevity reasons.
}

So row caching happens before rendering the rows at all, that means all other code is not affected by this, which is good.

However this also shows that the renderer cache API should actually have a public API as this clearly shows that #pre_render / #cache is not enough, as else we cannot multiple prepare the rows and my little cache get code would not take care of bubbling cache contexts properly (using e.g. #cache_redirect internally).

I will open an issue for that.

Optimizing the code

So now after the cache get we have the following to get all rows that still need to be rendered and we add in a prepare function to optimize:

function render() {
  $rendered_rows = $this->getCachedRows($this->rows);

  $still_to_render = array_diff_key($this->rows, $rendered_rows);

  $this->preparedRows = $this->prepareRows($still_to_render);

  foreach ($still_to_render as $index => $row) {
    $row['index'] = $index;
    $rendered_rows[$index] = $this->renderRow($row);
  }

  $this->setCachedRows($this->rows, $rendered_rows); // Optimization to not cache already cached data omitted for brevity reasons.
}

so that renderRow looks like this:

<?php
function renderRow($row) {
  $rendered_fields = [];
  foreach ($this->fields as $index => $field) {
    $field['index'] = $index;
    $rendered_fields[$index] = $this->renderField($field, $row);
  }

  return implode('|', $renderer_fields); // This is the row style formatter, omitted for brevity.
}

and that renderField now looks like this:

function renderField($field, $row) {
  return '<div class="field">' . $this->preparedRows[$row['index']][$field['index']] . '</div>';
}

Now we only need the $this->prepareRows() function and this is what this ticket is about.

Prepare Rows

Because this is an exercise in understanding lets try to see how an ideal API for that use case would work.


function prepareRows($rows) {
  $prepared_rows = [];
  $entities_by_type = [];
  $entity_data_by_name = [];
  $row_map = [];

  // Find all entities and load them by type
  foreach ($rows as $row_index => $row) {
    foreach ($this->fields as $field_index => $field) {
      $entity_name = $field['entity'];
      $entity_type = $row[$entity_name][0];
      $entity_id = $row[$entity_name][1];

      $entities_by_type[$entity_type][] = $entity_id;
      $entity_data_by_name[$entity_name][$entity_type][] = $entity_id;
      $row_map[$entity_name][$entity_id] = $row_index; // @todo this might not be unique
    }
  }

  foreach ($entities_by_type as $entity_type => $entity_ids) {
    $loaded_entities_by_type = entity_load($entity_type, $entity_ids);
  }

  $builders_by_type = [];
  $render_entities_by_type = [];

  // Now lets prepare a display object for all the entities.
  foreach ($this->fields as $field_index => $field) {
     $entity_name = $field['entity'];
     $entity_type = reset(array_keys($entity_data_by_name[$entity_name]));

      if (!isset($builders_by_type)) {
        $builders_by_type[$entity_name] = new SimpleEntityViewDisplay($entity_type);
      }
     $field_name = $field['field name'];
     $field_config = $field['field configuration'];

     $builders_by_type[$entity_name]->addField($field_index, $field_name, $field_config); // This API does not exist, but this would make sense.
  }

  foreach ($builders_by_type as $entity_name => $evb) {
     $render_entities = [];

     // Now find the entities
     foreach ($entity_data_by_name[$entity_name] as $entity_type => $entity_ids) {
        foreach ($entity_ids as $entity_id) {
          $render_entities[] = clone $loaded_entities_by_type[$entity_type][$entity_id];
        }
     }

     $rendered = $evb->render($render_entities); // This API may not exist like this
     foreach ($rendered as $entity_id => fields) {
        $row_index = $row_map[$entity_name][$entity_id];
        foreach ($fields as field_index => $data) {
          $prepared_rows[$row_index][$field_index] = $data;
        }
     }
   }

  return $prepared_rows;
}

Even with an ideal Entity View Builder the code remains challenging.

However as the configuration for the different bundles is merely cloned we could use an Adapter pattern until EVB upstream supports something that is not configured per bundle.

Simplify

Okay, lets take another step back and do the simplest solution possible: Render all fields horizontally, that code looks like this:

( For brevity reasons the entity_load_multiple is replaced with a single loading here, as that optimization is trivial)

function prepareRows($rows) {

  $prepared_rows = [];

  foreach ($this->fields as $field_index => $field) {
     $entity_name = $field['entity'];
     $field_name = $field['field name'];
     $field_config = $field['field configuration'];

     $entities = [];
     $entity_types = [];
     $row_map = [];
     $rendered = [];

     foreach ($rows as $row_index => $row) {
        list($entity_type, $entity_id) = $row[$entity_name];
        $entity = clone entity_load_single($entity_type, $entity_id);
        $entities[$entity_type][$entity_id] = $entity;
        $entity_types[$entity_type] = $entity_type;

        // Store the address where we will find the data.
        $row_map[$row_index] = [$entity_type, $entity_id, $field_name];
     }

     foreach ($entity_types as $entity_type) {
        $evb = new SimpleEntityViewDisplay($entity_type);
        $evb->addField($field_name, $field_config);
        $rendered[$entity_type] = $evb->buildMultiple($entities[$entity_type]);
     }

    // Translate from entity_id / field_name back to row_index / field_index.
    foreach ($row_map as $row_index => $data) {
        list($entity_type, $entity_id, $field_name) = $data;
        $prepared_rows[$row_index][$field_index] = $rendered[$entity_type][$entity_id][$field_name];
    }
  }

  return $prepared_rows;
}

This looks better. Now all we need to do is to unroll the inner loop into preparation and now that we did this pre-work this becomes surprisingly simple.

Support multiple fields with the same name

To support multiple fields we need to add a $builder_index, which changes when the count for the seen field_name per entity_type changes.

function prepareRows($rows) {

  $builder_seen = [];
  $prepared_rows = [];
  $entities = [];
  $builders = [];
  $rendered = [];
  $row_map = [];

  foreach ($this->fields as $field_index => $field) {
     $entity_name = $field['entity'];
     $field_name = $field['field name'];
     $field_config = $field['field configuration'];

     $entity_types = [];

     // Get all entity types.
     foreach ($rows as $row_index => $row) {
        list($entity_type, $entity_id) = $row[$entity_name];
        $entity_types[$entity_type] = $entity_type;
     }

      $builder_index_map = [];

     // Prepare the builder index per entity type.
     foreach ($entity_types as $entity_type) {
        if (!isset($builder_seen[$entity_type][$field_name])) {
          $builder_seen[$entity_type][$field_name] = 0;
        }
        $builder_index_map[$entity_type] = $builder_seen[$entity_type][$field_name];
        $builder_seen[$entity_type][$field_name]++;
     }

     foreach ($rows as $row_index => $row) {
        list($entity_type, $entity_id) = $row[$entity_name];
        $builder_index = $builder_index_map[$entity_type];

        $entity = clone entity_load_single($entity_type, $entity_id);
        $entities[$builder_index][$entity_type][$entity_id] = $entity;

        // Store the address where we will find the data.
        $row_map[$row_index][$field_index] = [$builder_index, $entity_type, $entity_id, $field_name];
     }

     // Prepare the builders.
     foreach ($entity_types as $entity_type) {
        if (!isset($builders[$builder_index][$entity_type])) {
          $builders[$builder_index][$entity_type] = new SimpleEntityViewDisplay($entity_type);
        }
        $builders[$builder_index][$entity_type]->addField($field_name, $field_config); // The field is now unique thanks to the builder index.
     }
  }

  // Render the data per builder
  foreach ($builders as $builder_index => $entity_types) {
     foreach ($entity_types as $entity_type) {
        $rendered[$builder_index][$entity_type] = $builders[$builder_index][$entity_type]->buildMultiple($entities[$builder_index][$entity_type]);
     }
  }

  // Translate from entity_id / field_name back to row_index / field_index.
  foreach ($row_map as $row_index => $field_map) {
    foreach ($field_map as $field_index => $data) {
      list($builder_index, $entity_type, $entity_id, $field_name) = $data;
      $prepared_rows[$row_index][$field_index] = $rendered[$builder_index][$entity_type][$entity_id][$field_name];
    }
  }

  return $prepared_rows;
}

So regardless of the implementation, in the end it is a simple mapping from entity_id / field_name back to row_index / field_index.

Note that an implementation that uses field aliases for fields to add them to the Entity View Builder would still need a kind of 'seen' functionality per field per entity, but views already has unique identifiers, so that is not an issue.

All that changes then in the translation function is the omitting of the $builder_index and usage of a $field_alias instead:

  // Translate from entity_id / field_name back to row_index / field_index.
  foreach ($row_map as $row_index => $field_map) {
    foreach ($field_map as $field_index => $data) {
      list($entity_type, $entity_id, $field_alias) = $data;
      $prepared_rows[$row_index][$field_index] = $rendered[$entity_type][$entity_id][$field_alias];
    }
  }

That all explored, the answer to ycheds question are:

- Yes, this is generically useful to be able to have the bundle be optional, however an adapter could be written for now.
- And yes, field aliases would be nice to specify when adding a field so the $builder_index can be omitted and performance even more improved, however a build_index can be calculated, too.

That said I will see if my theory code can actually work ;)

TL;DR:

I explored various scenarios going from naive a implementation O(n * m) to using field based (column) multiple rendering, to the most complex creating multiple view builders per entity_type if fields are duplicated with O(k) performance.

I found out that we miss APIs for proper views row caching / render caching outside of #pre_render, #cache pattern.

I will check what would be involved to support the SimpleEntityViewDisplay API that would feel "naturally" to me.

dawehner’s picture

Let me try to understand your first part. You want to add row caching, so that the individual rows are not executed, fair,
but I think we already have the architecture to be able to do that. With \Drupal\views\Plugin\views\row\RowPluginBase::preRender
we can plug in the logic in the row styles, to be cacheable. This would automatically solve the point made by plach that we want to opt out for entity rows.

Thanks for the writeup but I still try to understand, how this would result in something actionable, because mainly it summarises the problem we already had.

a) Your approach lacks the issues as the patch in #1867518-86: Leverage entityDisplay to provide fast rendering for fields, multiple times the same field with different configuration. Yes, its an edge case,
but its not something you can just drop, so you have to take it into account, which obviously makes the code more complex again. Adding it on top of it,
adds again more complexity.
b) All that code is still there without a lot of benefit compared to just create the formatters and render the things.

PS: Can you please replace your uses of entity view builder with entity view display, given that the entity view builder is a total different api?

Fabianx’s picture

#116 was a case study and mainly sharing my thoughts, however the code could be used to develop a good and abstract EntityViewDisplay that is dynamic and solves views use cases.

#117:

Lets discuss views row caching in the row caching ticket, but in short the problem we are facing is:

- You have a list of rows
- You need to prepare something for all rows
- You want to cache rows

If you use cache->get / cache->set directly, then you are not doing it right as any post-bubbled context would not be taken into account in your $cid.

So you must use #pre_render / #cache pattern, but that is impossible to combine with a cache multi get to never prepare anything for the rows that are cached before.

Hence opened #2466585: Decouple cache implementation from the renderer and expose as renderCache service.

--

a) That is not true, multiple builders for similar fields are handled in the last example.
b) Yes and no, this is something anyone building lists of multiple entities with a certain configuration of fields will need to deal with.

PS: Updated to the fantasy SimpleEntityViewDisplay class.

dawehner’s picture

Alright, let's open up an issue to discuss that, see #2466913: Allow EVD to render fields across bundles

Gábor Hojtsy’s picture

Issue tags: +D8MI, +language-content

Tagging for D8MI.

plach’s picture

This tries to the latest incorporate feedback from @yched, at least what makes sense for the scope of this issue. Most of the work here was funded by the Drupal Association as a D8 Accelerate grant, and this is critical because of the performance gain. For this reason I tried to address @yched's concerns without introducing too many changes to the current architecture and leave the door open for additional refactorings as #2466913: Allow EVD to render fields across bundles. For the same reason I didn't try to implement Fabian's suggestions: the performance gain we have now is real, we should get it and build on top of it in non-critical issues.

I removed the hook_entity_prepare_view() invocation as the new approach does not support it, at least until we do #2466913: Allow EVD to render fields across bundles, however this is not a regression as in HEAD we don't do that either.

plach’s picture

From my profiling this is still ~10% faster and uses ~4% more memory. I still have to investigate that.

Status: Needs review » Needs work

The last submitted patch, 121: views-entity_display-1867518-121.patch, failed testing.

Fabianx’s picture

+++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
@@ -0,0 +1,237 @@
+      // Build field render arrays for the current entity types.
+      $displays[$entity_type_id]->buildFieldMultiple($this->build, $entities[$entity_type_id], $field->getFieldDefinition(), $field->options['type'], $field->options['settings'], $field_id);

Yes, this makes a lot of sense.

This is not too different from what I had FWIW and great that seemingly it does not matter where the loop is - as long as we render and prepare all rows at the same time for each field.

The field_id or field_alias indeed would need to be supported by EntityViewDisplay as well if we do the bundle thing.

However I feel that the code is re-used enough to not warrant that anymore now that we see how simple that is.

I still don't understand some things for re-use of the entityRenderer, but will check that code part again later.

Looks great!

The 4 MB more memory are likely the temporary cache for the build of the entity renderer. Destroying that after the rendering is done could help perhaps.

plach’s picture

Status: Needs work » Needs review
FileSize
1.64 KB
86.22 KB

Fixed test failures, hopefully.

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php
    @@ -265,6 +252,78 @@ public function buildMultiple(array $entities) {
    +  protected function formatField(array &$build_list, array $entities, FormatterInterface $formatter, $field_name, $field_key = NULL) {
    

    I'm curious whether the method name could make it clear that we deal with multiple entities at the same time.

  2. +++ b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php
    @@ -265,6 +252,78 @@ public function buildMultiple(array $entities) {
    +  public function buildFieldMultiple(array &$build_list, array $entities, FieldDefinitionInterface $field_definition, $type, $settings, $field_key = NULL) {
    

    It is an odd API to pass things by reference which is actually more the return value. Do we really have to do that? Internally this might make sense, but its super strange as an external API.

+++ b/core/modules/file/src/Tests/Views/ExtensionViewsFieldTest.php
@@ -72,6 +72,7 @@ public function testFileExtensionTarOption() {
     $this->executeView($view);
+    $view->field['extension']->preRender($view->result);

@@ -86,6 +87,7 @@ public function testFileExtensionTarOption() {
     $this->executeView($view);
+    $view->field['extension']->preRender($view->result);

That is the wrong fix IMHO, instead call out to $view->render();

plach’s picture

This should address #126. The main reason I picked that weird DX for ::buildFieldMultiple() was to avoid having to merge results in EntityFieldRenderer::buildFields(). However after additional profiling the difference was below the average variance. I compared the two fastest runs and exclusive diff for EntityFieldRenderer::buildFields() was ~1ms with 50 result items. I'm attaching also my current test site DB dump and my test view so people can profile this themselves.

plach’s picture

This is the interdiff to compare only #126.2.

plach’s picture

Thanks to Fabian's suggestion, this patch reduces the memory consumption increase to ~0.5%-1%.

@Fabian:

A final round of profiling to confirm my results would be great :)

Status: Needs review » Needs work

The last submitted patch, 129: views-entity_display-1867518-129.patch, failed testing.

Fabianx’s picture

Sure, I can do another round of profiling.

Can we make this green again before that?

Also I think its better to do the removal in a postRender / unPrepare stage? (and not per row)

Also (but out of scope for this issue): If that works, does that mean we hold references to the view object _after_ its lifetime so we never free the memory (of all the views handling)?

Is that because those are plugins that are generated in the container?

Is there other internal data like $this->view that we don't destroy?

I suspected similar in Drupal 7 for views, but was not able to pin-point the cause ...

So a follow-up issue for that would be great, I think!

dawehner’s picture

Also (but out of scope for this issue): If that works, does that mean we hold references to the view object _after_ its lifetime so we never free the memory (of all the views handling)?

We don't do for page displays, due to that obscure feature called views_get_page_view() which are used even internally in core,
but for all other ones like block displays, we free them, so all we potentially care about is the peek memory usage.

yched’s picture

Not much time to look at this right now, but this and #2466913: Allow EVD to render fields across bundles are on top of my hitlist for Dev Days next week.

Minor remark on the latest interdiff:

+++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
@@ -119,6 +119,10 @@ public function query(QueryPluginBase $query) {
   public function preRender(array $values) {
+    if (isset($this->build) || empty($values)) {
+      return;
+    }
+
     $this->buildFields($values);
   }

Then I guess we can switch to just if ($values && !isset($this->build)) {$this->buildFields($values); } ?

plach’s picture

Assigned: yched » Unassigned
Status: Needs work » Needs review
FileSize
2.35 KB
86.33 KB

This should be green again.

Also I think its better to do the removal in a postRender / unPrepare stage? (and not per row)

There is no such method to override for the Field handler, nor for the Display and Style plugins, hence we'd need to introduce it, and propagate it down the line to Field handlers. Moreover we'd be calling the ::postRender() method once for each field, but we'd actually need it just once, so I think this wouldn't help with performance, aside from being a bit out of scope.

If this is green and profiling numbers are still good, I think we should be ready to get back to RTBC now.

plach’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

Updated issue summary

xjm’s picture

Issue tags: +D8 Accelerate Dev Days
plach’s picture

After some work on #2450897: Cache Field views row output, I realized lazy-building fields is better than pre-building them.

@yched:

This also somehow addresses #133, it would be great if you could perform a full review :)

Fabianx’s picture

Yeah! That is a nice idea!

Status: Needs review » Needs work

The last submitted patch, 137: views-entity_display-1867518-137.patch, failed testing.

plach’s picture

Status: Needs work » Needs review
FileSize
1.9 KB
86.28 KB

Fixed relationship handling, due to the latest commits. This might be green again.

Status: Needs review » Needs work

The last submitted patch, 140: views-entity_display-1867518-140.patch, failed testing.

plach’s picture

Status: Needs work » Needs review
FileSize
700 bytes
86.96 KB

Fixed the last failures. Reviews welcome...

dawehner’s picture

@yched promised to do a review, you know I'm really fine with the patch as it is :)

+++ b/core/modules/comment/src/Tests/Views/CommentFieldNameTest.php
@@ -60,6 +60,7 @@ protected function setUp() {
+    $view->render();

@@ -84,6 +85,8 @@ public function testCommentFieldName() {
+    $view->render();

+++ b/core/modules/field/src/Tests/Views/HandlerFieldFieldTest.php
@@ -137,6 +137,7 @@ public function _testSimpleFieldRender() {
+    $view->render();

@@ -153,6 +154,7 @@ public function _testInaccessibleFieldRender() {
+    $view->render();

@@ -178,6 +180,7 @@ public function _testFormatterSimpleFieldRender() {
+    $view->render();

@@ -196,6 +199,7 @@ public function _testMultipleFieldRender() {
+    $view->render();

@@ -218,6 +222,7 @@ public function _testMultipleFieldRender() {
+    $view->render();

@@ -238,6 +243,7 @@ public function _testMultipleFieldRender() {
+    $view->render();

@@ -259,6 +265,7 @@ public function _testMultipleFieldRender() {
+    $view->render();

@@ -277,6 +284,7 @@ public function _testMultipleFieldRender() {
+    $view->render();

@@ -296,6 +304,7 @@ public function _testMultipleFieldRender() {
+    $view->render();

+++ b/core/modules/file/src/Tests/Views/ExtensionViewsFieldTest.php
@@ -72,6 +72,7 @@ public function testFileExtensionTarOption() {
+    $view->render();

@@ -86,6 +87,7 @@ public function testFileExtensionTarOption() {
+    $view->render();

+++ b/core/modules/views/src/Plugin/views/field/Field.php
@@ -715,41 +801,31 @@ public function renderItems($items) {
+    $build = $this->getEntityFieldRenderer()->render($values, $this);

I think we could get rid of all those changes now again, given that we are lazy rendering again.

marcvangend’s picture

FileSize
12.02 KB

I have been testing this patch yesterday. The overall performance improvement is significant (50 - 55%), but digging into the XHprof runs I'm seeing some interesting changes which I cannot completely understand.

test setup

  • XHprof, latest version, compiled with git, enabled with xhprof_enable(XHPROF_FLAGS_NO_BUILTINS); in index.php (I had to use the no-builtins flag to prevent segfaults on my php 5.6.7 machine)
  • Standard Drupal 8 install
  • 100 article nodes, generated with devel_generate
  • A view showing 100 nodes, 5 fields per row (see the attachment fields-test.txt for the export)
  • I did 10 test runs before applying the patch, then applied the patch, did a drush cr, reloaded the page once and 10 more test runs

results

Building the page consistently took between 4.0 and 4.4 seconds before the patch, and between 1.8 and 2.1 seconds after the patch. That's a huge win. Comparison of two typical runs, made with the XHprof UI:

Run #552e86e18369e Run #552e87948c455 Diff Diff%
Number of Function Calls 1,435,878 434,910 -1,000,968 -69.7%
Incl. Wall Time (microsec) 4,202,223 1,951,226 -2,250,997 -53.6%

Now what's really interesting, is the time spent in Drupal\Core\Database\Statement::execute. While practically everything is faster, that function went from 176,461 to 734,029 microseconds (Excl. Wall Time).

Drupal\Core\Database\Statement::execute Run #552e86e18369e Run #552e87948c455 Diff Diff%
Number of Function Calls 775 380 -395 -51.0%
Incl. Wall Time (microsec) 176,507 734,046 557,539 315.9%
Incl. Wall Time (microsec) per call 228 1,932 1,704 748.2%
Excl. Wall Time (microsec) 176,461 734,029 557,568 316.0%

I'm not sure what this means. Maybe this is completely logical, and related to how the performance improvement was achieved. It could also mean that some index is missing, or certain queries are not really efficient. In that case an even bigger performance increase might be possible.

Fabianx’s picture

Assigned: Unassigned » yched

Assigning to yched for final sign-off / review - though we definitely should see which $view->render() calls we can remove now again.

I will also do another profile of this ...

Fabianx’s picture

Issue summary: View changes

Added proposed commit message: Profiling takes a lot of time, too.

Wim Leers’s picture

Skimming through the patch, it looks awesome.

Also, thank you, @marcvangend! Excellent, excellent profiling!
Regarding the increase in time spent in the DB: that's to be expected to some extent, because it causes more render cache gets. But it seems a huge increase. I'd compare the time spent in the DB cache backend's ::get() function to the total time spent in the DB, if not 90% or more of the time in the DB is the time for the cache get, then something else is causing queries.

Fabianx’s picture

### FINAL REPORT

=== 8.0.x..8.0.x compared (552f8ad534e45..552f8b8d9d137):

ct  : 498,833|498,833|0|0.0%
wt  : 634,438|636,206|1,768|0.3%
mu  : 25,622,432|25,622,432|0|0.0%
pmu : 26,070,536|26,070,536|0|0.0%

=== 8.0.x..issue-1867518--entity-display compared (552f8ad534e45..552f8bebdbabd):

ct  : 498,833|462,348|-36,485|-7.3%
wt  : 634,438|575,718|-58,720|-9.3%
mu  : 25,622,432|26,104,568|482,136|1.9%
pmu : 26,070,536|26,567,584|497,048|1.9%

=== SUM: 8_0_x-summary..issue-1867518--entity-display-summary compared (552f8c1431dcb..552f8c1a392ee):

ct  : 49,886,490|46,237,990|-3,648,500|-7.3%
wt  : 64,524,153|58,505,873|-6,018,280|-9.3%
mu  : 2,562,803,104|2,611,018,216|48,215,112|1.9%
pmu : 2,607,602,280|2,657,353,144|49,750,864|1.9%

---
ct = function calls, wt = wall time, cpu = cpu time used, mu = memory usage, pmu = peak memory usage

### XHPROF-LIB REPORT

+-------------------------------+------------+------------+------------+------------+------------+
| namespace                     |        min |        max |       mean |     median |       95th |
+-------------------------------+------------+------------+------------+------------+------------+
| Calls                         |            |            |            |            |            |
|                               |            |            |            |            |            |
| issue-1867518--entity-display |    462,348 |    465,538 |    462,380 |    462,348 |    462,348 |
| 8_0_x                         |    498,833 |    502,023 |    498,865 |    498,833 |    498,833 |
|                               |            |            |            |            |            |
| Wall time                     |            |            |            |            |            |
|                               |            |            |            |            |            |
| issue-1867518--entity-display |    575,718 |    636,996 |    585,059 |    584,136 |    592,481 |
| 8_0_x                         |    636,206 |    689,209 |    645,242 |    645,626 |    650,797 |
|                               |            |            |            |            |            |
| Memory usage                  |            |            |            |            |            |
|                               |            |            |            |            |            |
| issue-1867518--entity-display | 26,104,568 | 26,480,520 | 26,110,182 | 26,104,568 | 26,104,776 |
| 8_0_x                         | 25,621,992 | 26,184,456 | 25,628,031 | 25,622,432 | 25,622,432 |
|                               |            |            |            |            |            |
| Peak memory usage             |            |            |            |            |            |
|                               |            |            |            |            |            |
| issue-1867518--entity-display | 26,567,584 | 26,941,040 | 26,573,531 | 26,567,584 | 26,569,056 |
| 8_0_x                         | 26,069,264 | 26,630,960 | 26,076,023 | 26,070,536 | 26,070,536 |
|                               |            |            |            |            |            |
+-------------------------------+------------+------------+------------+------------+------------+

This are my numbers with plach' view.

We still save 9-10%, which is fine, the remainder we will get back with row caching.

--

So yched review, remove no longer needed ->render() calls, RTBC and commit! :)

marcvangend’s picture

Status: Needs review » Needs work

I've been digging deeper into the profiler comparison and found that the increase in database time is caused by inserts. From the XHprof UI:

Regression/Improvement summary for Drupal\Core\Database\Connection::query

Drupal\Core\Database\Connection::query Run #552e86e18369e Run #552e87948c455 Diff Diff%
Number of Function Calls 775 380 -395 -51.0%
Incl. Wall Time (microsec) 202,249 749,172 546,923 270.4%
Incl. Wall Time (microsec) per call 261 1,972 1,711 655.5%
Excl. Wall Time (microsec) 8,389 7,314 -1,075 -12.8%



Parent/Child Regression/Improvement report for Drupal\Core\Database\Connection::query [View Callgraph Diff]



Function Name Calls Diff Calls
Diff%
Incl. Wall
Diff
(microsec)
IWall
Diff%
Current Function
Drupal\Core\Database\Connection::query -395 -20.0% 546,923 24.3%
Exclusive Metrics Diff for Current Function -1,075 -0.2%
Parent functions
Drupal\Core\Database\Driver\mysql\Insert::execute 168 42.5% 694,908 127.1%
Drupal\Core\Cache\DatabaseBackend::getMultiple -495 -125.3% -126,221 -23.1%
Drupal\Core\Database\Query\Select::execute -66 -16.7% -21,316 -3.9%
Drupal\Core\Cache\DatabaseCacheTagsChecksum::calculateChecksum -1 -0.3% -804 -0.1%
Drupal\Core\Routing\RouteProvider::getRoutesByNames 0 0.0% -681 -0.1%
Drupal\Core\Routing\RouteProvider::getRoutesByPath 0 0.0% 351 0.1%
Drupal\Core\Session\SessionHandler::read 0 0.0% 264 0.0%
Drupal\Core\Path\AliasStorage::lookupPathSource 0 0.0% 261 0.0%
Drupal\Core\KeyValueStore\DatabaseStorage::getMultiple -1 -0.3% 161 0.0%
Child functions
Drupal\Core\Database\Statement::execute -395 -25.0% 557,539 101.9%
Drupal\Core\Database\Connection::prepareQuery -395 -25.0% -5,115 -0.9%
Drupal\Core\Database\Connection::expandArguments -395 -25.0% -4,055 -0.7%
Drupal\Core\Database\Connection::defaultOptions -395 -25.0% -371 -0.1%

As it turns out, every time I load the page, no less than 168 messages are logged through Drupal\dblog\Logger\DbLog::log. That explains the increased time. There seem to be just 3 different messages being logged:

Notice: Undefined index: per_page in Drupal\comment\Plugin\Field\FieldFormatter\CommentDefaultFormatter->viewElements() (line 168 of /home/marc/http/public/drupalcore/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php).

Notice: Undefined index: default_mode in Drupal\comment\Plugin\Field\FieldFormatter\CommentDefaultFormatter->viewElements() (line 167 of /home/marc/http/public/drupalcore/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php).

Notice: Undefined index: form_location in Drupal\comment\Plugin\Field\FieldFormatter\CommentDefaultFormatter->viewElements() (line 183 of /home/marc/http/public/drupalcore/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php).

Looks like fixing these notices will improve performance even more.

Wim Leers’s picture

#148/@Fabianx: can you describe the scenario you tested? Your numbers are very different. Ideally, you'd do the same experiment as @marcvangend did.

Fabianx’s picture

#150 1 million times in call number of function calls for core seems bogus, I used the test_view.yml by plach:

https://www.drupal.org/files/issues/test_view.yml

and generated 400 nodes.

We had seen a 10% performance decrease before, so it is very unlikely we will see a 50% suddenly ...

--

However I see that Marc displayed comments and comment count as part of his view, so maybe something is wrong there (me starts up profiler for his scenario).

It might be unrelated to this patch however - this should be checked.

HOWEVER:

Much more important:

Drupal\Core\Field\FormatterBase::prepareView is empty for plach's scenario (0 before / 0 after).

Are we missing things that we can prepare in bulk before for those simple things?

That explains a lot why we only see 10% for plach' scenario ...

plach’s picture

I think ~10% gain with the current total time means a ~20% gain wrt HEAD before the base field formatters patch, which is not bad :)

@marcvangend:

Can you post your test view so I can reproduce the issue?

Sorry, you already did, marcvangend++ :)

marcvangend’s picture

@plach it is already attached to comment 144.

marcvangend’s picture

Looking at the notices I posted in comment 149, they are all related to comment module, more specifically CommentDefaultFormatter::viewElements().

I put a breakpoint at line 160 of CommentDefaultFormatter.php to see what's going on. When viewing the node normally, the $comment_settings array contains all elements (6 in total) the rest of the code expects. However, when called by the view, $comment_settings only contains one element: 'comment_type' => 'comment'.

yched’s picture

Investigating the notices with @marcvangend at DDD

yched’s picture

So, the primary source of the notices is a bug in BaseFieldDefinition::createFromFieldStorageDefinition(), which creates a BFD that misses default values of field-level settings. This causes notices in code that tries to access them down the line (CommentDefaultFormatter in the sample view @marcvangend used, but it could be any formatter that needs to access the settings of the field it's displaying)

BFD::createFromFieldStorageDefinition($storage_definition) returns (shortened) :

static::create($storage_definition->getType())->setSettings($storage_definition->getSettings())

- BaseFieldDefinition::create() has code that explicitly populates the default values for both storage-level and field-level settings.
- but then ->setSettings() *overwrites* the settings with an array that only contains storage-level settings :-(

More importantly though, even if this was fixed, this would just mean that we run formatters with a field definition that only contains default values for field-level settings (because it's generated from the FieldStorageDefinition), instead of the *actual* field definition for the entity being rendered. That is wrong IMO, this won't give the expected output.

Field handlers, being cross-bundle, have to do *some* logic using the "fake", cross-bundle FieldDefinition that is derived from the FieldStorageDefinition. But by the time we're rendering, we have actual entities that have actual bundles, we should do the display using the right FieldDefinition with the correct settings for each bundle.
Meaning, we do need to group entities and render them *by bundle*, instead of all at once currently :-/

andypost’s picture

This notices means that formatter for comment field is not configured and comment_entity_storage_load() is not invoked, related issue #2304939: Stop loading comment statistics into entity object

EDIT another one #1919834: Field instance got no default value when created in field UI

yched’s picture

Status: Needs work » Needs review
FileSize
87.67 KB
2.61 KB

Discussed with @dewehner, he agreed that we need to render using the true field definitions, and thus render entities by bundle.

Attached patch does that. Tested with a view that includes relationships, seems to work ok (and removes the notices mentioned in #149 / #156 - but I'll let @plach vet the code.

More remarks on code organisation and responsibilty in next comment.

yched’s picture

Then, I've been discussing with @swentel, @amateescu, @dawehner and @plach (on IRC) today, about EntityViewDisplay probably not being the right place for the reusable buildFieldMultiple() method added in the current approach.

What it does is "render this $field in those $entities using this $formatter_id and $settings" - everything is self-contained in the params and, as mentioned in the @todo in the interdiff above the processing is completely independent of the actual content of the EVD. So:
- we don't really *need* to instantiate an EVD, this could be done by a stateless service; the FormatterPluginManager might be a good candidate :-)
- OTOH, that new method in the public interface of EVDs dilutes its "domain identity" and the perception of "what is an EVD and how you can use it".

@alexpott also said that he would prefer the patch here to get in with the "right" code arrangement, rather than adjusting / breaking the newly added APIs in a followup refactor.

I have started to work on a version of the patch that moves the reusable parts of "render some fields using a given formatter + settings" to the FormatterPluginManager, but first let's settle what code has to run exactly (and keeps decent perf), then we'll sort out what goes where.

marcvangend’s picture

I quickly re-ran the tests I did before with the patch from #158. Two minor changes: This time I devel-generated nodes with max. 4 comments instead of 0. I also had to select another formatter for the author field, due to recent commits in Drupal core (new view export is attached).

The performance increase is not as huge as it was in my previous test, but I guess that makes sense since the previous patch was over-simplifying things. Timings are now around 4.4 seconds before and 4.2 seconds after. This screenshot of the xhprof diff show where the biggest changes are:

diff of two profiler runs

yched’s picture

The performance increase is not as huge as it was in my previous test, but I guess that makes sense since the previous patch was over-simplifying things

IMO a likely explanation for the huge perf boost @marcvangend saw earlier was that, because of missing settings & notices discussed above, a bunch of processing was just skipped in the comment formatter - it loads and displays a thread of comments, + a comment form, so you sure save CPU cycles if you skip that :-)

Patch #158 removes that bug, meaning it's not skipping rendering anymore, so looks like a perf regression on the specific test view that was used.

yched’s picture

FileSize
87.41 KB
4.87 KB

While double-checking whether we were doing the only the right amount of formatter invocation, I noticed that actually we weren't, in the case of relationships - i.e when a row covers several entity types.

In those cases, there is one EntityFieldRenderer object by entity type, and each one of them builds *all* fields across oll entity types involved.

Discussed with @dawehner, we agreed that EntityFieldRenderer::getRenderableFieldIds() should only return the fields *this* EntityFieldRenderer should be involved with (i.e for the entity type it covers). That allows to further simplify the logic in EntityFieldRenderer::buildField()

marcvangend’s picture

I run a quick profiling of patch #162, results are similar to the results in #160.

@yched suggested that we would to a diff of the full html output (what you get with wget) to make sure the patch does not change the output. For my test case, the output was exactly the same (apart from some hashed strings in filenames and id's) so that's good.

yched’s picture

FileSize
87.37 KB
1.41 KB

Then, trying to understand the logic in EntityFieldRenderer::render(ResultRow $row, Field $field) :

- It is called iteratively for each result row and field, and returns the corresponding render array.

- On the first call, it builds the render arrays for all the fields on all the result row (doing that just once upfront is the whole intent of this issue) - conceptually :

$this->build = $this->buildFields($this->view->result);

- Then it extracts the render array for the specifc $row and $field it is currently being called - conceptually :

if (isset($this->build[$row->index][$field_id])) {
  return that
}
else {
  // The inital build does not contain anything for this field in this row.
  // Then render this single row once again - why ?
  $build = $this->buildFields([$row]);
  // And then return that - this time we don't check if it's present or not - why ?
  return $build[$row->index][$field_id];
}

I don't understand the logic in that "else" branch - why do we re-try "build this row" if the initial "build all rows" left that $row + $field empty ? Why would it work best the second time ?

Checking what happens if we remove that "second call to buildFields() on the single row".

Status: Needs review » Needs work

The last submitted patch, 164: 1867518_views-EVD-163.patch, failed testing.

plach’s picture

I did some more profiling and I didn't notice big changes in my numbers with the last patch.

yched’s picture

OK, plach explained #164 to me on IRC.
- Views API lets you execute a view and render it piecemeal, or render pieces of it twice. A couple tests do that (HandlerFieldUserNameTest even changes the definition of a field in between, which sounds a bit sick :-p)
- But to optimize memory usage for the most common case, the patch unsets the pieces of the "big pre-computed fields render array" as each row and field is rendered, so we need to re-compute it if the field/row gets rendered another time.

I'll revert #164 and put the above in a code comment :-)

yched’s picture

Status: Needs work » Needs review
FileSize
5.22 KB

Added comments for #164 / #167, and tried to streamline the code a bit :
- moved the inital "build all" to EntityFieldRenderer::preRender()
- changed EntityFieldRenderer::buildFields() to return a render array, rather than acting on $this->build.

Then, fixed a bug on EntityFieldRenderer::buildFields() for relationships : we need to collect entities by relationship, instead of one entity per row. The bug happens if the relationship stays on the same entity type.

For example, for a view with fields :
- node title
- (relationship: entity_ref field to a node) node title

If node 1 references node 2, the previous patches would display "node 1 | node 1", instead of "node 1 | node 2".

Will try to add that case to a test, because we apparently don't cover that.

yched’s picture

FileSize
88.7 KB

Er, and the actual patch

Status: Needs review » Needs work

The last submitted patch, 169: 1867518_views-EVD-168.patch, failed testing.

yched’s picture

Status: Needs work » Needs review
FileSize
88.85 KB
2 KB

- Fixed an array logic error with latest patch in case of empty result sets, which was causing the notices
- Silly me, moving the building to preRender() still needs the "only on first call" logic; also, my comment there was kind of half-baked.

yched’s picture

Streamlined the code in EVD a little bit, before moving it to somewhere else.

Also, the code in there will need to be updated after #2099137: Entity/field access and node grants not taken into account with core cache contexts, which changes the field #access logic in EVD.

The last submitted patch, 171: 1867518_views-EVD-171.patch, failed testing.

yched’s picture

So - about the actual rendering flow...

The big issue here is the "we need to run the same hooks" issue I mentioned back in #107 : the current patch doesn't run hook_entity_display_build_alter() on Views-rendered fields anymore.

hook_entity_display_build_alter($entity_render_array, $context = [Entity $entity, EVD $display]) acts at the level of the entity render array, and receives an EVD.
It is the only way 3rd party code can alter the behavior of a formatter, because for performance reasons we do not have a per-formatter alter hook (would be called hundreds of times on a listing page).
Thus, it really needs to run on Views-rendered fields, and that is why D7's field_view_field() and D8's $field->view() do what they currently do. But the hook needs to receive an EVD that contains the display settings of each of the fields that have been rendered :-/

So we're back at "we need to build an EVD and let it do the rendering" :-/

But, since the discussions around #107 earlier, we have a patch that already segragates rendering between bundles and relationships.
Which means that, among the painpoints that were identified back then about really using an EVD to do the rendering (bundles, relatioships, fields that appear several times), only the last one still needs adressing....

It should add *some* complexity to the current patch, but I would tend to think not that much.
It will also add *some* overhead, but only for views that do have fields appearing several times.

Also, I don't really see an alternative ?

yched’s picture

FileSize
87.99 KB
4.22 KB

Attached patch comes back to actually using the EVD for the display (doesn't adress "fields that appear several times within the same relationship level" for now)
Will re-do a series of perf comparisons between that one and #172, see where this puts us.

plach’s picture

Status: Needs review » Needs work

Please, let's go back to lazy-building instead of pre-building, that provides a huge performance boost in combination with #2450897: Cache Field views row output. The reason is that code in ::preRender() is always run, before render cache has any chance to kick in, thus fields are built and then markup is fetched from cache and field render arrays are simply ignored.

plach’s picture

+++ b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php
@@ -265,6 +254,76 @@ public function buildMultiple(array $entities) {
+  public function buildField(FieldableEntityInterface $entity, FieldDefinitionInterface $field_definition, $type, $settings) {
...
+  public function buildFieldMultiple(array $entities, FieldDefinitionInterface $field_definition, $type, $settings) {

With the last patch these are no longer needed...

plach’s picture

Btw, I did some quick profiling and it seems this still around 10% faster...

The last submitted patch, 175: 1867518_views-EVD-175.patch, failed testing.

yched’s picture

@plach:
I will revert the preRender part, no problem. I thought it made a bit more sense that way and was otherwise fairly innocuous, but apparently I was wrong :-)

Also, yes, I haven't removed all the parts that can be reverted with the latest approach, I wanted to check where it puts us in terms of performance first. Will try to run more tests tomorrow.

Also, I have a patch that takes care of the "same field repeated several times at the same relationship level", will post it later tonight.

yched’s picture

Status: Needs work » Needs review
FileSize
81.34 KB
10.25 KB

Updated patch for the "use actual EVD rendering" approach in #174 / #175.

- Handles the case of fields that appear several times in the view, which EVDs can't natively run. As per my original suggestion, additional occurrences of fields get split to additional "overflow" EVDs (see the "$layers" code block - the var name and code comment could use some polishing, but the logic is pretty simple)

- Reverts EVD and its interface to the current state in HEAD, those changes are not needed in that approach.

- Reverts the preRender() change I carelessely introduced in #168 (it isn't part of the interdiff because of the way I organized my git branches, but I promise it's reverted :-))

Will post a summary in the next comment.

Wim Leers’s picture

The reason is that code in ::preRender() is always run, before render cache has any chance to kick in […]

Wow, that's thoroughly confusing, because in the Render API, #pre_render callbacks do not always run: they only run on cache misses. This should be renamed to e.g. ::build() or really anything except "preRender" to avoid confusion there.


  1. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -134,35 +134,48 @@ protected function buildFields(array $values) {
    +        // Build "layers" @todo explain
    

    Interesting :)

  2. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -134,35 +134,48 @@ protected function buildFields(array $values) {
    +            // Create the Display.
    +            $display = new EntityViewDisplay([
    ...
    +            ], 'entity_view_display');
    

    I don't think I've ever seen an entity being constructed this way. Why not EntityViewDisplay::create($values)?

    Is it for performance reasons?

yched’s picture

To summarize, here are the patches for the two recent approaches :

1) 1867518-views_EVD-182-skip_EVD.patch: This is the previous "skip EVD" approach (rerolled for the conflicts after #2099137: Entity/field access and node grants not taken into account with core cache contexts)

This is basically @plach's latest patch from #142, with the couple adjustements I made between #156 and #172 : separate render by bundle, fix relationships, streamline code a bit.

I called that one "skip EVD", because that approach basically skips regular EVD rendering (even though it uses mutualized rendering code that happens to be placed in EVD.php, but doesn't really belong there).

2) 1867518-views_EVD-182-use_EVD.patch (same as #181) : This gets back to an approach that use regular EVD rendering.

It keeps the same integration with the View rendering flow, and the same logic for bundles and relationships than the "skip EVD" patch, but uses the regular $display->buildMultiple($entities) to do the rendering. Attached interdiff is from "skip EVD" patch to "use EVD" patch.

"use EVD" is the right approach IMO, for the reasons explained in #174 (basically: we have to run hook_entity_display_build_alter()). I uploaded the two patches so that we can compare performance. I'll try to run a couple benchmarks in the train back from dev days, but anyone is more than welcome to do their own :-)

yched’s picture

re @Wim #182 : I probably agree, but AFAICT that method name is pre-existing in Views, and is not introduced here ?

Wim Leers’s picture

Possibly — I didn't read the entire patch, but just reading plach's comment was already very confusing :)

yched’s picture

Benchmarked "skip_EVD vs 8.0.x" and "use_EVD vs 8.0.x" on a couple sample views I used for testing so far.

- Relationship
Fields: node type, node title, [node_ref relationship] node type, [node_ref relationship] node title
--> 50 results spanning 2 node types

skip_EVD vs 8.0.x (negative is a win) :
ct : 477,521|414,968|-62,553|-13.1%
wt : 2,815,659|2,399,228|-416,431|-14.8%
mu : 25,396,584|28,200,944|2,804,360|11.0%
pmu : 25,779,040|28,573,256|2,794,216|10.8%

use_EVD vs 8.0.x (negative is a win) :
ct : 477,521|416,026|-61,495|-12.9%
wt : 2,815,659|2,408,794|-406,865|-14.5%
mu : 25,396,584|28,202,336|2,805,752|11.0%
pmu : 25,779,040|28,583,472|2,804,432|10.9%

- Fields present several times
Fields: node type, node title, 1st delta of field_int, remaining deltas of field_int
--> 50 results spanning 2 node types

skip_EVD vs 8.0.x (negative is a win) :
ct : 540,359|525,290|-15,069|-2.8%
wt : 3,250,362|3,128,156|-122,206|-3.8%
mu : 29,016,216|28,831,600|-184,616|-0.6%
pmu : 29,417,648|29,258,008|-159,640|-0.5%

use_EVD vs 8.0.x (negative is a win) :
ct : 540,359|526,395|-13,964|-2.6%
wt : 3,250,362|3,123,271|-127,091|-3.9%
mu : 29,016,216|28,833,192|-183,024|-0.6%
pmu : 29,417,648|29,242,488|-175,160|-0.6%

- "Heavy" view, inspired from the one @marcvangend used in #160
Fields: node type, node title, node author, node body, node image, comments stats count, comment (list + form), [author relationship] email, [author relationship] name
--> 50 results spanning 2 node types

skip_EVD vs 8.0.x (negative is a win) :
ct : 994,023|960,945|-33,078|-3.3%
wt : 2,328,680|2,231,958|-96,722|-4.2%
mu : 44,413,944|44,929,056|515,112|1.2%
pmu : 47,454,384|47,918,408|464,024|1.0%

use_EVD vs 8.0.x (negative is a win) :
ct : 994,023|961,409|-32,614|-3.3%
wt : 2,328,680|2,239,833|-88,847|-3.8%
mu : 44,413,944|44,929,328|515,384|1.2%
pmu : 47,454,384|47,907,408|453,024|1.0%

yched’s picture

In other words : as expected, the "use_EVD" patch is a little more costly than the former "skip_EVD" approach in some cases, but the difference remains reasonable IMO ?

yched’s picture

I'll be afk next week, so I'm posting the (minor) remarks / questions i had about the "other" parts in the patch (i.e outside of EntityFieldRendered / EntityViewDisplay), that I didn't touch :

  1. +++ b/core/lib/Drupal/Core/Entity/EntityManager.php
    @@ -855,7 +855,7 @@ public function getEntityTypeLabels($group = FALSE) {
    -    if ($entity instanceof TranslatableInterface) {
    +    if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) {
    
    +++ b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
    @@ -1139,6 +1139,9 @@ public function testGetTranslationFromContext() {
    +    $entity->expects($this->any())
    +      ->method('getTranslationLanguages')
    +      ->will($this->returnValue([new Language(['id' => 'en']), new Language(['id' => 'custom_langcode'])]));
    

    Not sure why those changes are needed, but I guess there's a good reason :-)
    (not sure if that's still the case with the current patch though ?)

  2. +++ b/core/modules/rdf/rdf.module
    @@ -216,6 +216,11 @@ function rdf_entity_prepare_view($entity_type, array $entities, array $displays)
    +          // @todo Initialize field item attributes, if they are missing. Remove
    +          //   this in https://www.drupal.org/node/2450477.
    +          if (!isset($item->_attributes)) {
    +            $item->_attributes = array();
    +          }
    

    Likewise, I'm wondering if that change is still needed ?

  3. +++ b/core/lib/Drupal/Core/Field/FormatterInterface.php
    @@ -60,7 +60,7 @@ public function settingsSummary();
    -   *   Array of field values, keyed by entity ID.
    +   *   An array of field item values.
    

    Both the current comment and the new one are bit sloppy :-)
    If we want to keep this hunk, I'd suggest "An array with the field values from the multiple entities being rendered." ?

yched’s picture

Polished the comments in buildFields() a bit, trying to explain the code flow better.

(also, switched from new EntityViewDisplay() to EntityViewDisplay::create() ?)

effulgentsia’s picture

Re #186, why are the absolute differences (not only percent differences) in the first scenario ("Relationship" view) more than in the last scenario (the "Heavy" view that also includes relationships)?

dawehner’s picture

Some remarks / questions on the latest iterations.

  1. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,297 @@
    +   * - Each relationship is handled separately, since it operates on its own
    +   *   set of entities.
    

    Mh, this doesn't explain it for me, honestly.

  2. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,297 @@
    +   * - For each set of fields, entities are processed by bundle, so that
    +   *   formatters can operate on the proper field definition for the bundle.
    

    I'd honestly hate to do that. The code is again so incredible complex ... and to be honest, it just doesn't make sense for me. Why care we about the bundles, if everything which really matters, is the formatter type and setting which is part of views itself.

  3. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,297 @@
    +            $display = EntityViewDisplay::create([
    +              'targetEntityType' => $entity_type_id,
    +              'bundle' => $bundle,
    +              'status' => TRUE,
    +            ]);
    

    I think we really should not indicate that we render a proper EVD but rather continue to set 'bundle' => _custom ... which seemed to work quite fine. I think the problem is just partially performance, but my main concern is the understandability of the system.

  4. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,297 @@
    +        unset($this->build[$row->index][$field_id]);
    ...
    +        unset($this->build[$row->index]);
    

    Yeah, I'm not sure whether we should really worry about the memory here, but it seems to be that it matters ... I'm curious primarily, because the potential chance to have to re-render a field, seems not worth the memory overhead, just saying.

  5. +++ b/core/modules/views/src/Entity/Render/EntityTranslationRendererBase.php
    @@ -0,0 +1,64 @@
    +      $entity = $row->_entity;
    

    It would be great if we could put a TODO for #2457999: Cannot use relationship for rendered entity on Views in there.

  6. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -707,6 +692,107 @@ public function renderItems($items) {
    +    $entity_type_id = $this->getEntityType();
    +    if (!isset($this->entityFieldRenderer[$entity_type_id])) {
    

    Its honestly confusing, and I doubt we need it. ... Why should we store $this->entityFieldRenderer by entity type? The entity type of a field should not be more than one.

yched’s picture

re #191

1. Lol - I had to find it the hard way in #168, but I thought it would have been clearer for you of all people :-)

Basically, the explanation lies in FieldPluginBase::getEntity() (which is the method EntityFieldRenderer::buildFields() calls to get the entity that we need to render). It does :

    $relationship_id = $this->options['relationship'];
    if ($relationship_id == 'none') {
      return $values->_entity;
    }
    else {
      return $values->_relationship_entities[$relationship_id];
    }

So, the entity that contains the loaded values for the field is in :
- $values->_entity if the field isn't on a relationship (i.e is the entity from the base table),
- $values->_relationship_entities[$relationship_id] if the field is on a relationship (is on a joined/referenced entity)
*And it's not the same entity*, even if they happen to have the same entity type. One is the referring entity (left end of the relationship), the other is the entity being referred to (right end of the relationship).

In short, the relationships present in a row, define "spaces", each with their set of entities, and fields to display, and display settings for those fields.
The $entities for the fields without a relationship are different than the entities for the fields on the relationship "node author" or "node reference". Thus, they cannot be rendered together, of course - and even if they happen to be of the same entity type.

2. I thought we had settled that in Montpellier ? Sorry, but no other way, the formatters need to receive the correct field definition *for the bundle*, or they won't render the right thing (HEAD is slow but correct on that, so that would be a regression). See #156 and the errors mentioned by @marcvangend above.

3. Same reason than 2, see #156.

4. I'd happily remove that, but @plach and @Fabianx seem to think it has a real perf impact, so I believe them :). See #164 & #167.

5. No opinion, not my part of the patch, but I likely won't have the time to take care of that myself before I go afk :-)

6. Likewise, not my part of the patch, i did not touch what @plach put in place for the integration with the overall rendering pipeline. I'm fine with it and was grateful that it works (I would have been unable to pull that part, for that matter), but have no strong opinion about it.

[edit : deleted stupid comment. Please forgive me, I guess I'm a bit tired tonight]

yched’s picture

Unfortunately I won't be able to spent more time on the patch before another week, I'm going afk tomorrow and need to prepare...

I tried to explain in detail in the issue the changes that I did on top of @plach's and why I do think they are required
- clustering by bundle : #156
- clustering by relationship : #168 and #192.1 just above
- creating EVDs and using their regular building flow : #174

I also I tried to check that the perf impact compared to the previous approach was not too big, and to make the code and comments as clear as I could in a limited time today. We can certainly do better, I can give it another shot when I get back :-)

I agree that the code is not completely trivial, but I don't think it's that complex after all. It's combinatory logic to group things that are similar and can be rendered together. Recombine, then uncombine. I don't know how we could avoid that, we're rendering rows in a table, trying to "multiple-do" stuff as much as we can.

Also, my sincere apologies to @plach for taking the patch away (well, the part below EntityFieldRenderer::buildFields()). I was not too available before dev days, and then we didn't get a chance to discuss live or on IRC when I finally had time for the issue. that's unfortunate.

I can re-explain / try to explain better the latest approach and why I see no other option, we can set up a call if needed. But next week :-)

yched’s picture

Assigned: yched » Unassigned

Unassigning

Fabianx’s picture

Some short comments:

Now that we know that without rendering by bundle some formatters, like comment can't work, we indeed should use the EVD approach again (+1 for that).

I think it is worth it to still add 'field_alias' support to EVD to not having to create EVD displays for overflow-fields.

While I explored that possibility myself, in retrospective I don't think this is worth it.

And that even works nicely with the schema (field_name == field_alias for all other core uses) if we wanted to export it.

So I think that is a step that is worth it if we go the EVD route.

yched’s picture

re: #195

true, contrary to what I wrote earlier up, it would indeed probably be pretty simple to add support for field aliases in EVD, removing the need for the "overflow " displays here. I could have come up with that idea earlier on, maybe it would have saved us some back and forth dance on the approach :-/

Also, we can still explore #2466913: Allow EVD to render fields across bundles to move the complexity of cross-bundle grouping into EVD (a bit more hypothetical though)

Both points look like optimizations that could go in follow-ups IMO

plach’s picture

I did a final profiling on #182 (skip EVD) and #189 (use EVD) with my usual test view (see #127). Attached you can find the details, basically as in #186 we have no big differences in the two approaches (both ~11% faster than HEAD).

I tried to comment the two lines freeing static cache in #189 and the improvement is still substantial, around 5% more memory used without it.

Performance-wise, I think both approaches are acceptable. I agree with Daniel that #189 is more complex than I'd like it to be, however as suggested in #195/#196 there's room to make it simpler, which could make it even slightly more performant, I think.

All that said, I think we spent enough time on this issue and if #189 is more correct, in that it leverages the Entity Display API in a more correct way, we should go on with it and perform incremental improvements in follow-ups. In an ideal word I think we should abstract out the Views field rendering engine, which is more advanced than the Entity one, into a standalone component both subsystem can rely on (this would make also having common theming easier), however this would definitely be D9 stuff, unless we can come up with a very clever BC layer.

I'll address the non-controversial pending review items now.

The only concern I still have, is whether the fact that #182 is green indicates a lack of test coverage somewhere.

@dawehner, #191.6:

Its honestly confusing, and I doubt we need it. ... Why should we store $this->entityFieldRenderer by entity type? The entity type of a field should not be more than one.

I had to introduce that in #140 to be able to pass the relationship on to the various methods that require it. Not sure how to fix it otherwise.

HEAD vs EVD_skip

HEAD vs EVD_use

HEAD vs EVD_use (static cache not unset)

Fabianx’s picture

Okay, lets create the follow-up for EVD and field aliases as major follow-up tasks to reduce complexity and get this in?

It is RTBC from my side, leaving final review / patch to plach.

plach’s picture

This should address #188 and #191 (except bullet 1), and clean-up some code.

plach’s picture

This is an attempt to address #191.1 and #191.6. Let's see whether the bot likes it.

Fabianx’s picture

#200 looks really great!

The relationship thing feels better than entity_type's as in views that is the only way that you can get a new relationship and fields are already self-aware of that ...

Also the code in the end is not as complex as I feared ...

The $display_sets works nicely like my $builder_index in #116.

Nice work!

yched’s picture

<3 #200 :-)

plach’s picture

Thanks, Fabian.

This performs a final code clean-up. If it's green, it's RTBC for what I'm concerned.

plach’s picture

@yched, #188.1:

Not sure why those changes are needed, but I guess there's a good reason :-)
(not sure if that's still the case with the current patch though ?)

That's an optimization to avoid needlessly invoking hook_language_fallback_candidates_alter().

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Checked it again, RTBC if tests pass for #203.

Fabianx’s picture

Assigned: plach » dawehner

Assigning to dawehner for taking a final look, too.

However core committers can start looking too already as this will take a while to review ...

We still need to create the follow-up issue:

- "Simplify logic in Views field renderer by allowing EVD to have field aliases"

effulgentsia’s picture

Any thoughts on #190? Why do the profiling results of #186 show a much smaller impact of the patch for 2 of the 3 test views? #197 seems to match the result of the first test View from #186, but how sensitive is that result to which View is tested?

plach’s picture

I will dig into this, trying to recreate @yched's test view. However the perf gain is surely bound to the number of fields and rows displayed.

Fabianx’s picture

I think the win on author and comments is less than the win on nodes.

dawehner’s picture

Summary: RTBC+1 great work!

  1. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,289 @@
    +      // For each set of fields, build the output by bundle.
    +      foreach ($display_sets as $display_fields) {
    +        foreach ($entities_by_bundles as $bundle => $bundle_entities) {
    

    Given that these bits are more or less on-time operations we don't have to care about really in the performance regard, as the numbers are showing.

  2. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,289 @@
    +  protected function getRenderableFieldIds() {
    +    if (!isset($this->renderableFieldIds)) {
    +      $this->renderableFieldIds = [];
    

    I like the idea for the by-relationship storage, that makes just sense!

  3. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -115,6 +112,13 @@ class Field extends FieldPluginBase implements CacheablePluginInterface, MultiIt
       /**
    +   * Static cache for ::getEntityFieldRenderer().
    +   *
    

    Oh nice idea for documentation.

plach’s picture

Assigned: dawehner » Unassigned

Cool, thanks :)

effulgentsia’s picture

Assigned: Unassigned » effulgentsia

I'll review this today. Additional reviews and patches addressing any remaining nits are still welcome. As are details on #208 / #209.

Wim Leers’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,289 @@
    +   * Constructs a renderer object.
    

    Nit: s/a renderer/an EntityFieldRenderer/

  2. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,289 @@
    +   * @param \Drupal\views\ViewExecutable $view
    +   *   The entity row being rendered.
    

    Comment doesn't quite match the parameter.

  3. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,289 @@
    +   * - Each relationship is handled by separate EntityFieldRenderer instances,
    

    Nit: "separate instances" -> "a separate instance"

  4. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,289 @@
    +   *   contain the same field several times with different display options)
    

    Missing trailing period.

  5. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,289 @@
    +  protected function buildFields(array $values) {
    ...
    +  public function render(ResultRow $row, Field $field = NULL) {
    

    These seemed tricky at first, but actually aren't tricky at all, in no small part thanks to the excellent code comments.

  6. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,289 @@
    +   * Return the code of the language the field should be displayed in.
    

    Nit: s/Return/Returns/

  7. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,289 @@
    +        // array was removed be the unset() above. We the manually rebuild the
    +        // render array for the row.
    

    s/We the/ We have to/

  8. +++ b/core/modules/views/src/Entity/Render/EntityTranslationRendererBase.php
    @@ -0,0 +1,66 @@
    +    return ['languages'];
    

    This means "vary by all language types".

    This is wrong, because:

    • class EntityFieldRenderer extends RendererBase, which does return $this->getEntityTranslationRenderer()->getCacheContexts();
    • abstract class EntityTranslationRendererBase extends RendererBase (this class), which returns ['languages']
    • this patch removes RendererBase::getCacheContexts(), which used to return the empty array
    • class TranslationLanguageRenderer extends EntityTranslationRendererBase, but that class overrides it to return ['languages:' . LanguageInterface::TYPE_CONTENT]; anyway
    • class ConfigurableLanguageRenderer extends EntityTranslationRendererBase, which uses a hardcoded (well, configured) language, which means there isn't anything being varied, so the default of the base class (['languages']) is wrong
    • class DefaultLanguageRenderer extends EntityTranslationRendererBase uses the entity's default language, which means there isn't anything being varied, so the default of the base class (['languages']) is wrong

    AFAICT this should just be the empty array.

    "wrong" is actually the wrong word, "suboptimal" is better. You can vary by ten unnecessary cache contexts and still get correct results: it'd just be suboptimal. Similarly here: we vary by more things than necessary.

    NW for this.

  9. +++ b/core/modules/views/src/Tests/Handler/FieldFieldTest.php
    @@ -232,7 +420,72 @@ public function testRevisionRender() {
    +   * Tests the revision result.
    

    "the revision result"?

    A better description would help with the maintainability/understandability of this test.

effulgentsia’s picture

Not a full review yet, but some things I found so far:

  1. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,289 @@
    + * Defines a base class for entity translation renderers.
    + */
    +class EntityFieldRenderer extends RendererBase {
    

    Wrong docs.

  2. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -762,15 +839,15 @@ public function getItems(ResultRow $values) {
    -  function process_entity(ResultRow $values, EntityInterface $entity) {
    +  public function processEntity(ResultRow $values, EntityInterface $entity, $clone = TRUE) {
    

    Looks like this function is no longer called by anything. Instead of renaming it and changing its signature, should we just remove it?

  3. +++ b/core/modules/views/src/Tests/Handler/FieldFieldTest.php
    @@ -48,14 +50,45 @@ class FieldFieldTest extends ViewUnitTestBase {
    +    // Bypass any field access.
    +    $this->adminUser = User::create([
    +      'admin_role' => true,
    +    ]);
    

    Does this work? I don't think "admin_role" is a meaningful property on user entities anymore. But also, this is a test environment, so what field access do we need to bypass?

dawehner’s picture

Looks like this function is no longer called by anything. Instead of renaming it and changing its signature, should we just remove it?

Good catch!! Its so great that you found it now!

If this is the case, we have a big bug / missing test coverage as this is the code which handles group by support, but I'm pretty sure we had figured that out earlier ...
#2081533: Views "group by" on "Field API" fields broken and #1866260: Undefined variable in Field:process_entity() seems both still open.

plach’s picture

Status: Needs work » Needs review
FileSize
7.87 KB
74.44 KB

This should address #213 and #214, except for #214.2 as I'm not sure how to manually test it. I tried creating a views using aggregation and it seems to work correctly.

Digging into #190 now.

Wim Leers’s picture

Glad to see you agreed with #213.8, and liking your fix even more: this now effectively means less changes :)

plach’s picture

FileSize
18.58 KB

I thoroughly profiled/traced the attached view, and I get similar results to #186. In this case the perf improvement is only ~6%. The main reason is rendering of the Comments field is very heavy at the moment: each one triggers a regular load multiple / view multiple for the node's comments and it seems render cache is not exploited.

Removing the Comments field suddenly pushes the perf improvement to ~9%, which is consistent with the previous results, since we have one less Entity Field to display and thus we should expect a smaller gain.

@Wim:

Glad to see you agreed with #213.8, and liking your fix even more: this now effectively means less changes :)

Actually, I just tried to replicate HEAD's behavior :)

plach’s picture

Issue tags: +Needs tests
FileSize
837 bytes
74.5 KB

I also had to introduce this fix as the test view above was triggering some notices with my DB. We'll have to expand test coverage to account for that and likely for #214.2, so this is actually NW.

Status: Needs review » Needs work

The last submitted patch, 219: views-entity_display-1867518-219.patch, failed testing.

plach’s picture

Status: Needs work » Needs review
FileSize
74.46 KB

Rerolled

yched’s picture

Interdiff #221 doesn't look unreasonable, but curious to know what is the case that needed it ?

Also, right, testing on a view with the comments field shows less perf gains in comparison, because the comment formatter is very costly, so the perf gains are smaller because most of the time is spent there anyway.

Patch would bring more gains if the comment formatter did multiple load in prepareView(), but I don't know if that's doable

plach’s picture

Here is the additional test coverage for #219, the failing patch just reverts the related interdiff.

Waiting for Daniel's feedback for #214.2.

Edit: the interdiff includes the failing patch changes, but the passing patch should be correct.

The last submitted patch, 223: views-entity_display-1867518-223-fail.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 223: views-entity_display-1867518-223.patch, failed testing.

Status: Needs work » Needs review
plach’s picture

To clarify: I manually tested aggregation and I was not able to find problems, I asked @dawehner to provide a failing test view so I can find the proper fix. What's currently trying to achieve ::processEntity() is not very clear to me atm.

Fabianx’s picture

Assigned: effulgentsia » dawehner

Assigning to dawehner for feedback on #214.2.

iMiksu’s picture

Lets see what tests would say about #214.2

Status: Needs review » Needs work

The last submitted patch, 229: views-entity_display-1867518-229.patch, failed testing.

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 229: views-entity_display-1867518-229.patch, failed testing.

plach’s picture

Status: Needs work » Needs review
FileSize
5.76 KB

The interdiff in #229 is not correct, the attached one is. In fact by applying the interdiff posted in #229 to my local branch tests are green, while with this one they fail.

Were changes to the test views intended?

(patch to review still in #223)

yched’s picture

Not sure what is left here ? Check whether processEntity() is still useful ?

iMiksu’s picture

Issue summary: View changes

Sorry, I misunderstood interdiffs I guess.

plach’s picture

@yched:

I'm waiting for Daniel to provide a test view not working correctly due to the missing ::processEntity() invocation.

Status: Needs review » Needs work

The last submitted patch, 229: views-entity_display-1867518-229.patch, failed testing.

plach’s picture

Status: Needs work » Needs review

@iMiksu:

Are the changes made to the test view in #229 meant to expose the problem with not calling ::processEntity()?

dawehner’s picture

Assigned: dawehner » Unassigned
Issue summary: View changes
Issue tags: -Needs tests
FileSize
35.27 KB
81.29 KB
4.49 KB

Did quite some research here, to understand where its coming from, how it supposed to be used etc.
Thank you bojanz for the quick discussion.

When you deal with aggregation you always have two parts:

  • Fields you aggregate by, count(node.nid) for example
  • Fields you group by, for example node.type

Aggregation

The first case is handled in views by replacing the used field handler entirely and use a specific one, just used for aggregation,
see core/modules/views/src/Plugin/views/display/DisplayPluginBase.php:877 and \Drupal\views\Plugin\views\query\Sql::getAggregationInfo

Group by

Group by, especially configurable fields, is more tricky, let's have a look at an example like a custom text field with a 'value' column. For that one
we join to the field table, create an sql field alias pointing to the textfield.value column and finally use that as part of the GROUP BY part of the query.
In order to somehow be able to still render such a field, we additional need some entity to work with. The way how we do it is to add a MIN(node.nid) to the query in that case.
This allows us to get a NID to load an entity from, while at the same time not extend the amount of results.

This particular entity has the interesting property that the values of the grouped columns are the same as GROUP BY values of the query,
so when you render this entity with grouped BY field, you end up rendering the data you expect it to be there.

This all works fine, until you come to a multidelta field. So if you have field_test.0.value field_test.1.value and field_test.2.value.

So you have:

$data = [];
$data[] = [
  'nid' => 1
  'field_test' => [1, 1, 1],
];
$data[] = [
  'nid' => 2
  'field_test' => [2, 2, 2],
];
$data[] = [
  'nid' => 3
  'field_test' => [2, 2, 2],
];

If you don't process those values, you end up with something which looks like the result on the screenshot:

As you see, its not really exactly what you expect.
Let's write a test for that, which works after a bit of tweaking for HEAD.
As already seen in the screenshot, it fails on #223 + #233

I think one way to fix it now would be to special case the rendering, in case we group by the field, entirely, which means to maybe just call $process_entity->{$field_name}->view() as we should IMHO not make the code for the common case more complex, see interdiff.

Status: Needs review » Needs work

The last submitted patch, 240: 1867518-240.patch, failed testing.

yched’s picture

+++ b/core/modules/views/src/Plugin/views/field/Field.php
@@ -802,7 +802,16 @@ protected function getEntityFieldRenderer() {
+    }

No strong opinion on this, works for me, just : if processEntity() is used only for groupBy, and since we're renaming it anyway, can we rename it to something more clearly related to what it is actually about ? ;-)

dawehner’s picture

Fair point, what about processEntityForGroupby()?

dawehner’s picture

Fair point, what about processEntityForGroupby()?

plach’s picture

@yched, you'd better reply :)

yched’s picture

Wow, I'd better reply indeed ;-)

processEntityForGroupby() works for me - proper camelCasing would be "GroupBy" though ?

A couple more remarks on that repurposing of the method for "groupBy handling only" :

1. (minor) func args : given that it primarily acts on and returns an entity, maybe $entity should be the first param ?

2. func args : the patch adds a boolean switch param for "clone the entity or modify in place", but the method is only ever called from one place, without a value for that param --> do we still need that switch ?

3. That single place that calls the method already takes care of checking "if (displayHandler->useGroupBy())", so the "// If we are grouping" check inside the method seems moot/duplicate ?

5. There is remaining code outside of that "If we are grouping" code branch :

    // The field we are trying to display doesn't exist on this entity.
    if (!isset($processed_entity->{$this->definition['field_name']})) {
      return FALSE;
    }

Is that still relevant as part of the method ?
[edit : given the calling code, it seems returning FALSE will fatal out ?]

4. From the phpdoc : "Takes delta settings into account (@todo remove in #1758616)." - should be removed now, that part has been moved somewhere else :-)

dawehner’s picture

FileSize
81.45 KB
1.45 KB

1 word: stupidity

dawehner’s picture

Status: Needs work » Needs review

Back to needs review.

plach’s picture

This should address #247.

3/5: I left the !empty check because it prevents PHPStorm from frowning upon dynamic properties, but I merged it with the following check.
4: The related code was moved to ::prepareItemsByDelta() but the @todo no longer applies I think, so I just removed that bit.

I think we should be ready to go back to RTBC now, @yched?

Status: Needs review » Needs work

The last submitted patch, 250: views-entity_display-1867518-249.patch, failed testing.

yched’s picture

@plach: thanks !

Last nitpick on that "group by" part:

+++ b/core/modules/views/src/Plugin/views/field/Field.php
@@ -716,66 +802,66 @@ public function renderItems($items) {
+   * Processes an entity before rendering grouped field values.
...
+  public function processEntityGroupBy(EntityInterface $entity, ResultRow $values) {
     $processed_entity = clone $entity;

So if this now always returns a new $entity, the phpdoc and method name could be refined a bit ?
Maybe like :
createEntityForGroupBy()
"Creates a fake entity with grouped field values"
?

Other than that, I'll try to have a last look at the overall patch tonight, but I think we should be done here :-)

iMiksu’s picture

@plach #239: Yes :)

plach’s picture

Status: Needs work » Needs review
FileSize
2.33 KB
82.27 KB

I did not add the new test file...

plach’s picture

... and I missed #252 :)

plach’s picture

FileSize
82.85 KB

This time with patch...

Wim Leers’s picture

1288 insertions, 235 deletions — the majority of the insertions actually comes from the expanded test coverage.

I'm no Views expert, but I can sign off on the entity row renderer changes and the general use of cacheability metadata. RTBC as far as I'm concerned. I can't even find nitpicks! Stellar work, plach!

I'll leave it to yched to RTBC.

plach’s picture

Thanks, a lot :)

Obviously a huge thanks goes to Daniel who provided most of the additional test coverage and lots of functional code and great reviews.

yched’s picture

Hem - mostly nitpicks :-)

  1. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,293 @@
    +      // Collect the entities for the relationship, fetch the right translation,
    +      // and group by bundle.
    +      $entities_by_bundles = [];
    +      $field = $this->view->field[current($field_ids)];
    +      foreach ($values as $result_row) {
    +        $entity = $field->getEntity($result_row);
    +        $entities_by_bundles[$entity->bundle()][$result_row->index] = $this->getEntityTranslation($entity, $result_row);
    +      }
    

    Rer-reading that code, I think it's the last part that is a bit obscure, maybe the following comment would help :
    "For each result row, the corresponding entity can be obtained from any of the fields handlers, so we arbitrarily use the first one".

  2. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,293 @@
    +  protected function getRenderableFieldIds() {
    +    if (!isset($this->renderableFieldIds)) {
    

    That method is only ever called once in a regular view render flow, do we still need the static cache ?

    Also, it would make the code in buildFields() a bit simpler it the method directly returned the handler objets from $this->view->field instead of just their keys/IDs ?

  3. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,293 @@
    +  public function render(ResultRow $row, Field $field = NULL) {
    

    Ubernitpick : would make the class easier to parse if the method came before the protected helpers it internally uses - following execution order, that's after query() ?

  4. +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php
    @@ -0,0 +1,293 @@
    +        // array was removed be the unset() above. We have to manually rebuild
    

    my bad - s/be/by

  5. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -707,6 +692,107 @@ public function renderItems($items) {
    +  protected function getEntityFieldRenderer() {
    +    if (!isset($this->entityFieldRenderer[$this->relationship])) {
    

    A Field has only one relationship, so not sure why we need to key $this->entityFieldRenderer by $this->relationship ? The code would be a bit simpler to understand with :

    foreach ($this->view->field as field) {
      if ($field->relationship == $this->relationship && isset($field->entityFieldRender)) {
        $this->entityFieldRender = $field->entityFieldRender;
        break;
      }
    }
    

    Also, a comment could help, that's some non-trivial logic here, took me a while to convince myself we're only creating one renderer per relationship :-). Something like "We create one EntityFieldRender per relationship, which will be used for all fields in that relationship. So first check if the EntityFieldRender for this field has already been created for another field" ?

  6. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -707,6 +692,107 @@ public function renderItems($items) {
    +      if (!empty($this->view->field)) {
    +        foreach ($this->view->field as $field) {
    

    The empty() check looks extraneous, isn't $this->view->field always at least an empty array ?
    [edit: actually, at least an array with one field if this code is being run in the first place ? ;-) ]

  7. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -716,72 +802,72 @@ public function renderItems($items) {
    +      // For grouped results use the process entity functionality, as this
    

    "the process entity functionnality" is a bit off after the last method rename :-)

  8. +++ b/core/modules/views/src/Plugin/views/field/Field.php
    @@ -716,72 +802,72 @@ public function renderItems($items) {
    +      $build_list = $this->createEntityForGroupBy($this->getEntity($values), $values)
    +        ->{$this->definition['field_name']}
    +        ->view($display);
    

    So for fields using groupBy, we keep the old unefficient field-by-field rendering. I think there could be a way to keep the groupBy case in the same rendering flow as the "regular" case, by moving the if (useGroupBy) {$entity = createEntityForGroupBy()} inside EntityFieldRenderer::buildFields(). Or maybe: if a field uses groupBy, handle it separately with its own dedicated EntityFieldRenderer ?

    We can totally keep that for a followup if at all - but : EntityFieldRenderer::buildFields() is also the one that takes language and translation into account, right ? So it looks like the current code skips this aspect for fields using GroupBy ? Is that an issue ?

dawehner’s picture

Obviously a huge thanks goes to Daniel who provided most of the additional test coverage and lots of functional code and great reviews.

It was great to work with you on such a fundamental part of the system!

plach’s picture

@yched:

Great review, as usual! I think should address all its bullets.

We can totally keep that for a followup if at all - but : EntityFieldRenderer::buildFields() is also the one that takes language and translation into account, right ? So it looks like the current code skips this aspect for fields using GroupBy ? Is that an issue ?

Yep, @dawehner and I agreed that this case is uncommon enough to be handled in a non-critical follow-up, which I'm going to create ASAP. I'm working to some additional test coverage for the group by/translation issue, posting this meanwhile to ensure I didn't break anything.

yched’s picture

Status: Needs review » Reviewed & tested by the community

Thanks @plach !

I participated in parts of the patch, but those have been largely peer-reviewed since then, and others have already expressed their approval, so I think it's OK for me to push the button.

Yay :-)

Wim Leers’s picture

Hurray! :) Thank you, plach, dawehner and yched! It's the three of you that really brought this to the finish line :)

plach’s picture

Thanks for the RTBC but I promised some additional test coverage :)

plach’s picture

Status: Reviewed & tested by the community » Needs review
dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Nice!!

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 264: views-entity_display-1867518-264.patch, failed testing.

Status: Needs work » Needs review
Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs change record

I think we need change records for both the views and entity display changes? No?

+++ b/core/modules/views/src/Tests/QueryGroupByTest.php
@@ -7,6 +7,11 @@
+use Drupal\entity_test\Entity\EntityTest;

Can be removed - not used.

yched’s picture

@alexpott: I can't speak for the Views side, but the final patch has no API change whatsoever on the entity display side - doesn't even touch any file in there.

dawehner’s picture

Issue tags: -Needs change record

Thew new internal class EntityFieldRenderer is also just a pure internal detail, something noone would actually have to deal with.

The changes to RendererBase also doesn't matter, as they are not extensible.
The rest of the changes are test only, so I would argue that we don't need a change record.

plach’s picture

Status: Needs work » Needs review

@alexpott:

What do you think about the comments above?

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Yes, we do not need a change record here.

This is an internal optimization.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Thanks for the answers to my CR question.

This issue is a critical task and is allowed per https://www.drupal.org/core/beta-changes. Committed 13922db and pushed to 8.0.x. Thanks!

diff --git a/core/modules/views/src/Tests/QueryGroupByTest.php b/core/modules/views/src/Tests/QueryGroupByTest.php
index e17ceba..ad3e225 100644
--- a/core/modules/views/src/Tests/QueryGroupByTest.php
+++ b/core/modules/views/src/Tests/QueryGroupByTest.php
@@ -7,7 +7,6 @@
 
 namespace Drupal\views\Tests;
 
-use Drupal\entity_test\Entity\EntityTest;
 use Drupal\entity_test\Entity\EntityTestMul;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;

Fixed on commit.

  • alexpott committed 13922db on 8.0.x
    Issue #1867518 by plach, dawehner, yched, iMiksu, epari.siva,...
yched’s picture

W00t ! Awesome !

Fabianx’s picture

Fantastic! Thanks so much @all!

plach’s picture

Yay, finally!

dawehner’s picture

mradcliffe’s picture

Status: Fixed » Needs work

If order by is being worked on, it should be tested in PostgreSQL too especially.

This patch caused 11 failures in Drupal\views\Tests\QueryGroupByTest. It looks like the order is not guaranteed for the following queries. I actually don't see any order by being added to these queries, which is circumspect because the views in the patch have sorts defined. It may take some time to review this large patch to see what's going on...

SELECT entity_test_mul__field_test.field_test_value AS entity_test_mul__field_test_field_test_value, entity_test_mul_property_data.langcode AS entity_test_mul_property_data_langcode, COUNT(entity_test_mul_property_data.id) AS id, MIN(entity_test_mul_property_data.id) AS id_1
FROM 
{entity_test_mul_property_data} entity_test_mul_property_data
LEFT JOIN {entity_test_mul__field_test} entity_test_mul__field_test ON entity_test_mul_property_data.id = entity_test_mul__field_test.entity_id AND (entity_test_mul__field_test.deleted = :views_join_condition_0 AND entity_test_mul__field_test.langcode = entity_test_mul_property_data.langcode)
GROUP BY entity_test_mul__field_test_field_test_value, entity_test_mul_property_data_langcode
LIMIT 10 OFFSET 0

Arguments: Array
(
    [:views_join_condition_0] => 0
)
SELECT entity_test_mul__field_test.field_test_value AS entity_test_mul__field_test_field_test_value, entity_test_mul_property_data.langcode AS entity_test_mul_property_data_langcode, COUNT(entity_test_mul_property_data.id) AS id, MIN(entity_test_mul_property_data.id) AS id_1
FROM 
{entity_test_mul_property_data} entity_test_mul_property_data
LEFT JOIN {entity_test_mul__field_test} entity_test_mul__field_test ON entity_test_mul_property_data.id = entity_test_mul__field_test.entity_id AND (entity_test_mul__field_test.deleted = :views_join_condition_0 AND entity_test_mul__field_test.langcode = entity_test_mul_property_data.langcode)
GROUP BY entity_test_mul__field_test_field_test_value, entity_test_mul_property_data_langcode
LIMIT 10 OFFSET 0

Arguments: Array
(
    [:views_join_condition_0] => 0
)
SELECT entity_test_mul__field_test.field_test_value AS entity_test_mul__field_test_field_test_value, entity_test_mul_property_data.langcode AS entity_test_mul_property_data_langcode, COUNT(entity_test_mul_property_data.id) AS id, MIN(entity_test_mul_property_data.id) AS id_1
FROM 
{entity_test_mul_property_data} entity_test_mul_property_data
LEFT JOIN {entity_test_mul__field_test} entity_test_mul__field_test ON entity_test_mul_property_data.id = entity_test_mul__field_test.entity_id AND (entity_test_mul__field_test.deleted = :views_join_condition_0 AND entity_test_mul__field_test.langcode = entity_test_mul_property_data.langcode)
GROUP BY entity_test_mul__field_test_field_test_value, entity_test_mul_property_data_langcode
LIMIT 10 OFFSET 0

Arguments: Array
(
    [:views_join_condition_0] => 0
)
mradcliffe’s picture

Berdir’s picture

Status: Needs work » Fixed

Can you please open a new issue for this?

This was a huge and long issue with almost 300 comments and many participants, and reopening issues is very confusing.

plach’s picture

@mradcliffe:

This patch did not touch any Views querying component, it just refactored entity field rendering. It did add lots of test coverage in the process so it's likely that it just uncovered an existing bug. This is a critical issue and as such it should not be re-opened for a non-critical follow-up. I'd suggest to open a separate issue for the sorting problem and switch this back to fixed, even if it's just matter of fixing a test view.

mradcliffe’s picture

I'll open up a new issue again this time, but I want to mention that this patch DID break HEAD, and we should get used to the idea that testing needs to occur first and patches should get reverted in the future.

plach’s picture

Do you mean we should ensure a patch introduces no regression before committing it? That would mean we commit no patch at all. This adds tons of test coverage and went through hours of manual testing, so please don't assume this went in lightly.

Fabianx’s picture

#287: I think that is only realistic once the new TestBotNG bots are there and we have easy ways to test Postgres / SQlite etc..

Thanks for your efforts there!

But even then core committers might have deemed getting this issue in (and unblocking other release blocking things) more critical then introducing a temporary regression that might have been a pre-existing bug no one had noticed before.

So please understand that without the necessary infrastructure to test Postgres automatically it is very hard to support that.

Wim Leers’s picture

Indeed, without Postgres bots, that's not realistic. We can't expect that everybody tests every patch on MySQL, PostgresQL and SQLite.

dawehner’s picture

Yeah I agree this patch could have been tested against PGSQL, especially because now its pretty straightforward to do so, I think, with drupalci, but still,
pgsql will never be a first class sitizen as core developer will most of the time not run them locally.

I hope we will have at least maybe a testbot which tests core after each commit.

Wim Leers’s picture

I hope we will have at least maybe a testbot which tests core after each commit.

+1

webchick’s picture

Status: Fixed » Closed (fixed)

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