Problem/Motivation

In #2865616-49: Add an option for EntityConverter to load the latest entity revision and fix all entity forms to use this option. and following we discovered that the rendering of HEAD links can be very expensive, depending on the amount of links a node defines.

Proposed resolution

Remove the adding of every relationship (delete-form, edit-form etc.) which is done by both node and taxonomy modules in different ways. These are expensive to access check, and we don't even have theoretical use cases for them, let alone real life examples. They also add additional cache contexts to node renders, potentially reducing cache hit rate.

There is no way to verify that no-one uses them, except for the complete lack of use cases for doing so. However the only thing that would use them would be some kind of external tool (like a scraper) rather than Drupal code.

In place of this, generically add shortlink and canonical links for every entity type. This will maintain the useful links for node and taxonomy, and also add them to other entity types.

Remaining tasks

Perform more benchmarks once the following issues are in:

  • Validate the proposed solution
  • Write a patch
  • Reviews

User interface changes

TBD

API changes

None foreseen

Data model changes

None foreseen

Release note snippet

Drupal-specific <link> tags have been removed from node and taxonomy term pages to improve performance. There are no known uses of these tags in the wild.

<link rel="canonical"> and <link rel="shortlink"> tags remain, and have been extended to cover all entity types.

CommentFileSizeAuthor
#91 2922570-90.9.2.5.patch17.69 KBTomefa
#90 2922570-90.9.2.4.patch17.72 KBdww
#89 2922570-89.9.2.x.patch18.27 KBdww
#88 D91x-2922570-88.patch17.54 KBjoseph.olstad
#80 2922570-80.patch17.66 KBpaulocs
#80 interdiff-69-80.txt6.58 KBpaulocs
#77 blackfire_diff_relationships.png62.04 KBBerdir
#69 2922570-69.patch17.54 KBtobiasb
#64 2922570-64.8_9_x.patch17.6 KBdww
#61 2922570-61.patch17.47 KBpaulocs
#61 interdiff-57-61.txt788 bytespaulocs
#57 2922570-57.patch16.91 KBravi.shankar
#31 interdiff_27_31.txt3.2 KBanmolgoyal74
#31 remove-generic-nodeterm-link-relationships-2922570-31.patch17.53 KBanmolgoyal74
#27 remove-generic-nodeterm-link-relationships-2922570-27.patch17.35 KBBerdir
#26 remove-generic-nodeterm-link-relationships-2922570-26-interdiff.txt5.75 KBBerdir
#26 remove-generic-nodeterm-link-relationships-2922570-26.patch17.5 KBBerdir
#24 remove-generic-nodeterm-link-relationships-2922570-22-interdiff.txt8.02 KBBerdir
#24 remove-generic-nodeterm-link-relationships-2922570-22.patch11.74 KBBerdir
#20 remove-generic-nodeterm-link-relationships-2922570-20-interdiff.txt1.01 KBBerdir
#20 remove-generic-nodeterm-link-relationships-2922570-20.patch4.94 KBBerdir
#19 remove-generic-nodeterm-link-relationships-2922570-19.patch4.82 KBBerdir
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

plach created an issue. See original summary.

dawehner’s picture

Reading through #1970360: Entities should define URI templates and standard links it was never discussed that this might become a performance issue.

I am also wondering whether we could make this somehow an opt IN feature rather than an opt out feature.

plach’s picture

After researching this a bit more, I realized that probably the proposed solution is not viable. AAMOF the access checks for many node routes, including the edit form, are performed anyway while generating local tasks. In case of an anonymous user, these access checks are performed twice.

This is probably not hugely problematic in the current HEAD, because these checks are run only with cold caches and a big part of the effort is loading the entity, which is then statically cached. So performing those checks multiple times is not making a big difference.

OTOH #2620980: ContentEntityStorageBase::loadRevision() should use the static and the persistent entity cache like ContentEntityStorageBase::load() and #2784201: Track the latest_revision and latest_revision_translation_affected ID in the entity data table for revisionable entity types are likely going to bring us in a similar scenario while loading the latest revision, but I don't think we can skip these checks altogether.

We could explore statically caching access checks and entity conversion so we perform those just once, but I don't think it would imply a huge gain once those two issues are fixed.

