Problem/Motivation

PermissionsHandler:moduleProvidesPermissions costs quite a lot and so should be static cached.

Proposed resolution

Static cache result
Let ::getPermissions accept an optional module name.

Remaining tasks

Patch

User interface changes

None

API changes

New optional argument to PermissionsHandlerInterface::getPermissions - BC is maintained

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Task because performance
Issue priority Normal, but could be major because performance
Prioritized changes The main goal of this issue is performance.
Disruption Non-disruptive as API changes are backwards compatible

Issue fork drupal-2339487

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Berdir’s picture

But it is also not called a lot.

Previously, we had static caching in Simpletest and it was extremely annoying, because when you added things like node types or filters, or something else that dynamically creates permissions, you had to call a weird, magic simpletest method to clear that cache.

It might be a bit better if it's part of the official API, but still there is a reason we do not do static caching at the moment.

dawehner’s picture

Yeah, the only place where this is a problem is AdminController::index() system_get_module_admin_tasks and so also PermissionHandler::modulesHasPermission() is called once per module so you end up with a lot of calls. I think static cache for that
method is enough, no need for the permissions itself (as they also cause the test failures, if enabled).

larowlan’s picture

Issue summary: View changes
larowlan’s picture

Status: Active » Needs review
FileSize
4.45 KB

Updated issue summary, added beta template

Here's a patch

Status: Needs review » Needs work

The last submitted patch, 4: perms-cache-2339487.1.patch, failed testing.

larowlan’s picture

Status: Needs work » Needs review
FileSize
1.6 KB
5.32 KB

one day I'll remember to run unit tests first

jibran’s picture

