Skip to content

Plugins: Speed up hooks addition/removal by ~26%.#11865

Closed
bor0 wants to merge 18 commits into
WordPress:trunkfrom
bor0:trac-58291-speed-up-hooks
Closed

Plugins: Speed up hooks addition/removal by ~26%.#11865
bor0 wants to merge 18 commits into
WordPress:trunkfrom
bor0:trac-58291-speed-up-hooks

Conversation

@bor0
Copy link
Copy Markdown
Member

@bor0 bor0 commented May 19, 2026

Replaces spl_object_hash() with spl_object_id() in _wp_filter_build_unique_id() and WP_Widget_Factory, and adds an early return for plain object callbacks.

Fixes https://core.trac.wordpress.org/ticket/58291

Replaces spl_object_hash() with spl_object_id() in _wp_filter_build_unique_id()
and WP_Widget_Factory, adds an early return for plain object callbacks, removes
the unreachable null return, and drops the stale falsey-key guard in has_filter().

Fixes https://core.trac.wordpress.org/ticket/58291
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 19, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props bor0, westonruter.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions
Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

Comment thread src/wp-includes/plugin.php
Comment thread src/wp-includes/plugin.php
Comment thread src/wp-includes/plugin.php
Comment thread src/wp-includes/plugin.php
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Comment thread src/wp-includes/plugin.php Outdated
Comment thread src/wp-includes/plugin.php Outdated
Comment thread src/wp-includes/plugin.php Outdated
Comment thread src/wp-includes/plugin.php Outdated
Comment thread src/wp-includes/plugin.php Outdated
bor0 and others added 2 commits May 20, 2026 00:03
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Co-authored-by: Boro Sitnikovski <boro.sitnikovski@automattic.com>
Comment thread src/wp-includes/plugin.php Outdated
Comment thread src/wp-includes/plugin.php Outdated
Comment thread src/wp-includes/plugin.php Outdated
bor0 and others added 5 commits May 20, 2026 00:34
Co-authored-by: Weston Ruter <westonruter@gmail.com>
…que_id().

The guard should return null for non-callable arrays, not for valid ones.
…_build_unique_id().

isset()+is_string() is ~22% faster while providing the same type safety guarantee
that $callback[1] exists and is a string.
…th syntax_only.

Replaces isset()+is_string() guard with is_callable( $callback, true ), which
already guarantees $callback[0] is a string or object, removing the dead
elseif branch and trailing return null.
… type.

Narrows @param from callable|string|array to callable and fixes alignment,
reflecting that invalid callback values are usage errors caught by static
analysis, not valid input variants.
return (string) spl_object_id( (object) $callback );
}

$callback = (array) $callback;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, I thought that this line could be removed, since above $callback is excluded from being a string or an object. The only other possible value which can be callable at this point is array, so this casting would seem to be redundant. But PHPStan doesn't currently do this narrowing. Without the cast, it is seen as: mixed~(object|string) and then the is_callable() narrows it it callable(): mixed: https://phpstan.org/r/577fd6d9-b6b9-4362-9acc-0794187fa71a

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have thought PHPStan would narrow it to non-empty-list<mixed>&callable() automatically.

Copy link
Copy Markdown
Member

@westonruter westonruter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the iterations!

@bor0
Copy link
Copy Markdown
Member Author

bor0 commented May 19, 2026

Benchmark results of the PR in its current state; tested on wordpress-7.0-RC3 using wp-env

┌───────────────┬───────────────────────┬─────────────────────┬─────────────┐
│ Callback type │ Old (spl_object_hash) │ New (spl_object_id) │    Diff     │
├───────────────┼───────────────────────┼─────────────────────┼─────────────┤
│ string        │ 32.19ms               │ 32.73ms             │ ~0% (noise) │
├───────────────┼───────────────────────┼─────────────────────┼─────────────┤
│ closure       │ 50.59ms               │ 37.27ms             │ −26%        │
├───────────────┼───────────────────────┼─────────────────────┼─────────────┤
│ object+method │ 48.44ms               │ 45.56ms             │ −6%         │
├───────────────┼───────────────────────┼─────────────────────┼─────────────┤
│ static        │ 39.66ms               │ 43.10ms             │ ~0% (noise) │
└───────────────┴───────────────────────┴─────────────────────┴─────────────┘

After several iterations, I noticed that static is consistently slower by 2-3 ms.

This is because they go through the new is_callable guard that wasn't there before, but we decided that the safety guard trade-off was worth it.

For completeness:

-  if ( ! is_callable( $callback, true ) ) {
+  if ( ! isset( $callback[1] ) || ! is_string( $callback[1] ) ) {
┌─────────┬─────────┬──────────────┬─────────────────┐
│         │   Old   │ is_callable  │ isset+is_string │
├─────────┼─────────┼──────────────┼─────────────────┤
│ string  │ 32.40ms │ ~32ms (0%)   │ 32.14ms (0%)    │
├─────────┼─────────┼──────────────┼─────────────────┤
│ closure │ 51.43ms │ ~37ms (−27%) │ 36.75ms (−27%)  │
├─────────┼─────────┼──────────────┼─────────────────┤
│ object  │ 49.29ms │ ~45ms (−6%)  │ 42.70ms (−13%)  │
├─────────┼─────────┼──────────────┼─────────────────┤
│ static  │ 39.84ms │ ~43ms (+9%)  │ 40.38ms (~0%)   │
└─────────┴─────────┴──────────────┴─────────────────┘

@bor0 bor0 changed the title Plugins: Speed up hooks addition/removal by ~20%. Plugins: Speed up hooks addition/removal by ~26%. May 19, 2026
bor0 and others added 5 commits May 20, 2026 09:30
Covers return type consistency (string), uniqueness between objects,
stability for the same object, and null returns for malformed arrays.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@westonruter
Copy link
Copy Markdown
Member

This is because they go through the new is_callable guard that wasn't there before, but we decided that the safety guard trade-off was worth it.

@bor0 It looks like I was wrong. After re-testing with is_callable(), PHPStan still complains:

------ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/plugin.php
 ------ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  1016   Binary operation "." between numeric-string and mixed results in an error.
         🪪  binaryOp.invalid
  1020   Binary operation "." between mixed and '::' results in an error.
         🪪  binaryOp.invalid
  1020   Binary operation "." between non-falsy-string and mixed results in an error.
         🪪  binaryOp.invalid

I reverted back to what you had before in 6f23ca4, and this is now fixed.

If we wanted to use is_callable(), then the condition would have had to be as follows for PHPStan:

if ( ! is_callable( $callback, true ) || ! is_string( $callback[1] ) ) {

As I understand, what you had before was slightly more performant anyway, so I think it's a win-win.

@westonruter
Copy link
Copy Markdown
Member

Via 541dc90 I also added a missing null check in WP_Hook::add_filter() for the return value of _wp_filter_build_unique_id(). This fixes:

  91     Cannot access offset string|null on mixed.                                                                                        
         🪪  offsetAccess.nonOffsetAccessible                                                                                              
         at src/wp-includes/class-wp-hook.php:91  

This check was present WP_Hook::remove_filter():

$function_key = _wp_filter_build_unique_id( $hook_name, $callback, $priority );
$exists = isset( $function_key, $this->callbacks[ $priority ][ $function_key ] );
if ( $exists ) {

It was also present in ::has_filter():

$function_key = _wp_filter_build_unique_id( $hook_name, $callback, false );
if ( ! $function_key ) {
return false;
}

But it was missing in ::add_filter().

@bor0
Copy link
Copy Markdown
Member Author

bor0 commented May 20, 2026

Alright, thanks! Looks good 🙌

pento pushed a commit that referenced this pull request May 22, 2026
…of `spl_object_hash()` to construct unique IDs.

* Also use `spl_object_id()` similarly when registering and unregistering classic widgets.
* Improve typing and phpdoc in `_wp_filter_build_unique_id()`. Return `null` for malformed callbacks.
* Add tests for `_wp_filter_build_unique_id()`.
* Improve type safety of `WP_Hook::add_filter()` in case an invalid callback is provided for parity with `::has_filter()` and `::remove_filter()`.

Developed in #11865
Follow-up to r46220, r46801, r60179.

Props bor0, westonruter, SergeyBiryukov, schlessera, arshidkv12, knutsp, spacedmonkey, swissspidy.
See #64898.
Fixes #58291.


git-svn-id: https://develop.svn.wordpress.org/trunk@62408 602fd350-edb4-49c9-b593-d223f7449a82
@github-actions
Copy link
Copy Markdown

A commit was made that fixes the Trac ticket referenced in the description of this pull request.

SVN changeset: 62408
GitHub commit: 9560811

This PR will be closed, but please confirm the accuracy of this and reopen if there is more work to be done.

@github-actions github-actions Bot closed this May 22, 2026
markjaquith pushed a commit to markjaquith/WordPress that referenced this pull request May 22, 2026
…of `spl_object_hash()` to construct unique IDs.

* Also use `spl_object_id()` similarly when registering and unregistering classic widgets.
* Improve typing and phpdoc in `_wp_filter_build_unique_id()`. Return `null` for malformed callbacks.
* Add tests for `_wp_filter_build_unique_id()`.
* Improve type safety of `WP_Hook::add_filter()` in case an invalid callback is provided for parity with `::has_filter()` and `::remove_filter()`.

Developed in WordPress/wordpress-develop#11865
Follow-up to r46220, r46801, r60179.

Props bor0, westonruter, SergeyBiryukov, schlessera, arshidkv12, knutsp, spacedmonkey, swissspidy.
See #64898.
Fixes #58291.

Built from https://develop.svn.wordpress.org/trunk@62408


git-svn-id: http://core.svn.wordpress.org/trunk@61689 1a063a9b-81f0-0310-95a4-ce76da25c4cd
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants