Problem/Motivation

In EntityViewController::view() we have the following:

    // If the entity's label is rendered using a field formatter, set the
    // rendered title field formatter as the page title instead of the default
    // plain text title. This allows attributes set on the field to propagate
    // correctly (e.g. RDFa, in-place editing).
    if ($_entity instanceof FieldableEntityInterface) {
      $label_field = $_entity->getEntityType()->getKey('label');
      if ($label_field && $_entity->getFieldDefinition($label_field)->getDisplayOptions('view')) {
        // We must render the label field, because rendering the entity may be
        // a cache hit, in which case we can't extract the rendered label field
        // from the $page renderable array.
        $build = $this->entityManager->getTranslationFromContext($_entity)
          ->get($label_field)
          ->view($view_mode);
        $page['#title'] = $this->renderer->render($build);
      }
    }

When profiling, I'm seeing this take anything up to 40ms on node/1 (anonymous, logged out, no page cache)

Proposed resolution

Move the setting of #title to a #pre_render callback, so that it gets cached along with the rest of the render array.

Remaining tasks

None.

User interface changes

None.

API changes

Yes - #title won't be set on the render array until it's actually been rendered. Only entity view controllers that override the view method will run into this (see the changes to NodeViewController and NodePreviewController).

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Bug because performance is unnecessarily slow and we're doing double work
Issue priority Critical because this is > 10ms with warm caches, is an improvement for every entity view page, and involves a small API change which at least we'd need to defer until a minor release
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

catch’s picture

Status: Active » Needs review
FileSize
152.36 KB
93.75 KB
3.3 KB

Here's a patch and xhprof screenshots.

I don't get reliable timings on my system for this, but the work we're saving comes up as 8-10% of request time - would be great to confirm that.

See what the bot thinks.

catch’s picture

FileSize
3.89 KB

Small update: render the title field from $build instead of building it all over again. This avoids double-rendering the title on cache misses too.

Berdir’s picture

+++ b/.htaccess
@@ -2,6 +2,11 @@
 
+php_value auto_prepend_file "/Users/catch/xhprof/header.php"
+php_value auto_append_file "/Users/catch/xhprof/footer.php"
+
+
+
 # Protect files and directories from prying eyes.
 <FilesMatch "\.(engine|inc|install|make|module|profile|po|sh|.*sql|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\..*|Entries.*|Repository|Root|Tag|Template)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig\.save)$">
   <IfModule mod_authz_core.c>
@@ -39,9 +44,6 @@ AddEncoding gzip svgz

@@ -39,9 +44,6 @@ AddEncoding gzip svgz
   php_value mbstring.http_input             pass
   php_value mbstring.http_output            pass
   php_flag mbstring.encoding_translation    off
-  # PHP 5.6 has deprecated $HTTP_RAW_POST_DATA and produces warnings if this is
-  # not set.
-  php_value always_populate_raw_post_data   -1
 </IfModule>

left-over? no idea why you had to make the php_value change, maybe an old file that you copied in? :)

catch’s picture

FileSize
2.96 KB

Whoops...

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
    @@ -57,6 +57,21 @@ public static function create(ContainerInterface $container) {
    +      $page['#title'] = $this->renderer->render($page[$label_field]);
    

    I'm curious, does this works for view displays, which don't have their label field configured as to be shown?

  2. +++ b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
    @@ -78,22 +93,9 @@ public function view(EntityInterface $_entity, $view_mode = 'full', $langcode =
    -    // If the entity's label is rendered using a field formatter, set the
    -    // rendered title field formatter as the page title instead of the default
    -    // plain text title. This allows attributes set on the field to propagate
    -    // correctly (e.g. RDFa, in-place editing).
    

    Let's ensure we don't loose the documentation here ... I think its not obvious why you can't just use $entity->label() and be done

catch’s picture

Priority: Major » Critical
FileSize
3.24 KB

#1 - in HEAD when we render the field separately, we use the same view mode being requested - so it ought to be the same, but could use a check.

If it's broke for some reason we could go back to the initial patch on here, but would be nice to keep the cold cache optimisation if we can.

#2 - added the docs back (no other changes in the patch).

This is more than 10ms improvement with a warm cache on our most commonly requested pages, and it's an albeit small API change for EntityViewController::view() implementors/callers (#title not being set since it's in the pre_render now). So bumping to critical.

dawehner’s picture

If it's broke for some reason we could go back to the initial patch on here, but would be nice to keep the cold cache optimisation if we can.

So the node title is configured as be possible. What about checking for isset() and otherwise fallback to $entity->label() directly?

    $fields['title'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Title'))
      ->setRequired(TRUE)
      ->setTranslatable(TRUE)
      ->setRevisionable(TRUE)
      ->setDefaultValue('')
      ->setSetting('max_length', 255)
      ->setDisplayOptions('view', array(
        'label' => 'hidden',
        'type' => 'string',
        'weight' => -5,
      ))
+++ b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
@@ -57,6 +57,24 @@ public static function create(ContainerInterface $container) {
+    $view_mode = $page['#view_mode'];

$view_mode is not used.

catch’s picture

The title callback already does $entity->label() so we're fine here if #title never gets set. isset() check is reasonable though.

$view_mode is cruft - will remove on next update.

The last submitted patch, 1: 2498849.patch, failed testing.

The last submitted patch, 2: 2498849.patch, failed testing.

The last submitted patch, 4: 2498849.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 6: 2498849.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
7.57 KB
2.84 KB

Trying to fix things.

I removed EntityViewControllerTest, because all what its doing is to setup a bunch of mocks, IMHO there is no logic tested in there.

Wim Leers’s picture

Wow, amazing find!

Wim Leers’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
    @@ -57,6 +57,31 @@ public static function create(ContainerInterface $container) {
    +   *   A render array.
    

    A *page* render array.

  2. +++ b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
    @@ -78,22 +103,9 @@ public function view(EntityInterface $_entity, $view_mode = 'full', $langcode =
    +    $page['#pre_render'][] = array($this, 'buildTitle');
    

    Let's use the [] syntax?

  3. +++ b/core/modules/node/src/Controller/NodePreviewController.php
    @@ -55,6 +54,23 @@ public function view(EntityInterface $node_preview, $view_mode_id = 'full', $lan
    +    $build['nodes'] = call_user_func($build['nodes']['#pre_render'][0], $build['nodes']);
    +    $build['nodes'] = call_user_func($build['nodes']['#pre_render'][1], $build['nodes']);
    

    Besides being very weird… (is there really no other way?) this will fail to execute any #pre_render callbacks beyond the first two.

plach’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
    @@ -58,18 +58,25 @@ public static function create(ContainerInterface $container) {
    -      $page['#title'] = $this->renderer->render($page[$label_field]);
    +      if (isset($page[$label_field]) && $title = $this->renderer->render($page[$label_field])) {
    +        $page['#title'] = $title;
    +      }
    

    Won't this prevent us from having an empty title? What if $page[$label_field]['#markup'] was explicitly set to the empty string?

  2. +++ b/core/modules/node/src/Controller/NodePreviewController.php
    @@ -55,6 +54,23 @@ public function view(EntityInterface $node_preview, $view_mode_id = 'full', $lan
    +    $build['nodes'] = call_user_func($build['nodes']['#pre_render'][0], $build['nodes']);
    +    $build['nodes'] = call_user_func($build['nodes']['#pre_render'][1], $build['nodes']);
    

    This does not look reliable at first sight, it could at least use an inline comment.

Edit: oops, missed Wim's review :)

dawehner’s picture

FileSize
7.66 KB
2.58 KB

Won't this prevent us from having an empty title? What if $page[$label_field]['#markup'] was explicitly set to the empty string?

Mh I thought this fixed something, nevermind let's try it different.

This does not look reliable at first sight, it could at least use an inline comment.

Used foreach and added some comment.

catch’s picture

FileSize
0 bytes
1.9 KB

I think the preview fix can be simpler - just skip that extra array level as we do for the main node view controller.

catch’s picture

FileSize
7.17 KB

0 byte patch, classy.

dawehner’s picture

Oh nice fix!

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Here is the RTBC

The last submitted patch, 18: 2498849-18.patch, failed testing.

catch’s picture

Issue summary: View changes
plach’s picture

Issue summary: View changes

RTBC +1

(lol @ #18 failing badly :D)

alexpott’s picture

Issue summary: View changes
Status: Reviewed & tested by the community » Fixed

Committed 4fb37aa and pushed to 8.0.x. Thanks!

Thanks for adding the beta evaluation.

+++ /dev/null
@@ -1,75 +0,0 @@
-class EntityViewControllerTest extends UnitTestCase{

I agree this test does not look useful.

  • alexpott committed 4fb37aa on 8.0.x
    Issue #2498849 by catch, dawehner: Entity view controller title...
tim.plunkett’s picture

Status: Fixed » Closed (fixed)

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