+++ b/core/modules/user/src/PermissionHandler.php
@@ -94,32 +101,49 @@ public function getPermissions() {
+   *   permissions for all modules.
+   * @return \array[] Each return permission is an array with the following keys:
+   * Each return permission is an array with the following keys:

Blank comment line missing between @param and @return also desc of @return is not correctly indent.

Fabianx’s picture

Issue tags: +needs profiling, +Performance
  1. +++ b/core/modules/user/src/PermissionHandler.php
    @@ -51,6 +51,13 @@ class PermissionHandler implements PermissionHandlerInterface {
    +  protected $permisisonsCache;
    

    typo - permisisonsCache

    its used consistently though ...

  2. +++ b/core/modules/user/src/PermissionHandler.php
    @@ -94,32 +101,49 @@ public function getPermissions() {
    +    else {
    +      // Use the default discovery - which handles all enabled modules.
    +      $discovery = $this->getYamlDiscovery();
    +    }
    

    If we want to do that, getYamlDiscovery() should take a modules parameter as well - instead of copying logic directly in here.

As Berdir I am not yet convinced on this, how often is the static cache used anyway?

This needs some up-front profiling, I think.

In general it makes sense, but its in the details ...

dawehner’s picture

As Berdir I am not yet convinced on this, how often is the static cache used anyway?

The only place where this kinda matters is when you install Drupal via CLI, as far as I understand, because in that case you parse the permissions
multiple times per request. Once for every user_modules_installed() call.

Berdir’s picture

Right, but does that actually run multiple times, the admin role does't exist until the profile configuration is installed, which is the last module of an installation?

I'd say lets wait and see if #2435075: Implement admin role as a flag on the role storage, simplify permissions page, remove user_modules_installed gets in, if so, we can probably close this as won't fix/duplicate?

dawehner’s picture

Status: Needs review » Postponed

Yeah let's wait.

Fabianx’s picture

For user_modules_installed() this would not have helped in the form it is here.

I think that was intended for the admin usage in system_admin_tasks or such ...

But yes, lets wait.

jhedstrom’s picture

Now that #2435075: Implement admin role as a flag on the role storage, simplify permissions page, remove user_modules_installed is in, is this still needed? I came across this issue from the TODO in the code that is being worked on over at #2342229: Add permissionByModule to user.permissions service. If this is now resolved, that TODO should be removed.

dawehner’s picture

Well, you could have a look, I think admin/index is still slow, due to that.

jhedstrom’s picture

Status: Postponed » Needs review

Patch from #6 still applies. I profiled admin/index with and without the patch:

        | Incl. Wall Time | IWall% | Excl. Wall Time |
Without |        1,892,381|  100.0%|             373 |
With    |         561,759 |  100.0%|             351 |
Fabianx’s picture

I think we should do this :)

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Its a little bit odd that we just static cache the module one, but I guess this is the primary usecase we have. We can iterate later, if needed.

Fabianx’s picture

Status: Reviewed & tested by the community » Needs work

Sorry, CNW per review from #8

Berdir’s picture

Once again, wondering how much of an improvement remains here when we have the file cache issue in. We need to get that done :)

jhedstrom’s picture

Status: Needs work » Needs review
FileSize
3.35 KB
5.96 KB

This should address #8.

claudiu.cristea’s picture

We will load the permission list also here #2571235: [regression] Roles should depend on objects that are building the granted permissions. So I think we need not only a static but a persistent cache :(

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.

jncruces’s picture

Is this patch usable for a production site?

Every time i access to extend page i get a timeout. Normally i only comment the lines of permission in ModulesListForm.php (lines 224 - 232).

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.

Berdir’s picture

We have a few cases now where this would be useful, for example the mentioned #2571235: [regression] Roles should depend on objects that are building the granted permissions and also #2934812: Use render caching on permissions UI table that I just opened. And those cases actually need the permissions, not just whether or not a module has permissions.

So lets try to statically cache the actual permissions list. I have an idea on how to solve the testing problem (basically just reset the cache whenever we validate permissions in tests).

Based on some quick profiling, the previous patch was 54% faster than HEAD on admin/index, this one is about 7% faster again but not sure if I trust that number and that would also only be true if moduleProvidesPermissions() is called for most/all installed modules, but if so, then getting the permissions and sorting them just once is a slight advantage. And not calling it multiple times for the same module because I removed the per-module static cache.

One advantage of this is also that it becomes trivial and doesn't require any test or interface additions.

Status: Needs review » Needs work

The last submitted patch, 27: permission-cache-2339487-27.patch, failed testing. View results

Berdir’s picture

Status: Needs work » Needs review
Issue tags: -needs profiling
FileSize
1.99 KB

Like this. We could also add an explicit method, but then we need an interface change again.

I think we've done enough profiling?

Status: Needs review » Needs work

The last submitted patch, 29: permission-cache-2339487-29.patch, failed testing. View results

Berdir’s picture

Status: Needs work » Needs review
FileSize
4.24 KB
2.25 KB

So a single actual test fail and that's a update test that tests that new permissions are added. I think that's OK to break that?

phenaproxima’s picture

We should probably add a resetCache() method or something, so that it's possible to clear the static cache explicitly if needed...no?

Berdir’s picture

I thought about that, not sure. That would require an additional method on the interface.

The only use cases to do that are tests IMHO, for that resetting the container might be sufficient?

phenaproxima’s picture

The only use cases to do that are tests IMHO, for that resetting the container might be sufficient?

I could imagine possible use cases outside of tests, like if your code has installed or uninstalled a module and needs to refresh the set of available permissions for some reason. I think it would be worthwhile to add the method (which is allowed in a minor release as long as there's a base implementation), because without it, any code that finds itself needing to refresh the list of permissions in a non-testing context is up you-know-which creek without a paddle. :)

Berdir’s picture

> like if your code has installed or uninstalled a module and needs to refresh the set of available permissions

1. Installing/uninstalling module creates a new container => new permission handler object, cache is gone

2. We don't need the list of permissions to check if you have a permission, we only need it for validation in tests and some UI elements.. the permission page and the modules/admin index page tocheck if a a module has permissions or not.

I'm not against it, just think that we should have a good reason to extend an interface with a method, even if it's very unlikely that someone has an alternative implementation.

dawehner’s picture

What about we add the method when something actually has a usecase for it?

larowlan’s picture

Agree

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.

joachim’s picture

Version: 8.6.x-dev » 8.8.x-dev
Status: Needs review » Needs work

This should be a permanent cache, not static.

The use case is places in the UI that refer to a permission by its human-readable label, and which currently violate DRY by repeating the label that's defined in permissions.yml.

For example, on the user admin settings page:

      '#description' => $this->t('Users with the %select-cancel-method or %administer-users <a href=":permissions-url">permissions</a> can override this default method.', ['%select-cancel-method' => $this->t('Select method for cancelling account'), '%administer-users' => $this->t('Administer users'), ':permissions-url' => $this->url('user.admin_permissions')]),

(For at least a while, the permission label here didn't match the one in user.permissions.yml! That bug is fixed in 8.8.x at least!)

Berdir’s picture

A persistent cache causes a lot of problems with dynamic permission, I'm not convinced that's worth it because nothing uses permission labels in the critical path and that UI not using the label is a different issue IMHO.

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.

Oscaner’s picture

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.

andileco’s picture

We were getting locked out of the /admin/modules page, and applying #29 on 8.8.6 fixed this for us. We will try the other patches too. Just wanted to bump this a bit, as without this patch, we couldn't enable modules.

narendra.rajwar27’s picture

Assigned: Unassigned » narendra.rajwar27
narendra.rajwar27’s picture

Status: Needs work » Needs review
FileSize
3.35 KB
2.99 KB

Adding patch for 8.9 and 9.1 branch.

narendra.rajwar27’s picture

Assigned: narendra.rajwar27 » Unassigned

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.

Nigel Cunningham’s picture

Status: Needs review » Needs work

I'm currently building a rather large site with quite a lot of modules enabled. With this patch enabled, /admin/modules went from timeout out after 70s to loading in 23s (both with profiling enabled). The profiling led me to this patch via the @TODO comment in moduleProvidesPermissions.

The patch itself looks good to me, but needs some tests and therefore needs work.

Regards,

Nigel

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.

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

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

drupalviking’s picture

I had a problem with a site that I upgraded from 9.0.6 => 9.3.3 I had not worked on it prior to the project's completion, but before I upgraded I did access the /admin/module page at least once.

After updating, I just got 504 Gateway Timeout, but locally and on the staging server, only when accessing the Module list page. After some hefty debugging, I was able to find a comment that lead me to this page. I applied #47 with composer and now it works just fine. It's still relatively slow, and I'm not sure why this is happening on this project alone, but at least, I can confirm that the patch fixes my error, and from my behalf, it has been tested by the community.

joegraduate’s picture

Issue tags: +Needs tests

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

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now 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.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now 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: 10.1.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, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

almunnings’s picture

With patch
Drupal 10.1 gets a heap of weird issues on installing modules.
Specifically with Drupal\Component\DependencyInjection\ReverseContainer

Service Container has a null value for generateServiceIdHash.

Running a drush cim that would install a module, occasionally throws a delightfully difficult error to trace.

It's a result of \Drupal::getContainer()->set('user.permissions', NULL);

TypeError: Drupal\Component\DependencyInjection\ReverseContainer::generateServiceIdHash(): Argument #1 ($object) must be of type object, null given in /app/site/web/core/lib/Drupal/Component/DependencyInjection/ReverseContainer.php on line 87 #0 [internal function]: Drupal\Component\DependencyInjection\ReverseContainer->generateServiceIdHash(NULL)
#1 /app/site/web/core/lib/Drupal/Component/DependencyInjection/ReverseContainer.php(75): array_map(Array, Array)
#2 /app/site/web/core/lib/Drupal/Core/DrupalKernel.php(903): Drupal\Component\DependencyInjection\ReverseContainer->recordContainer()
#3 /app/site/vendor/drush/drush/src/Drupal/DrupalKernelTrait.php(70): Drupal\Core\DrupalKernel->initializeContainer()
#4 /app/site/web/core/lib/Drupal/Core/DrupalKernel.php(816): Drush\Drupal\DrupalKernel->initializeContainer()
#5 /app/site/web/core/lib/Drupal/Core/Extension/ModuleInstaller.php(608): Drupal\Core\DrupalKernel->updateModules(Array, Array)
#6 /app/site/web/core/lib/Drupal/Core/Extension/ModuleInstaller.php(244): Drupal\Core\Extension\ModuleInstaller->updateKernel(Array)
#7 /app/site/web/core/lib/Drupal/Core/ProxyClass/Extension/ModuleInstaller.php(83): Drupal\Core\Extension\ModuleInstaller->install(Array, false)
#8 /app/site/web/core/lib/Drupal/Core/Config/ConfigImporter.php(872): Drupal\Core\ProxyClass\Extension\ModuleInstaller->install(Array, false)
#9 /app/site/web/core/lib/Drupal/Core/Config/ConfigImporter.php(624): Drupal\Core\Config\ConfigImporter->processExtension('module', 'install', 'graphql_compose...')
#10 /app/site/web/core/lib/Drupal/Core/Config/ConfigImporter.php(561): Drupal\Core\Config\ConfigImporter->processExtensions(Array)
#11 /app/site/vendor/drush/drush/src/Drupal/Commands/config/ConfigImportCommands.php(300): Drupal\Core\Config\ConfigImporter->doSyncStep('processExtensio...', Array)
#12 /app/site/vendor/drush/drush/includes/drush.inc(122): Drush\Drupal\Commands\config\ConfigImportCommands->doImport(Object(Drupal\Core\Config\StorageComparer))
#13 /app/site/vendor/drush/drush/includes/drush.inc(113): drush_call_user_func_array(Array, Array)
#14 /app/site/vendor/drush/drush/src/Drupal/Commands/config/ConfigImportCommands.php(271): drush_op(Array, Object(Drupal\Core\Config\StorageComparer))
#15 [internal function]: Drush\Drupal\Commands\config\ConfigImportCommands->import(Array)
#16 /app/site/vendor/consolidation/annotated-command/src/CommandProcessor.php(276): call_user_func_array(Array, Array)
#17 /app/site/vendor/consolidation/annotated-command/src/CommandProcessor.php(212): Consolidation\AnnotatedCommand\CommandProcessor->runCommandCallback(Array, Object(Consolidation\AnnotatedCommand\CommandData))
#18 /app/site/vendor/consolidation/annotated-command/src/CommandProcessor.php(176): Consolidation\AnnotatedCommand\CommandProcessor->validateRunAndAlter(Array, Array, Object(Consolidation\AnnotatedCommand\CommandData))
#19 /app/site/vendor/consolidation/annotated-command/src/AnnotatedCommand.php(391): Consolidation\AnnotatedCommand\CommandProcessor->process(Object(Symfony\Component\Console\Output\ConsoleOutput), Array, Array, Object(Consolidation\AnnotatedCommand\CommandData))
#20 /app/site/vendor/symfony/console/Command/Command.php(326): Consolidation\AnnotatedCommand\AnnotatedCommand->execute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#21 /app/site/vendor/symfony/console/Application.php(1081): Symfony\Component\Console\Command\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#22 /app/site/vendor/symfony/console/Application.php(320): Symfony\Component\Console\Application->doRunCommand(Object(Consolidation\AnnotatedCommand\AnnotatedCommand), Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#23 /app/site/vendor/symfony/console/Application.php(174): Symfony\Component\Console\Application->doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#24 /app/site/vendor/drush/drush/src/Runtime/Runtime.php(124): Symfony\Component\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#25 /app/site/vendor/drush/drush/src/Runtime/Runtime.php(51): Drush\Runtime\Runtime->doRun(Array, Object(Symfony\Component\Console\Output\ConsoleOutput))
#26 /app/site/vendor/drush/drush/drush.php(79): Drush\Runtime\Runtime->run(Array)
#27 /app/site/vendor/drush/drush/drush(4): require('/app/site/vendo...')
#28 {main}
TypeError: Drupal\Component\DependencyInjection\ReverseContainer::generateServiceIdHash(): Argument #1 ($object) must be of type object, null given in Drupal\Component\DependencyInjection\ReverseContainer->generateServiceIdHash() (line 87 of /app/site/web/core/lib/Drupal/Component/DependencyInjection/ReverseContainer.php).
mglaman’s picture

This shouldn't use a cache property. It should use a memory cache backend that falls back to a permanent cache backend.

Like JSON:API does:

  # Cache.
  cache.jsonapi_memory:
    class: Drupal\Core\Cache\MemoryCache\MemoryCache
    public: false
  cache.jsonapi_resource_types:
    class: Drupal\Core\Cache\BackendChain
    calls:
      - [appendBackend, ['@cache.jsonapi_memory']]
      - [appendBackend, ['@cache.default']]
    tags: [{ name: cache.bin }]

This allows for invalidating the in-memory cache within one request.

catch’s picture

Just read down and was thinking the same as #59, and then there was the comment so I didn't have to write it :)

Although given #41 I think we should add the memory backend here and then move persistent caching, if it's needed at all, to a follow-up.

kim.pepper made their first commit to this issue’s fork.

kim.pepper’s picture

Status: Needs work » Needs review

Pushed an MR that adds a MemoryCache.

kim.pepper’s picture