Problem/Motivation

Have been meaning to open this issue for years now. Latest reminder was bojanz linking to https://github.com/composer/composer/pull/5174#issuecomment-208824818

The apcu classloader from Symfony uses an entry-per-class. This means lots of sets and gets on each request. Also APCu doesn't handle LRU, or getting full very well, so it's prone to fragmentation.

Proposed resolution

Add an alternative APCu classloader which maintains a single entry. The main challenge is to avoid write-stampedes. Fetching and writing at the end of the request, in a similar way to CacheCollector would help vs. that.

Also need to be careful that the array size doesn't get ridiculous.

Remaining tasks

User interface changes

API changes

Data model changes

Comments

catch created an issue. See original summary.

dawehner’s picture

Status: Active » Needs review
StatusFileSize
new4.83 KB

Let's try that out. This is a really naive implementation

catch’s picture

+++ b/core/lib/Drupal/Component/ClassLoader/ApcClassLoader.php
@@ -0,0 +1,123 @@
+  public function __destruct() {
+    if (!$this->data) {
+      apcu_store($this->prefix, $this->data);
+    }
+  }

Not sure we need much more than that, except this isn't going to work ;) Probably needs a protected $writeCache; for this.

dawehner’s picture

Especially the boolean logic is broken :)

dawehner’s picture

Issue tags: +needs profiling
StatusFileSize
new5.04 KB
new1.12 KB

Alright, here is a more working implementation :P

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now 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.

anavarre’s picture

Issue tags: +apcu
catch’s picture

+++ b/core/lib/Drupal/Component/ClassLoader/ApcClassLoader.php
@@ -0,0 +1,131 @@
+      apcu_store($this->prefix, $this->data);

Probably worth doing a get -> merge -> set here?

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

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now 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.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now 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.

dawehner’s picture

StatusFileSize
new5.41 KB
new2.22 KB

Probably worth doing a get -> merge -> set here?

This is really a strong point, especially when it comes down to plugin/controller classes.

Status: Needs review » Needs work

The last submitted patch, 11: 2704571-11.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new6.04 KB
new644 bytes

This just fixes the last failure.

catch’s picture

This looks great, thanks for keeping it going!

Two comments on comments, otherwise looks ready to me.

