Problem/Motivation

Currently we load every theme negotiator for every request. This leads to initialization of potentially obscure services, like batch and what not

Proposed resolution

  • Convert \Drupal\Core\Theme\ThemeNegotiatorInterface::applies to accept a route object
  • Store for each route, which theme negotiator apply on route rebuild time.
  • Use that information on runtime to not initialize all services

Remaining tasks

User interface changes

API changes

Comments

wim leers’s picture

mr.baileys’s picture

Assigned: Unassigned » mr.baileys
mr.baileys’s picture

Status: Active » Needs review
StatusFileSize
new17.61 KB

First attempt, everything seems to work when testing manually, but the tests still need to be converted so will fail. Uploading the patch to get feedback on approach. Probably needs some polishing.

Initial numbers on performance impact:
=== 8.0.x..2471657 compared (552fb1c82db64..552fb30e6fe9a):
ct : 53,018|52,755|-263|-0.5%
wt : 185,132|183,287|-1,845|-1.0%
mu : 18,538,776|18,527,664|-11,112|-0.1%
pmu : 18,698,432|18,687,480|-10,952|-0.1%
---
ct = function calls, wt = wall time, cpu = cpu time used, mu = memory usage, pmu = peak memory usage

  • Added a container compiler pass: RegisterThemeNegotiators, which finds all services tagged 'theme_negotiator' and ensures these are injected in the ThemeNegotiationSubscriber.
  • ThemeNegotiationSubscriber alters routes when routes are rebuilt, adding an '_theme_negotiators'-option containing the prioritized theme negotiator service names that apply for that route.
  • ThemeNegotiator now only evaluates the theme negotiators that are stored on the route.
wim leers’s picture

Assigned: mr.baileys » dawehner

Great job! Now to @dawehner for a review.

dawehner’s picture

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

