Verified Commit 43004a38 authored by Lee Rowlands's avatar Lee Rowlands
Browse files

Issue #3327856 by alexpott, catch, zcht, longwave, znerol, Berdir,...

Issue #3327856 by alexpott, catch, zcht, longwave, znerol, Berdir, BramDriesen, Spokje, callen321, andypost, neclimdul, acbramley, mstrelan: Performance regression introduced by container serialization solution
parent db0bd74a
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -370,6 +370,8 @@ services:
  context.repository:
    class: Drupal\Core\Plugin\Context\LazyContextRepository
    arguments: ['@service_container']
  Drupal\Component\DependencyInjection\ReverseContainer:
    arguments: [ '@service_container' ]
  cron:
    class: Drupal\Core\Cron
    arguments: ['@module_handler', '@lock', '@queue', '@state', '@account_switcher', '@logger.channel.cron', '@plugin.manager.queue_worker', '@datetime.time']
+10 −0
Original line number Diff line number Diff line
@@ -25,6 +25,11 @@ public function getServiceIds();
   *
   * @return array
   *   Service ids keyed by a unique hash.
   *
   * @deprecated in drupal:9.5.1 and is removed from drupal:11.0.0. Use the
   *   'Drupal\Component\DependencyInjection\ReverseContainer' service instead.
   *
   * @see https://www.drupal.org/node/3327942
   */
  public function getServiceIdMappings(): array;

@@ -36,6 +41,11 @@ public function getServiceIdMappings(): array;
   *
   * @return string
   *   A unique hash identifying the object.
   *
   * @deprecated in drupal:9.5.1 and is removed from drupal:11.0.0. Use the
   *   'Drupal\Component\DependencyInjection\ReverseContainer' service instead.
   *
   * @see https://www.drupal.org/node/3327942
   */
  public function generateServiceIdHash(object $object): string;

+98 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Component\DependencyInjection;

use Symfony\Component\DependencyInjection\Container as SymfonyContainer;
use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface;

/**
 * Retrieves service IDs from the container for public services.
 *
 * Heavily inspired by \Symfony\Component\DependencyInjection\ReverseContainer.
 */
final class ReverseContainer {

  /**
   * A closure on the container that can search for services.
   *
   * @var \Closure
   */
  private \Closure $getServiceId;

  /**
   * A static map of services to a hash.
   *
   * @var array
   */
  private static array $recordedServices = [];

  /**
   * Constructs a ReverseContainer object.
   *
   * @param \Drupal\Component\DependencyInjection\Container|\Symfony\Component\DependencyInjection\Container $serviceContainer
   *   The service container.
   */
  public function __construct(private readonly Container|SymfonyContainer $serviceContainer) {
    $this->getServiceId = \Closure::bind(function ($service): ?string {
      /** @phpstan-ignore-next-line */
      return array_search($service, $this->services, TRUE) ?: NULL;
    }, $serviceContainer, $serviceContainer);
  }

  /**
   * Returns the ID of the passed object when it exists as a service.
   *
   * To be reversible, services need to be public.
   *
   * @param object $service
   *   The service to find the ID for.
   */
  public function getId(object $service): ?string {
    if ($this->serviceContainer === $service || $service instanceof SymfonyContainerInterface) {
      return 'service_container';
    }

    $hash = $this->generateServiceIdHash($service);
    $id = self::$recordedServices[$hash] ?? ($this->getServiceId)($service);

    if ($id !== NULL && $this->serviceContainer->has($id)) {
      self::$recordedServices[$hash] = $id;
      return $id;
    }

    return NULL;
  }

  /**
   * Records a map of the container's services.
   *
   * This method is used so that stale services can be serialized after a
   * container has been re-initialized.
   */
  public function recordContainer(): void {
    $service_recorder = \Closure::bind(function () : array {
      /** @phpstan-ignore-next-line */
      return $this->services;
    }, $this->serviceContainer, $this->serviceContainer);
    self::$recordedServices = array_merge(self::$recordedServices, array_flip(array_map([$this, 'generateServiceIdHash'], $service_recorder())));
  }