For profiling I think we'd just want to see one get and one set per request, and a request with a warm cache that does not do a set? It would be interesting to know how large the items gets after clicking around admin pages a bit maybe too.

  1. +++ b/core/lib/Drupal/Component/ClassLoader/ApcClassLoader.php
    @@ -0,0 +1,138 @@
    +      throw new \RuntimeException('Unable to use ApcClassLoader as APC is not installed.');
    

    APC or APCU now?

  2. +++ b/core/lib/Drupal/Component/ClassLoader/ApcClassLoader.php
    @@ -0,0 +1,138 @@
    +        if ($class_map != $previous_classmap) {
    

    Do we need a comment here to say we're not using strict comparison because we don't care about array order?

catch’s picture

One positive for this patch from #2926309: Random fail due to APCu not being able to allocate memory - it will significantly reduce the number of entries, which means default APCu settings might be a bit closer to reality.

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

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now 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.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now 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.

andypost’s picture

andypost’s picture

+++ b/core/lib/Drupal/Component/ClassLoader/ApcClassLoader.php
@@ -0,0 +1,138 @@
+namespace Drupal\Component\ClassLoader;

Maybe better to move this to `Drupal\Core` namespace? otherwise this new component needs composer.json

andypost’s picture

StatusFileSize
new3.4 KB
new6.25 KB

Patch with fixed comments for #14 based on reroll for 8.7.x

+++ b/core/lib/Drupal/Component/ClassLoader/ApcClassLoader.php
@@ -0,0 +1,138 @@
+    if (!empty($this->classmap[$class])) {
...
+      $this->classmap[$class] = $this->decorated->findFile($class);
...
+    return $this->classmap[$class];

decorated could return FALSE or NULL when file not found so probably better to normalize it

For example \Composer\Autoload\ClassLoader::findFile() returns FALSE

Status: Needs review » Needs work

The last submitted patch, 20: 2704571-20.patch, failed testing. View results

dawehner’s picture

This feels like a patch which ideally someone actually battle-tests in a live environment.

andypost’s picture

@dawehner yep, that's why I rerolled to get real numbers on project - at least it removes 8k calls to apcu_fetch() but I need to get the RAM effect

ndobromirov’s picture

+++ b/core/lib/Drupal/Component/ClassLoader/ApcClassLoader.php
@@ -0,0 +1,144 @@
+      // A different request might have loaded different classes, so let's merge
+      // the data to improve efficiency.
+      if ($previous_classmap = apcu_fetch($this->prefix)) {
+        $class_map = array_merge($this->classmap, $previous_classmap);
+        // Using non-strict comparison because order of keys does not matter.

I am concerned about initialization. If the key is missing, how can this initialize without triggering warnings? On first call we will end with array_merge(array, false). The negative case should be handled with a sane default for $previous_classmap - an empty array.

gábor hojtsy’s picture

Adding to the Drupal 9 issue tree, since this may need to be done as part of moving to Symfony 4 as per #3020296: Remove Symfony's classloader as it does not exist in Symfony 4.

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

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new25.82 KB
new2.29 KB
  • Added composer.json and license to the component
  • Expanded the documentation

Status: Needs review » Needs work

The last submitted patch, 27: 2704571-27.patch, failed testing. View results

yogeshmpawar’s picture

Status: Needs work » Needs review

All looks good so setting back to Needs Review! Tests failure is not related (see #3055648: Frequent random fail in \Drupal\Tests\media_library\FunctionalJavascript\MediaLibraryTest).

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.

bradjones1’s picture

Version: 8.9.x-dev » 9.1.x-dev
catch’s picture

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

This is one of two classloader options for unblocking Drupal 9's release, so needs to stay tracked against 9.0.x and could still potentially be backported to 8.9.x. See #3020296: Remove Symfony's classloader as it does not exist in Symfony 4.

bradjones1’s picture

Thanks for the clarification and sorry for the noise. I'm trying to digest these in context of wrapping my head around an option for preloading in #3108687: Compatibility with/take advantage of code preloading in PHP 7.4 so doing some cleanup on some older tickets as I go. I'll try to help with these since it would be great not to have a delay on 9.0.

bradjones1’s picture

StatusFileSize
new25.82 KB

Re-rolled for a small bit of fuzz against 9.0.x.

Status: Needs review » Needs work

The last submitted patch, 34: 2704571-34.patch, failed testing. View results

bradjones1’s picture

The failure is an assertion after module uninstallation of help_topics... the test was added in October, so after the last pass. I'll have to manually test to see if this is a fluke or related somehow.

gapple’s picture

Issue tags: +Needs tests
+    if (!empty($this->classmap[$class])) {
+      return $this->classmap[$class];
+    }
+    elseif (!isset($this->classmap) && $data = apcu_fetch($this->prefix)) {
+      $this->classmap = $data;
+    }
+    else {
+      $this->classmap[$class] = $this->decorated->findFile($class);
+      $this->writeClassMap = TRUE;
+    }
+
+    return $this->classmap[$class];

The logic here doesn't look right - my understanding is that if the classmap is not already initialized, and the requested class does not already exist in the cached data, then it will skip the call to the decorated class

+      if ($previous_classmap = apcu_fetch($this->prefix)) {
+        $class_map = array_merge($this->classmap, $previous_classmap);
+        // Using non-strict comparison because order of keys does not matter.
+        if ($class_map != $previous_classmap) {
+          apcu_store($this->prefix, $this->classmap);
+        }
+      }

@ndobromirov raised concern that if the classmap wasn't previously cached this would raise a warning due to calling array_merge(array, false), but I think the conditional will prevent the data from ever being cached, since fetch will return FALSE.

gapple’s picture

Also, I think since the classmap is initialized to an empty array

  /**
   * A map between classname and filename containing this class.
   *
   * @var string[]
   */
  protected $classmap = [];

Then the isset check will always return TRUE

    elseif (!isset($this->classmap) && $data = apcu_fetch($this->prefix)) {
      $this->classmap = $data;
    }

and data will never be fetched from APCu.

gapple’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
StatusFileSize
new6.28 KB
new30.52 KB

Fixes for my previous comments, plus a test to check that a repeated path lookup uses cached data.

gapple’s picture

An important consideration is that if composer's caching is enabled, then it will cache each class lookup individually as well as this decorator's single cache entry.

- This decorator could be only enabled in DrupalKernel::initializeSettings() if composer's caching is not

  if (function_exists('apcu_fetch')) {
    if (method_exists($this->classLoader, 'getApcuPrefix') && $this->classLoader->getApcuPrefix() == NULL) {
      $loader = new ApcClassLoader($prefix, $this->classLoader);
    }
  }

- or Composer's caching could be disabled in favour of Drupal's

  if (function_exists('apcu_fetch')) {
    if (method_exists($this->classLoader, 'setApcuPrefix')) {
      $this->classLoader->setApcuPrefix(NULL);
    }
    $loader = new ApcClassLoader($prefix, $this->classLoader);
  }

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.

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.

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.

nod_’s picture

I'm getting a little off-track but when I saw various xdebug profiles runs for a stock D9 site, the acpu thing jump at me and I've been thinking about the idea of bundling PHP classes that are dependent/related in a single file, like we'd bundle js code.

This would be more of a composer-level optimisation (like in the dump-autoload command). Had the idea seeing the symfony code where there are many class_exists all over the place to "optimize" loading, for example the HttpKernel.php has:

// Help opcache.preload discover always-needed symbols
class_exists(LegacyEventDispatcherProxy::class);
class_exists(ControllerArgumentsEvent::class);
class_exists(ControllerEvent::class);
class_exists(ExceptionEvent::class);
class_exists(FinishRequestEvent::class);
class_exists(RequestEvent::class);
class_exists(ResponseEvent::class);
class_exists(TerminateEvent::class);
class_exists(ViewEvent::class);
class_exists(KernelEvents::class);

A whole dependency graph would be needed but since it's done at composer time that might be acceptable in term of time/memory consumption. The file above would bundle everything only used by this class (using php multiple namespace feature):

namespace Symfony\Component\EventDispatcher {
  use Psr\EventDispatcher\StoppableEventInterface;
  use Symfony\Contracts\EventDispatcher\Event as ContractsEvent;
  use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface;
  final class LegacyEventDispatcherProxy implements EventDispatcherInterface {}
}

namespace Symfony\Component\HttpKernel {
  // ...
  class HttpKernel implements HttpKernelInterface, TerminableInterface
  // ...
}

This should reduce the number of separate file needed (don't have a measure of how much it would help for now, didn't spend too much time on this). I mean if it looks good, we might not need to do much on the acpu front. There were a couple of strange things going on, I think the "use" are global within the file so if we use different class with the same name in different namespace it kinda breaks but it should be possible to find a solution.

berdir’s picture

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.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new151 bytes

The Needs Review Queue Bot tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

bkosborne’s picture

catch’s picture

@bkosborne yes the composer classloader uses APCu entry per file, and because modules can be installed and uninstalled, we can't rely on the compiled container.

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.

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.