It is looking great in general!

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterThemeNegotiators.php
    @@ -0,0 +1,38 @@
    +      // @TODO: make better yes?
    

    Is that a c&p?

  2. +++ b/core/lib/Drupal/Core/EventSubscriber/ThemeNegotiatorSubscriber.php
    @@ -0,0 +1,62 @@
    +        $route_match = new RouteMatch($route_name, $route);
    ...
    +        if ($theme_negotiator->applies($route_match, $route)) {
    
    +++ b/core/lib/Drupal/Core/Theme/DefaultNegotiator.php
    @@ -35,7 +36,7 @@ public function __construct(ConfigFactoryInterface $config_factory) {
    -  public function applies(RouteMatchInterface $route_match) {
    +  public function applies(RouteMatchInterface $route_match, Route $route) {
         return TRUE;
    

    I think its wrong to use route match + route, but instead we should go back to route name + route

  3. +++ b/core/lib/Drupal/Core/Theme/ThemeNegotiator.php
    @@ -98,8 +69,10 @@ public function applies(RouteMatchInterface $route_match) {
    +      if ($negotiator->applies($route_match, $route)) {
    

    Now that we are doing that on route compile time, we no longer have to check it on runtime. Let's get rid of the applies() call.

The last submitted patch, 3: store_theme_negotiators_on_route-2471657-3.patch, failed testing.

mr.baileys’s picture

Status: Needs work » Needs review
mr.baileys’s picture

Assigned: Unassigned » mr.baileys
Status: Needs review » Needs work
mr.baileys’s picture

Status: Needs work » Needs review
StatusFileSize
new17.98 KB
new8.55 KB
  1. Removed the @TODO. During registration of the available theme negotiators, only the first 'theme_negotiator'-tag and its priority is taken into account. I don't see any reason to have multiple theme_negotiator tags with differing priorities on one service, so I think this is fine.
  2. Done.
  3. Done.

Tests still need to be converted. Since we transformed from having all logic in the ThemeNegotiators to splitting the logic up in three different locations ((a) container compiling, (b) route rebuilding and (c) theme negotiator) it might make sense to convert the current Theme Negotiator test from a Unit test to a Web Test?

New performance statistics:
=== 8.0.x..2471657 compared (5530c03d7e613..5530c300bd097):

ct : 53,018|52,752|-266|-0.5%
wt : 185,591|184,057|-1,534|-0.8%
mu : 18,537,112|18,525,848|-11,264|-0.1%
pmu : 18,696,824|18,685,744|-11,080|-0.1%

Status: Needs review » Needs work

The last submitted patch, 9: store_theme_negotiators_on_route-2471657-9.patch, failed testing.

mr.baileys’s picture

Assigned: mr.baileys » Unassigned
Status: Needs work » Needs review
StatusFileSize
new21.64 KB
new12.19 KB

Unassigning since converting the tests from Unit tests to KernelTestBase has proven a bit too difficult for me...

Status: Needs review » Needs work

The last submitted patch, 11: store_theme_negotiators_on_route-2471657-11.patch, failed testing.

mr.baileys’s picture

I started out converting the ThemeNegotiatorTest from a UnitTest to a KernelTest. However, AFAICT, all test cases covered currently by ThemeNegotiatorTest are already covered by existing WebTestCases except for theme access checking.

\Drupal\Tests\Core\Theme\ThemeNegotiatorTest ... is covered by
::testDetermineActiveTheme There are numerous tests that verify whether the correct theme is determined for a given page, so this seems sufficiently covered by existing tests. For example \Drupal\block\Tests\BlockTest::testBlockThemeSelector
::testDetermineActiveThemeWithPriority \Drupal\system\Tests\Theme\ThemeTest::testNegotiatorPriorities
::testDetermineActiveThemeWithNotApplyingNegotiator Implicitly covered by numerous theme-related tests, since when specificly testing for active theme, themes that do not apply are correctly disregarded.

The only thing not already explicitly covered is the theme access checking done in determineActiveTheme in the ThemeNegotiator, so I added an additional test case for that to ThemeTest.

mr.baileys’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 13: store_theme_negotiators_on_route-2471657-13.patch, failed testing.

wim leers’s picture

Sounds good, great research!

fabianx’s picture

Patch looks great already, just 2 test fails.

I like the approach of setting the theme negotiators in a onRouteAlter() and checking the applies() part.

That is really good thinking.

wim leers’s picture

1000 auth requests ($ ab -n 1000 -c 1 -g ab.tsv -C SESS3d5f1d095f03cbeab065f280e5c42801=mK-rDsC8XPuc1zGUOsKOFDa5s8PvRVVB89XlO99pABk http://tres/)

Before
Requests per second:    12.66 [#/sec] (mean)
Time per request:       78.984 [ms] (mean)
Time per request:       78.984 [ms] (mean, across all concurrent requests)
Transfer rate:          163.45 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       1
Processing:    75   79   1.7     79      89
Waiting:       66   70   1.6     70      80
Total:         75   79   1.7     79      89
After
Requests per second:    12.74 [#/sec] (mean)
Time per request:       78.471 [ms] (mean)
Time per request:       78.471 [ms] (mean, across all concurrent requests)
Transfer rate:          164.52 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:    74   78   2.0     78      95
Waiting:       66   70   1.9     69      84
Total:         74   78   2.0     78      95
Conclusion
Approximately a half a millisecond improvement, but tough to measure. So, I did profiling to find the function call/mem delta:
Number of Function Calls 40,660 40,531 -129 -0.3%
Incl. MemUse (bytes) 18,901,128 18,851,512 -49,616 -0.3%
Incl. PeakMemUse (bytes) 19,057,872 19,007,832 -50,040 -0.3%

(Reusing data from https://groups.drupal.org/node/464283#comment-1100578.)

mr.baileys’s picture

Assigned: Unassigned » mr.baileys
Status: Needs work » Needs review
StatusFileSize
new28.32 KB
new1.61 KB

The 2 remaining test failures were due to incorrect theme negotiation logic on route '<none>'. Tests should now pass.

fabianx’s picture

  1. +++ b/core/lib/Drupal/Core/EventSubscriber/ThemeNegotiatorSubscriber.php
    @@ -0,0 +1,65 @@
    +    foreach ($event->getRouteCollection() as $route_name => $route) {
    +      $route_negotiators = [];
    +      foreach ($all_negotiators as $service_id) {
    +        /** @var \Drupal\Core\Theme\ThemeNegotiator $theme_negotiator */
    +        $theme_negotiator = $this->container->get($service_id);
    +        if ($theme_negotiator->applies($route_name, $route)) {
    +          $route_negotiators[] = $service_id;
    +        }
    +      }
    

    So this runs all theme negotiators for all routes?

    How often is this done?

    Is there some kind of caching for the routes when the router is rebuild?

    How long does that take in cold cache?

  2. +++ b/core/lib/Drupal/Core/Theme/ThemeNegotiator.php
    @@ -98,8 +66,16 @@ public function applies(RouteMatchInterface $route_match) {
    +      // theme negotiators.
    +      if ($route_theme_negotiator_ids || $negotiator->applies($route_match->getRouteName(), $route)) {
    

    This if looks wrong ...

    Why would we apply the determineActiveTheme if negotiators are stored on the route?

    What if there are several?

    I see this tries to optimize for the case that there is only one in which case we know it is matching, but for that it should check for count($r_t_n_ids) == 1.

mr.baileys’s picture

Thanks for the review Fabianx

Regarding #1:

  • This indeed runs all theme negotiators for all routes. Note that theme negotiation logic is fairly simple in and of itself, although I assume that on a site with a large number of routes and a large number of theme negotiators, this can result in a significant number of function calls.
  • This theme negotiator evaluation happens every time routes are rebuilt. This is in essence the change from the current situation, where all theme negotiators are instantiated and evaluated per request.
  • I don't know if there is any caching when routes are rebuilt...

I'm happy to profile impact of this patch on cold cache requests if someone can tell me how to properly test this (I have xprof and xhprof-kit already up-and-running, just need to know how this is usually tested/benchmarked for D8 core).

Regarding #2:
There are two possible scenarios when determineActiveTheme() is called: either (a) the theme negotiators that apply to the route have been stored on the route in \Drupal\Core\EventSubscriber\ThemeNegotiatorSubscriber::onRouteAlter(), in which case the service ids stored on the route are used, or (b) there are no theme negotiators stored on the route, in which case we need to evaluate the full set of available theme negotiators.

In scenario a), we just need to cycle through the services stored on the route until the determineActiveTheme for that negotiator returns a theme. However, in scenario b), we need to first test if the negotiator applies to the route by calling applies() (contrary to the route-stored negotiators, where this step happened earlier). Only if the negotiator applies, we should call determineActiveTheme().

Hence if ($route_theme_negotiator_ids || $negotiator->applies($route_match->getRouteName(), $route)) means "determine the active theme for this negotiator, but if it was not stored on the route, make sure it applies first".

fabianx’s picture

Status: Needs review » Needs work

#21

1: Thanks for explanation, so we should maybe add a threshold on the route collection OR just rely on sites to remove the subscriber from services if number of routes is too high?

2. Thanks, again makes perfect sense now. I missed that they are sorted ...

-------------------------

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterThemeNegotiators.php
    @@ -0,0 +1,33 @@
    +    foreach ($container->findTaggedServiceIds('theme_negotiator') as $id => $attributes) {
    +      $service_ids[$id] = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
    +    }
    +
    +    $container
    +      ->getDefinition('theme.negotiator')
    +      ->addArgument($service_ids);
    

    Who sorts this negotiators now?

    I think this is missing now ...

    --

    Do we use addArgument elsewhere?

    It seems way underdocumented as you cannot see there is another parameter from the core.services.yml.

    I think its more logical to have a dedicated setNegotiatorServiceIds() function ...

    But I might be wrong.

    Probably just a arsort thats needed, then using array_keys ...

    EDIT: Found it, see below ...

  2. +++ b/core/lib/Drupal/Core/Theme/AjaxBasePageNegotiator.php
    @@ -71,9 +72,9 @@ public function __construct(CsrfTokenGenerator $token_generator, ConfigFactoryIn
    -  public function applies(RouteMatchInterface $route_match) {
    +  public function applies($route_name = '', Route $route = NULL) {
    

    Not sure we can do this API change this late, but need to at least document the API change.

  3. +++ b/core/lib/Drupal/Core/Theme/ThemeNegotiator.php
    @@ -49,48 +45,20 @@ class ThemeNegotiator implements ThemeNegotiatorInterface {
    +    arsort($negotiator_service_ids);
    +    $this->negotiator_service_ids = array_keys($negotiator_service_ids);
    

    Ahhh, here we are doing the sorting, okay ...

    That makes sense.

  4. +++ b/core/lib/Drupal/Core/Theme/ThemeNegotiator.php
    @@ -98,8 +66,16 @@ public function applies(RouteMatchInterface $route_match) {
    +    $route = $route_match->getRouteObject() ?: NULL;
    +    $route_theme_negotiator_ids = ($route && $route->getOption('_theme_negotiators')) ? $route->getOption('_theme_negotiators') : array();
    

    So in case someone removes the subscriber this is exactly the old behavior?

    Nice!

  5. +++ /dev/null
    @@ -1,188 +0,0 @@
    - * @file
    - * Contains \Drupal\Core\Theme\ThemeNegotiatorTest.
    - */
    

    Lets keep that fully, this can still test the case when no negotiators are set at all on routes (as I have too many on my site and don't want the optimization).

joelpittet’s picture

@mr.baileys are @Fabianx's notes doable and is this doable for RC or should we postpone till 8.1.x?

wim leers’s picture

I don't see this happening now (between RC4 and 8.0.0). But maybe it can be done in 8.0.1? If there's no API change whatsoever, it's possible.

dawehner’s picture

Yeah, especially if with contrib it might be actually a performance issue, we should reconsider doing that.

On the other hand, it all comes down to

a) What we define as feature
b) How strict we want to be with semv

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

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

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

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

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

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

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

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

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

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

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

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

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

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

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

Drupal 8.6.x will not receive any further development aside from security fixes. Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

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

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

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

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

Drupal 8 is end-of-life as of November 17, 2021. There will not be further changes made to Drupal 8. Bugfixes are now made to the 9.3.x and higher branches only. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

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

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

Drupal 9.3.15 was released on June 1st, 2022 and is the final full bugfix release for the Drupal 9.3.x series. Drupal 9.3.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.4.x-dev branch from now on, and new development or disruptive changes should be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.9 was released on December 7, 2022 and is the final full bugfix release for the Drupal 9.4.x series. Drupal 9.4.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.5.x-dev branch from now on, and new development or disruptive changes should be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.