dawehner’s picture

AAMOF the access checks for many node routes, including the edit form, are performed anyway while generating local tasks.

I personally haven't seen those calls, but maybe I was blind, or render caching got applied to those blocks.

plach’s picture

Likely render cache, I have it disabled in my local env.

catch’s picture

Does render caching get applied to the HEAD links too? I think it does now so this might be moot after all. I left a comment on the main issue which would help the specific case there though.

dawehner’s picture

Does render caching get applied to the HEAD links too?

These links are generated on the controller level, which are only cached by the page level caching (page cache vs. dynamic page cache).

plach’s picture

Title: The node view page triggers a lot of expensive access checks for relationship links » [PP-3] The node view page triggers a lot of expensive access checks for relationship links
Issue summary: View changes
Status: Active » Postponed
dawehner’s picture

Fair point, let's hope the best as always.

Wim Leers’s picture

plach’s picture

Title: [PP-3] The node view page triggers a lot of expensive access checks for relationship links » [PP-2] The node view page triggers a lot of expensive access checks for relationship links

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

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

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

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

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

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

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

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

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

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

Berdir’s picture

Title: [PP-2] The node view page triggers a lot of expensive access checks for relationship links » The node view page triggers a lot of expensive access checks for relationship links
Status: Postponed » Needs review
FileSize
4.82 KB

So here's a pretty aggressive proposal to resolve this. From the original issue that added this:

> We can do the same in HTML
elements. Hypermedia FTW, again. (What could we do with that information client-side? The mind boggles!)

Yeah, my mind is indeed still boggled about it. As in, I don't understand the use case, to be honest. I get it for REST/JSON-API, fair enough, as these are then standardized protocols that can be more or less understood by generic clients. (And also fair point that for a decoupled site that actively uses those endpoints instead of the HTML format, this won't actually help, I get that too). But some random HTML forms, I can't imagine what the benefit there is.

If someone has a use case, IMHO a contrib module relatively easy provide this as well for all entity types with some trivial checks on the route name. Like content_translation_page_attachments() already does for translations and taxonomy does it in a hook too: taxonomy_page_attachments_alter().

Beside this performance issue, there are also #2856823: delete-form and edit-form links in header for anonymous users and #2821635: edit-form, delete-form etc. <link> tags added on /taxonomy/term/{taxonomy_term} are invalid according to W3C Validator (duplicate) to add access checks to terms as well. And [#2782283 ] about making it more generic.

So I'd propose to just keep the canonical and shortlink as hardcoded special cases but do those for all entity types*. We might also want to move content_translation_page_attachments() into a default implementation, as everything it depends on is actually generic entity API concepts, content_translation is just about the backend UI. We would need access checks for the translations but not the current language IMHO, because we know that you can access it.

To be defined: BC? Opt-in? Opt-out? This first patch is just to show how it could look like.

Berdir’s picture

I guess that would break on node preview, this should fix that. Although preview of an already saved entity would still add those, but we could unset them in the node preview controller if we care.

The last submitted patch, 19: remove-generic-nodeterm-link-relationships-2922570-19.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Status: Needs review » Needs work

The last submitted patch, 20: remove-generic-nodeterm-link-relationships-2922570-20.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

catch’s picture

To me this seems like 'data structure' - so could go into a minor release without a bc layer. Would obviously be possible to do something with Settings for opt-in/opt-out though if we can think of something the change would actually break.

Berdir’s picture

Had the wrong variable name, so that actually didn't do much yet. This should be better. Also fixed a bunch of tests, but adding url.site to more responses (had to make the URL absolute so we do need that cache context unfortunately, no way around it) might fail some additional ones.

Also fixed a bug in \Drupal\Tests\node\Functional\NodeViewTest::testLinkHeader(), it passed the alias = TRUE option to the wrong method. Was wondering about adding an alias to test that explicitly, but skipping that for now as it's also tested in some other places like \Drupal\Tests\path\Functional\PathAliasTest::testNodeAlias().

Status: Needs review » Needs work

The last submitted patch, 24: remove-generic-nodeterm-link-relationships-2922570-22.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Berdir’s picture

