Problem/Motivation

In #3518903: Convert template preprocess in system.module, I identified this dependency chain that loads ModuleExtensionList very early:

router_listener -> router -> router.no_access_checks -> url_generator -> renderer -> theme.manager -> theme.registry -> extension.list.module

And in #3535324: Convert template_preprocess and related functions in views_ui, I also noticed that OptionsRequestSubscriber triggers a very early chain for the ConfigFactory.

I'm thinking about some kind of debug script within the Container that will dump out dependency chains into a file, to identify long ones and verify the impact of changes.

Use some service closures in specific places.

Child issues:

CommentFileSizeAuthor
#3 1754652863--.txt7.35 KBberdir

Comments

berdir created an issue. See original summary.

catch’s picture

This is a good idea.

Specifically with this one:

router_listener -> router -> router.no_access_checks -> url_generator -> renderer -> theme.manager -> theme.registry -> extension.list.module

Url generator in this one looked weird, so I checked, it's not used.

This has been the case since #3293284: Throw an exception when Router::generate() is called which is 'only' three years ago. Opened #3540398: Remove Url generator from router class.

berdir’s picture

StatusFileSize
new7.35 KB

As mentioned in #3540398: Remove Url generator from router class, I wrote a little patch to write out all essentially all Container::createService() with their initiator. Initiator here means the first createService() call when we're not already within a createService() chain. And when leaving that initial createService() all, I write out that list as a single line into a txt file. The result is a list of all dependency chains. Any direct call to get() that is a new server, including such as plugin/storage/controller "DI" is seen as a standalone thing. As well as service locators and similar factories that initiate services lazily.

A separate file is written for each request with timestamp and path, which allow to compare them. It's a single line, but it's actually a tree of services, so not every -> actually represents a chained call. It would be possible to visualize this better, but it might also make it harder to diff. That wasn't a focus yet.

The idea is to track when a given service is used the first time. And the goal of this issue would be to find meaningful and worthwhile ways to push them "back", non-html (such as json api, but also things like autocomplete callbacks and assets) and cached pages are likely where we'll see the most benefit, as "back" might mean that they wouldn't be needed at all. The script can be run before and after a change to visual the change.

I excluded private services because those would differ between cache clears and likely aren't so meaningful here.

That can either be done with a regular diff, or what I prefer, within a fresh git repository with a word-based, as seen in the screenshot in that other issue.

The changes as diff:

diff --git a/core/lib/Drupal/Component/DependencyInjection/Container.php b/core/lib/Drupal/Component/DependencyInjection/Container.php
index dc68c9c8a24..5f4cc3d6521 100644
--- a/core/lib/Drupal/Component/DependencyInjection/Container.php
+++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
@@ -46,6 +46,8 @@
  */
 class Container implements ContainerInterface, ResetInterface {

+  protected array $currentDependencyChain = [];
+
   /**
    * The parameters of the container.
    *
@@ -225,6 +227,15 @@ public function reset(): void {
    *   and cannot be instantiated.
    */
   protected function createService(array $definition, $id) {
+
+    $initial = FALSE;
+    if (!str_starts_with($id, 'private__')) {
+      if (empty($this->currentDependencyChain)) {
+        $initial = TRUE;
+      }
+      $this->currentDependencyChain[] = $id;
+    }
+
     if (isset($definition['synthetic']) && $definition['synthetic'] === TRUE) {
       throw new RuntimeException(sprintf('You have requested a synthetic service ("%s"). The service container does not know how to construct this service. The service will need to be set before it is first used.', $id));
     }
@@ -299,6 +310,11 @@ protected function createService(array $definition, $id) {
       call_user_func($callable, $service);
     }

+    if ($initial && defined('DEPENDENCY_DEBUG_FILE')) {
+      file_put_contents(DEPENDENCY_DEBUG_FILE, implode(' -> ', $this->currentDependencyChain) . "\n", FILE_APPEND);
+      $this->currentDependencyChain = [];
+    }
+
     return $service;
   }

diff --git a/index.php b/index.php
index 750dc282dc2..d4fe93a2b0a 100644
--- a/index.php
+++ b/index.php
@@ -16,6 +16,9 @@
 $kernel = new DrupalKernel('prod', $autoloader);

 $request = Request::createFromGlobals();
+define('DEPENDENCY_DEBUG_FILE', __DIR__ . '/dependencies/' . $request->server->get('REQUEST_TIME') . '-' . str_replace('/', '-', $request->getPathInfo()) .  '.txt');
+touch(DEPENDENCY_DEBUG_FILE);
+
 $response = $kernel->handle($request);
 $response->send();

I attached the current state on head as a text file, it's a bit much as a snippet and wrapping makes it hard to read.

In some cases, we'll want to specifically test dynamic page or even page cache hits, I'll do some testing with this in the middleware issue with those scenarios.

catch’s picture

Here's some from https://www.drupal.org/files/issues/2025-08-08/1754652863--.txt that look like low-hanging fruit to break/shorten. Focused on ones where it seems pretty likely we can skip creating those services altogether on most requests.

theme_handler -> extension.list.theme -> info_parser -> extension.list.theme_engine

We don't need the info_parser unless we parse info files.

html_response.big_pipe_subscriber -> big_pipe -> logger.channel.php -> logger.factory -> logger.dblog -> logger.log_message_parser -> logger.drupaltodrush

The logger is only needed for errors.

html_response.subscriber -> html_response.attachments_processor -> asset.resolver -> library.discovery -> library.discovery.parser -> library.libraries_directory_file_finder -> extension.path.resolver -> plugin.manager.sdc -> Drupal\Core\Theme\ComponentNegotiator -> file_system -> Drupal\Core\Theme\Component\SchemaCompatibilityChecker -> 

library.discovery.parser is only needed on library discovery cache misses.

Drupal\Core\Template\IconsTwigExtension -> plugin.manager.icon_pack -> plugin.manager.icon_extractor 

Not sure which one of these should be the service closure but again we don't need these unless there's an actual icon to render.

theme.negotiator.system.batch -> batch.storage

Don't need the batch storage unless we're on the batch route.

drupal.proxy_original_service.node_preview -> tempstore.private

Don't need the private tempstore unless we're on the node preview route.

drupal.proxy_original_service.paramconverter.views_ui -> entity_type.manager -> string_translation -> string_translator.custom_strings -> entity.last_installed_schema.repository -> tempstore.shared

Similarly don't need the shared tempstore unless views ui param converter is actually active.

(all of these make me think we might want to rethink param converters and theme negotiators, maybe we should have a way to attach one to a specific route so they never get consulted unless we're on the route in the first place?)

Drupal\Core\Template\Loader\ComponentLoader -> logger.channel.default

Logger is only needed if we log.

twig.extension.debug -> twig.extension.varDumper

One of these can go.

catch’s picture

Also:

session -> session_manager -> database -> session_manager.metadata_bag

We should be able to avoid creating a database connection until we know we have a session cookie to check against. Dynamic page cache hits for anonymous users can theoretically be served without touching the database at all. Auth can if the an alternative session storage is used.

berdir’s picture

Title: Explore using more service closures to break deep dependency chains and load fewer services » [meta] Explore using more service closures to break deep dependency chains and load fewer services
Category: Task » Plan
Issue summary: View changes
berdir’s picture

catch’s picture

From the other issue, there must be a way to not use the is front page cache context for the breadcrumb (maybe we could use the route match from the main request only)?

berdir’s picture

Issue summary: View changes

Looked into the session_manager/database topic, but it's way more complex than I thought for a real benefit on dynamic page cache hits: #3541705: Lazy inject database into varous services

catch’s picture

#3511357-57: Clean up hook implementations in Node module brings up form builder in node module hooks that could probably have the same treatment.

godotislate’s picture

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.