  /**
   * Generates an identifier for a service based on the object class and hash.
   *
   * @param object $object
   *   The object to generate an identifier for.
   *
   * @return string
   *   The object's class and hash concatenated together.
   */
  private function generateServiceIdHash(object $object): string {
    // Include class name as an additional namespace for the hash since
    // spl_object_hash's return can be recycled. This still is not a 100%
    // guarantee to be unique but makes collisions incredibly difficult and even
    // then the interface would be preserved.
    // @see https://php.net/spl_object_hash#refsect1-function.spl-object-hash-notes
    return get_class($object) . spl_object_hash($object);
  }

}
+6 −1
Original line number Diff line number Diff line
@@ -5,7 +5,10 @@
/**
 * A trait for service id hashing implementations.
 *
 * Handles delayed cache tag invalidations.
 * @deprecated in drupal:9.5.1 and is removed from drupal:11.0.0. Use the
 *   'Drupal\Component\DependencyInjection\ReverseContainer' service instead.
 *
 * @see https://www.drupal.org/node/3327942
 */
trait ServiceIdHashTrait {

@@ -13,6 +16,7 @@ trait ServiceIdHashTrait {
   * Implements \Drupal\Component\DependencyInjection\ContainerInterface::getServiceIdMappings()
   */
  public function getServiceIdMappings(): array {
    @trigger_error(__METHOD__ . "() is deprecated in drupal:9.5.1 and is removed from drupal:11.0.0. Use the 'Drupal\Component\DependencyInjection\ReverseContainer' service instead. See https://www.drupal.org/node/3327942", E_USER_DEPRECATED);
    $mapping = [];
    foreach ($this->getServiceIds() as $service_id) {
      if ($this->initialized($service_id) && $service_id !== 'service_container') {
@@ -26,6 +30,7 @@ public function getServiceIdMappings(): array {
   * Implements \Drupal\Component\DependencyInjection\ContainerInterface::generateServiceIdHash()
   */
  public function generateServiceIdHash(object $object): string {
    @trigger_error(__METHOD__ . "() is deprecated in drupal:9.5.1 and is removed from drupal:11.0.0. Use the 'Drupal\Component\DependencyInjection\ReverseContainer' service instead. See https://www.drupal.org/node/3327942", E_USER_DEPRECATED);
    // Include class name as an additional namespace for the hash since
    // spl_object_hash's return can be recycled. This still is not a 100%
    // guarantee to be unique but makes collisions incredibly difficult and even
+13 −25
Original line number Diff line number Diff line
@@ -2,9 +2,9 @@

namespace Drupal\Core\DependencyInjection;

use Drupal\Component\DependencyInjection\ReverseContainer;
use Drupal\Core\Entity\EntityStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Drupal\Core\StringTranslation\TranslatableMarkup;

/**
 * Provides dependency injection friendly methods for serialization.
@@ -34,8 +34,12 @@ public function __sleep() {
    $vars = get_object_vars($this);
    try {
      $container = \Drupal::getContainer();
      $mapping = \Drupal::service('kernel')->getServiceIdMapping();
      $reverse_container = $container->has(ReverseContainer::class) ? $container->get(ReverseContainer::class) : new ReverseContainer($container);
      foreach ($vars as $key => $value) {
        if (!is_object($value) || $value instanceof TranslatableMarkup) {
          // Ignore properties that cannot be services.
          continue;
        }
        if ($value instanceof EntityStorageInterface) {
          // If a class member is an entity storage, only store the entity type
          // ID the storage is for, so it can be used to get a fresh object on
@@ -47,19 +51,7 @@ public function __sleep() {
          $this->_entityStorages[$key] = $value->getEntityTypeId();
          unset($vars[$key]);
        }
        elseif (is_object($value)) {
          $service_id = FALSE;
          // Special case the container.
          if ($value instanceof SymfonyContainerInterface) {
            $service_id = 'service_container';
          }
          else {
            $id = $container->generateServiceIdHash($value);
            if (isset($mapping[$id])) {
              $service_id = $mapping[$id];
            }
          }
          if ($service_id) {
        elseif ($service_id = $reverse_container->getId($value)) {
          // If a class member was instantiated by the dependency injection
          // container, only store its ID so it can be used to get a fresh object
          // on unserialization.
@@ -68,13 +60,9 @@ public function __sleep() {
        }
      }
    }
    }
    catch (ContainerNotInitializedException $e) {
      // No container, no problem.
    }
    catch (ServiceNotFoundException $e) {
      // No kernel, very strange, but still no problem.
    }

    return array_keys($vars);
  }
Loading