Some more test fixes around cache contexts. We could still split up the part that's making it the default from simplifying node/term, would require fewer test changes.

Berdir’s picture

Reroll for 9.1.x, conflict on setUp() return type changes.

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

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

catch’s picture

Status: Needs review » Needs work
+++ b/core/modules/node/src/Controller/NodeViewController.php
@@ -63,51 +63,7 @@ public static function create(ContainerInterface $container) {
-
-    return $build;
+    return parent::view($node, $view_mode);
   }

This isn't passing $langcode. Also do we need the method at all now?

Otherwise looks great. I'd prefer to make this change than #2856823: delete-form and edit-form links in header for anonymous users.

Berdir’s picture

Thanks for the review.

> This isn't passing $langcode. Also do we need the method at all now?

The parent method doesn't have a $langcode parameter, that was always bogus or at least for a very long time.

Not sure if I remember why I left the method, would have to look it up. BC maybe? I think there was some reason.

anmolgoyal74’s picture

Fixed the minor CS failure.
removed the unused use statement.

catch’s picture

Can't see why we'd need bc for the method, even if something passed $langcode or extended the controller it shouldn't make a difference.

catch’s picture

Issue summary: View changes
catch’s picture

Status: Needs review » Reviewed & tested by the community

Since my question was answered and the coding standards issue has been fixed, moving this to RTBC.

Adding issue credit from #2856823: delete-form and edit-form links in header for anonymous users which I'm about to mark as duplicate.

catch credited aamouri.

catch credited alexpott.

catch credited andypost.

catch credited klasseng.

catch credited longwave.

catch credited rpsu.

catch credited tanubansal.

catch’s picture

alexpott’s picture

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

This needs a re-roll unfortunately.

catch credited Dom..

catch credited Spokje.

catch credited anevins.

catch credited dschenk.

catch credited ducktape.

catch credited larowlan.

catch credited marcoliver.

catch’s picture

ravi.shankar’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
16.91 KB

Added reroll of patch #31.

Status: Needs review » Needs work

The last submitted patch, 57: 2922570-57.patch, failed testing. View results

alexpott’s picture

I think we need a release note to document that node html output is changing. Also this should have a change record which details which link relationships are being removed and how to add them back if they are being relied upon.

paulocs’s picture

Assigned: Unassigned » paulocs
paulocs’s picture

Status: Needs work » Needs review
FileSize
788 bytes
17.47 KB

Fixing the tests.

paulocs’s picture

Assigned: paulocs » Unassigned
Status: Needs review » Needs work

Moving back to NW as the Change Record need to be created.

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

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

dww’s picture

Version: 9.3.x-dev » 10.0.x-dev
Issue tags: +Bug Smash Initiative
FileSize
17.6 KB

This all looks promising. Haven't super closely reviewed, yet, but a quick skim looks good.

Sorry for the noise, but I'm going to be running this on a 8.9.x site, and the latest patches don't apply there. So here's a composer-friendly reroll. ;)

dww’s picture

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

Version change wasn't intentional...

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

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

Spokje’s picture

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

Let's see if Version 9.2.x-dev sticks now.

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

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

tobiasb’s picture

Status: Needs work » Needs review
FileSize
17.54 KB

Reroll against latest core changes.

longwave’s picture

