Managing Large Menus & Memory Usage during Drupal Cache Clears

Many things happen during a Drupal cache clear. Rebuilding routing and menu caches is one of them. When there are thousands of menu items this process can become resource intensive, to the point where the process may exceed memory limits causing deployments to fail. However, a small detail in how menu items are stored can completely change the outcome.

The menu_link_content_data table has two columns, link__uri and rediscover that are of interest.

| id | bundle | title | link__uri | rediscover | | --- | | --- | --- | --- | --- | | 30 | menu_link_content | Directory | internal:/directories | 1 | | 31 | menu_link_content | About Us | entity:node/23 | 0 |

Menu items saved with a link__uri such as internal:/directories will be marked for rediscovery. Menu items with a link__uri such as entity:node/26 will not be marked for rediscovery.

This differentiation is done via PHP's parse_url() function, specifically the value of the scheme data which informs the rediscovery state as seen below:

Drupal\menu_link_content\Entity\MenuLinkContent::preSave()

if (parse_url($this->link->uri, PHP_URL_SCHEME) === 'internal') {
$this->setRequiresRediscovery(TRUE);
}
else {
$this->setRequiresRediscovery(FALSE);
}

This rediscovery value comes into play in the following code that is called during the cache rebuild process:

Drupal\menu_link_content\Plugin\Deriver\MenuLinkContentDeriver::getDerivativeDefinitions()

// Get all custom menu links which should be rediscovered.
$entity_ids = $this->entityTypeManager->getStorage('menu_link_content')->getQuery()
->condition('rediscover', TRUE)
->execute();

With this knowledge, I could begin crafting a solution that would significantly reduce memory usage and speed up our deployment process. The key was to convert as many menu paths as possible to the entity:node/123 format.

First, I found all menu items with the internal:/node/% format and fixed those.

$query->condition('link.uri', 'internal:/node/%', 'LIKE');

I followed that up with a query to find the remaining aliased items.

$query->condition('link.uri', 'internal:/%', 'LIKE');

I placed these queries into some update hooks, ran the database updates, and the deployment issue was resolved. Deployments were now speedy and efficient (memory usage went from ~750mB to ~115mB), and I learned something new about Drupal along the way.

(I created a gist with the complete update hook code.)