Problem/Motivation

The Charts module currently invokes chart-specific alter hooks whose hook names contain the chart ID.

$alter_hooks = ['chart'];

if ($chart_id) {
  $alter_hooks[] = 'chart_' . $element['#chart_id'];
}

$this->moduleHandler->alter($alter_hooks, $element, $chart_id);

A similar pattern is used for chart definitions:

$alter_hooks = ['chart_definition'];

if ($element['#chart_id']) {
  $alter_hooks[] = 'chart_definition_' . $chart_id;
}

$this->moduleHandler->alter(
  $alter_hooks,
  $chart_definition,
  $element,
  $chart_id,
);

When #chart_id contains a dynamically generated UUID, Drupal stores two new hook names in the module_implements cache for every distinct chart ID:

chart_chart-<UUID>_alter
chart_definition_chart-<UUID>_alter

Drupal also caches empty hook lookup results, so this happens even when no module implements these generated UUID-specific hooks.

On a production installation this resulted in an oversized module_implements cache entry:

Cache entry: module_implements
Serialized size: approximately 86.4 MB
Total hook names: 1,234,496

The hook-name distribution showed that nearly all entries came from the two dynamic chart hook patterns:

616,767 entries with a length of 54
616,764 entries with a length of 65

These correspond almost entirely to:

chart_chart-<UUID>_alter
chart_definition_chart-<UUID>_alter

Only approximately 1,000 entries were regular Drupal hook names.

This causes:

  • Unbounded growth of the module_implements cache.
  • A very large cache_bootstrap record.
  • Increased database transfer and serialization overhead.
  • Increased PHP memory usage during Drupal bootstrap.
  • Increasing cache rebuild and request costs over time.

Steps to reproduce

  1. Install the Charts module using the 5.2.x-dev branch.
  2. Render charts whose #chart_id values contain dynamically generated UUIDs.
  3. Render many charts with different chart IDs.
  4. Inspect the module_implements cache:
drush php-eval '$c=\Drupal::cache("bootstrap")->get("module_implements"); print "Hooks: ".count($c->data).PHP_EOL."Size: ".round(strlen(serialize($c->data))/1024/1024,2)." MB".PHP_EOL;'

Count the dynamically generated chart hook names:

drush php-eval '$c=\Drupal::cache("bootstrap")->get("module_implements"); $chart=0; $definition=0; foreach (array_keys($c->data) as $hook) { if (str_starts_with((string) $hook, "chart_chart-")) { $chart++; } if (str_starts_with((string) $hook, "chart_definition_chart-")) { $definition++; } } print "chart_chart-*: ".$chart.PHP_EOL."chart_definition_chart-*: ".$definition.PHP_EOL;'

Actual result:

A new cached hook name is created for every distinct chart ID:

chart_chart-<UUID>_alter
chart_definition_chart-<UUID>_alter

The module_implements cache grows continuously.

Expected result:

Rendering charts with different runtime IDs must not create an unbounded number of hook names in Drupal's module_implements cache.

Proposed resolution

Invoke only the stable generic alter hooks:

$this->moduleHandler->alter(
  'chart',
  $element,
  $chart_id,
);

And:

$this->moduleHandler->alter(
  'chart_definition',
  $chart_definition,
  $element,
  $chart_id,
);

The chart ID remains available to hook implementations as an argument. Implementations that need chart-specific behavior can inspect $chart_id inside the generic hook:

/**
 * Implements hook_chart_alter().
 */
function mymodule_chart_alter(array &$element, $chart_id): void {
  if ($chart_id !== 'chart-example') {
    return;
  }

  // Apply chart-specific changes.
}

The same approach applies to chart definitions:

/**
 * Implements hook_chart_definition_alter().
 */
function mymodule_chart_definition_alter(
  array &$definition,
  array $element,
  $chart_id,
): void {
  if ($chart_id !== 'chart-example') {
    return;
  }

  // Apply chart-definition-specific changes.
}

The proposed patch also removes the dynamic hook documentation for:

hook_chart_CHART_ID_alter()
hook_chart_definition_CHART_ID_alter()