+++ b/core/modules/node/src/Controller/NodeViewController.php
@@ -63,51 +63,7 @@ public static function create(ContainerInterface $container) {
+    return parent::view($node, $view_mode);

This method can be deleted entirely, as it just calls the parent? This was also noted in #29-32 but I don't see the point of keeping it.

Berdir’s picture

The variable name is different between the methods, which is relevant for controller methods.

longwave’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs change record, -Needs release note

Oh I see. Worth a followup to deprecate that and unify it somehow?

Added a release note snippet and change notice: https://www.drupal.org/node/3216941

The code looks good to me, so back to RTBC.

longwave’s picture

Issue summary: View changes
joseph.olstad’s picture

great work on this one, I did some XHProfGUI profiling not long ago and confirm that we do need the performance enhancement. Has anyone done any profiling work on the latest patch yet? just wondering.

I recently used XHProfGUI and would recommend it but it did take some effort to set up. https://github.com/perftools/xhgui

It would be nice to know how much of a performance gain this patch will bring (approximately).

Berdir’s picture

I can try, but there's likely not a simple answer for this, as it depends a lot on what kind of node access checks you have. If you have node grants, are using modules like group/og or have expensive access logic in custom hooks then the benefit is going to be much bigger than just with core.

There might also be fewer cache variations and cache redirects in some cases, which is not something you can quantify easily with profiling of single requests.

joseph.olstad’s picture

Ya for profiling access checks here make sure to use a non-administrative role as the admin user account bypasses all these checks so to test access check improvements you'd want to use a different role/account.

Berdir’s picture

Yes, not just non-adminstrative but explicitly as anon user, because the old logic only checked access for anonymous users.

Did some testing. Disabled page and dynamic page cache to simulate uncached pages for anonymous users, created a very simple page node.

ab -n 100 -c 1 http://d8/node/2 without blackfire and xdebug extensions:
HEAD:
Requests per second:    39.40 [#/sec] (mean)
Time per request:       25.380 [ms] (mean)

Requests per second:    38.27 [#/sec] (mean)
Time per request:       26.132 [ms] (mean)

Patch:

Time per request:       24.392 [ms] (mean)
Time per request:       24.392 [ms] (mean, across all concurrent requests)

Requests per second:    41.22 [#/sec] (mean)
Time per request:       24.258 [ms] (mean)

Requests per second:    41.33 [#/sec] (mean)
Time per request:       24.198 [ms] (mean)

Actually surprised to see a bigger improvement as I expected here. Comparing also with blackfire and seeing a considerable improvement there as well (as always, take with a grain of salt as blackfire tends to considerable increase the cost of function calls. Response time there is 50% higher than with ab). Url/Route access checking really is expensive, especially of entity routes. We need to load the route, do upcasting and all kinds of other things beside just checking access.

So yes, lets do this :)

Considering the performance gains and amount of issues that were closed as duplicate, I'd even say this could be set to major.

I also added a code snippet and references to the change record if someone wants to have those relationships back.

larowlan’s picture

Status: Reviewed & tested by the community » Needs work

Thanks for working on this folks, will be great to see this solved once and for all.
Got a couple of questions.

  1. +++ b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
    @@ -101,6 +101,32 @@ public function view(EntityInterface $_entity, $view_mode = 'full') {
    +      // Set the non-aliased canonical path as a default shortlink.
    ...
    +          'href' => $url->setOption('alias', TRUE)->toString(),
    

    Here we say 'non-aliased' but then we use set alias to TRUE? Wouldn't non-aliased use FALSE?

    I've searched the code in UrlGenerator and I don't see any mention of 'alias'

    I was expecting this to use 'path_processing'

  2. +++ b/core/modules/comment/tests/src/Functional/CommentCacheTagsTest.php
    @@ -158,4 +159,11 @@ protected function getAdditionalCacheTagsForEntity(EntityInterface $entity) {
    +    return ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'];
    
    +++ b/core/modules/content_translation/tests/src/Functional/ContentTestTranslationUITest.php
    @@ -14,6 +14,11 @@ class ContentTestTranslationUITest extends ContentTranslationUITestBase {
    +  protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'url.query_args:_wrapper_format', 'user.permissions', 'url.site'];
    

    nit: as arrays > 80 chars these should be wrapped

  3. +++ b/core/modules/node/src/Controller/NodeViewController.php
    @@ -63,51 +63,7 @@ public static function create(ContainerInterface $container) {
    +    return parent::view($node, $view_mode);
    

    now this defers to the parent, is there any need to keep it?

  4. +++ b/core/modules/node/tests/src/Functional/NodeViewTest.php
    @@ -25,50 +25,11 @@ public function testHtmlHeadLinks() {
    +    $element = $this->assertSession()->elementExists('css', 'link[rel="shortlink"]');
    +    $this->assertEquals($node->toUrl('canonical', ['alias' => TRUE])->setAbsolute()->toString(), $element->getAttribute('href'));
    

    If 'alias' => TRUE is indeed not supported, then this shows the test is too close to the implementation, we should instead be compiling the expected uri manually, e.g. 'node/' . $node->id()

  5. +++ b/core/modules/node/tests/src/Functional/NodeViewTest.php
    @@ -79,8 +40,7 @@ public function testLinkHeader() {
    -      '<' . Html::escape($node->toUrl('canonical')->setAbsolute()->toString(), ['alias' => TRUE]) . '>; rel="shortlink"',
    

    hmm but we're using alias TRUE in HEAD

    I wonder if that's broken too 🤔

Berdir’s picture

Thanks for the review.

1./5. The alias option is weird, but correctly use. Easy to verify, just look at an article that does have an alias and you can see that shortlink is node/N. It kinda means "this url is already an alias, do not look for an alias again". Super weird, but has been like this for as long as I can remember. The option is in \Drupal\path_alias\PathProcessor\AliasPathProcessor. Disabling path processing entirely is not the same thing and not what we want, that would also not add the language prefix then for example.

3. See #70/#71.

4. I get the point, but it's an absolute link, so we would also need to replicate adding the domain/path to it and so on. This method apparently didn't test shortlink before, but there is an existing test just below that for this. And all existing tests also used toUrl().

Leaving at needs work for #2.

paulocs’s picture

Status: Needs work » Needs review
FileSize
6.58 KB
17.66 KB

Here is patch that addresses #78.2
I also changed protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'url.query_args:_wrapper_format', 'user.permissions', 'url.site']; in UserTranslationUITest.php
and
protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'url.query_args:_wrapper_format', 'user.permissions', 'url.site']; in TermTranslationUITest.php

longwave’s picture

Status: Needs review » Reviewed & tested by the community

All points in #78 answered or fixed, back to RTBC.

  • catch committed bfe59b2 on 9.3.x
    Issue #2922570 by Berdir, paulocs, anmolgoyal74, dww, ravi.shankar,...
catch’s picture

Status: Reviewed & tested by the community » Fixed

1./5. The alias option is weird, but correctly use. Easy to verify, just look at an article that does have an alias and you can see that shortlink is node/N. It kinda means "this url is already an alias, do not look for an alias again". Super weird, but has been like this for as long as I can remember. The option is in \Drupal\path_alias\PathProcessor\AliasPathProcessor. Disabling path processing entirely is not the same thing and not what we want, that would also not add the language prefix then for example.

Yeah I think this dates back about 12-15 years or so, must be barely used any more except in this issue. The 'alias' option used to mean "this is an alias, don't try to look up an alias for it again", in this issue it's being used purely to disable alias look-up. So I think we should open a follow-up to rename the option to something a bit more semantic - have done so here: #3218088: Rename the 'alias' option for URLs.

Nice to see how much this cleans things up too.

Committed bfe59b2 and pushed to 9.3.x. Thanks!

longwave’s picture

Issue tags: +9.3.0 release notes

I will leave it up to others to decide if this is a release highlight or not.

catch’s picture

I think it might be, adding the tag.

larowlan’s picture

thanks @Berdir, I see the code now.

again this slide proves true http://larowlan.github.io/ds.core-til/#/9/2

glad to see this fixed

Status: Fixed » Closed (fixed)

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

joseph.olstad’s picture

FileSize
17.54 KB

Too bad it missed 9.2.0

we need speed and are impatient so I'm rerolling backports

Reroll for D91x and also applies cleanly for HEAD of D92x (same patch for both)

dww’s picture

FileSize
18.27 KB

#88 no longer applies to 9.2.x branch. Here's another backport for composer folks.

dww’s picture

FileSize
17.72 KB

Sorry for the noise, but #89 only applies to 9.2.x branch. If you've got 9.2.4 official, it doesn't apply due to #3139404: Replace usages of AssertLegacyTrait::assertText, that is deprecated. So here's one that should apply to 9.2.4 itself...

Tomefa’s picture

FileSize
17.69 KB

Patch in #90 wasn't working for me on drupal 9.2.5, here the reroll for Drupal core 9.2.5

Zsuffa Dávid’s picture

Patch #91 works for me on drupal 9.2.6. thx.

gilles_webstanz’s picture

Hello,

I tested the Patch #91 on drupal 9.2.5 and it works.

Thank you !