- Issue created by @berdir
- 🇬🇧United Kingdom catch
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 📌 Remove Url generator from router class Active .
- 🇨🇭Switzerland berdir Switzerland
As mentioned in 📌 Remove Url generator from router class Active , 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.
- 🇬🇧United Kingdom catch
Here's some from https://www.drupal.org/files/issues/2025-08-08/1754652863--.txt → fthat look like low-hanging fruit to break/shorten:
theme_handler -> extension.list.theme -> info_parser -> extension.list.theme_engine
We don't need the info_parser until 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 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 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.
- 🇬🇧United Kingdom catch
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.