These dynamic hooks are the source of the unbounded cache-key cardinality.

Remaining tasks

  1. Review the proposed approach.
  2. Decide how the backward compatibility impact should be handled.
  3. Add automated coverage proving that different chart IDs do not create chart-ID-specific hook cache entries.
  4. Add automated coverage confirming that $chart_id remains available to the generic alter hooks.
  5. Update API documentation and change records if required.
  6. Test against the latest 5.2.x-dev branch.
  7. Confirm compatibility with supported Drupal core versions.
  8. Review and merge the attached patch or corresponding merge request.

User interface changes

None.

API changes

The following dynamically named hooks are removed:

hook_chart_CHART_ID_alter()
hook_chart_definition_CHART_ID_alter()

Use the generic hooks instead:

hook_chart_alter(array &$element, $chart_id)
hook_chart_definition_alter(
  array &$definition,
  array $element,
  $chart_id,
)

Chart-specific behavior should be selected by inspecting $chart_id inside the generic hook implementation.

This is a backward compatibility change for code that currently implements chart-ID-specific hooks such as:

function mymodule_chart_chart_example_alter(array &$element): void {
}

Equivalent logic can be moved to the generic hook:

function mymodule_chart_alter(array &$element, $chart_id): void {
  if ($chart_id === 'chart-example') {
    // Apply chart-specific changes.
  }
}

The maintainers should decide whether this can be accepted as a bug fix in 5.2.x, whether a deprecation path is required, or whether the change needs to target a later minor or major release.

Data model changes

None.

No database schema or configuration schema changes are required.

After deploying the fix, existing oversized cache entries can be removed using a normal cache rebuild:

drush cr

Release notes snippet:

Fixed an issue where chart IDs were used as part of dynamically generated alter-hook names. Sites rendering charts with unique runtime IDs could accumulate a very large module_implements cache entry, increasing database storage, bootstrap overhead, and PHP memory usage.

The chart ID remains available as an argument to the generic hook_chart_alter() and hook_chart_definition_alter() hooks.

AI disclosure: AI was used to help structure and edit this issue report. The technical findings, production measurements, patch, and proposed resolution were reviewed and verified by the issue author.

Issue fork charts-3608903

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

rhiss created an issue. See original summary.

rhiss’s picture

StatusFileSize
new2.55 KB

nikathone made their first commit to this issue’s fork.

nikathone’s picture

Priority: Major » Normal
Status: Active » Needs review

Hi @rhiss, thank you for raising this issue. I took a different approach on the fix you proposed and I believe it should solve your issue without requiring the maintainer to issue a new major release or break backward compatibility.

andileco made their first commit to this issue’s fork.

andileco’s picture

Status: Needs review » Postponed (maintainer needs more info)

After looking into this, we were not able to observe the described issue, as Charts doesn't use UUID for the chart ID. We are thinking there may be custom code using UUID. Will plan to move this to "works as designed" unless we hear back within the next week.

rhiss’s picture

Thanks for checking.

I agree that Charts may not generate the UUID itself. The issue is that Charts uses the provided `#chart_id` as part of the alter hook name.

If custom code provides a dynamic chart ID, Drupal caches every generated hook lookup in `module_implements`, even when no implementation exists.

In our case this created about 1.23 million cached hook names and an 86 MB `module_implements` cache. After switching to stable hooks and passing the chart ID only as context, this dropped to 891 hooks and 0.049 MB.

So the problem is not where the UUID comes from, but that an unbounded runtime value can become a cached hook name.

Suggested safer approach:

```php
$this->moduleHandler->alter('chart', $element, $chart_id);
$this->moduleHandler->alter('chart_definition', $chart_definition, $element, $chart_id);
```

If the current behavior is considered intentional, the documentation should clearly state that `#chart_id` must be stable and low-cardinality.

andileco’s picture

Status: Postponed (maintainer needs more info) » Active

andileco changed the visibility of the branch 3608903-dynamic-chart-specific-alter to hidden.

  • andileco committed dbaac169 on 5.2.x
    fix: #3608903 Dynamic chart-specific alter hooks cause unbounded...
andileco’s picture

Status: Active » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.