Skip to content

Commit

Permalink
Options, Meta APIs: Optimize get_option by relocating notoptions cach…
Browse files Browse the repository at this point in the history
…e lookup.

                    
In the get_option function, a cache lookup for the notoptions key is performed, which stores an array of keys for options known not to exist. This optimization prevents repeated database queries when certain options are requested. However, the cache lookup for notoptions was conducted before checking if the requested option exists in the cache. Given that it's more likely that the option does exist, this commit reorders the checks to first verify the option's existence in the cache before confirming its absence. This adjustment reduces redundant queries and also eliminates an unnecessary cache lookup, improving overall performance.

Props spacedmonkey, costdev, flixos90, azaozz.
Fixes #58277.

git-svn-id: https://develop.svn.wordpress.org/trunk@56595 602fd350-edb4-49c9-b593-d223f7449a82
  • Loading branch information
spacedmonkey committed Sep 15, 2023
1 parent 60d2d8a commit 0c7ddbd
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 31 deletions.
56 changes: 25 additions & 31 deletions src/wp-includes/option.php
Original file line number Diff line number Diff line change
Expand Up @@ -161,33 +161,6 @@ function get_option( $option, $default_value = false ) {
$passed_default = func_num_args() > 1;

if ( ! wp_installing() ) {
// Prevent non-existent options from triggering multiple queries.
$notoptions = wp_cache_get( 'notoptions', 'options' );

// Prevent non-existent `notoptions` key from triggering multiple key lookups.
if ( ! is_array( $notoptions ) ) {
$notoptions = array();
wp_cache_set( 'notoptions', $notoptions, 'options' );
}

if ( isset( $notoptions[ $option ] ) ) {
/**
* Filters the default value for an option.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 3.4.0
* @since 4.4.0 The `$option` parameter was added.
* @since 4.7.0 The `$passed_default` parameter was added to distinguish between a `false` value and the default parameter value.
*
* @param mixed $default_value The default value to return if the option does not exist
* in the database.
* @param string $option Option name.
* @param bool $passed_default Was `get_option()` passed a default value?
*/
return apply_filters( "default_option_{$option}", $default_value, $option, $passed_default );
}

$alloptions = wp_load_alloptions();

if ( isset( $alloptions[ $option ] ) ) {
Expand All @@ -196,17 +169,38 @@ function get_option( $option, $default_value = false ) {
$value = wp_cache_get( $option, 'options' );

if ( false === $value ) {
// Prevent non-existent options from triggering multiple queries.
$notoptions = wp_cache_get( 'notoptions', 'options' );

// Prevent non-existent `notoptions` key from triggering multiple key lookups.
if ( ! is_array( $notoptions ) ) {
$notoptions = array();
wp_cache_set( 'notoptions', $notoptions, 'options' );
} elseif ( isset( $notoptions[ $option ] ) ) {
/**
* Filters the default value for an option.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 3.4.0
* @since 4.4.0 The `$option` parameter was added.
* @since 4.7.0 The `$passed_default` parameter was added to distinguish between a `false` value and the default parameter value.
*
* @param mixed $default_value The default value to return if the option does not exist
* in the database.
* @param string $option Option name.
* @param bool $passed_default Was `get_option()` passed a default value?
*/
return apply_filters( "default_option_{$option}", $default_value, $option, $passed_default );
}

$row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $option ) );

// Has to be get_row() instead of get_var() because of funkiness with 0, false, null values.
if ( is_object( $row ) ) {
$value = $row->option_value;
wp_cache_add( $option, $value, 'options' );
} else { // Option does not exist, so we must cache its non-existence.
if ( ! is_array( $notoptions ) ) {
$notoptions = array();
}

$notoptions[ $option ] = true;
wp_cache_set( 'notoptions', $notoptions, 'options' );

Expand Down
56 changes: 56 additions & 0 deletions tests/phpunit/tests/option/option.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,62 @@ public function test_get_option_should_call_pre_option_filter() {
$this->assertSame( 1, $filter->get_call_count() );
}

/**
* @ticket 58277
*
* @covers ::get_option
*/
public function test_get_option_notoptions_cache() {
$notoptions = array(
'invalid' => true,
);
wp_cache_set( 'notoptions', $notoptions, 'options' );

$before = get_num_queries();
$value = get_option( 'invalid' );
$after = get_num_queries();

$this->assertSame( 0, $after - $before );
}

/**
* @ticket 58277
*
* @covers ::get_option
*/
public function test_get_option_notoptions_set_cache() {
get_option( 'invalid' );

$before = get_num_queries();
$value = get_option( 'invalid' );
$after = get_num_queries();

$notoptions = wp_cache_get( 'notoptions', 'options' );

$this->assertSame( 0, $after - $before, 'The notoptions cache was not hit on the second call to `get_option()`.' );
$this->assertIsArray( $notoptions, 'The notoptions cache should be set.' );
$this->assertArrayHasKey( 'invalid', $notoptions, 'The "invalid" option should be in the notoptions cache.' );
}

/**
* @ticket 58277
*
* @covers ::get_option
*/
public function test_get_option_notoptions_do_not_load_cache() {
add_option( 'foo', 'bar', '', 'no' );
wp_cache_delete( 'notoptions', 'options' );

$before = get_num_queries();
$value = get_option( 'foo' );
$after = get_num_queries();

$notoptions = wp_cache_get( 'notoptions', 'options' );

$this->assertSame( 0, $after - $before, 'The options cache was not hit on the second call to `get_option()`.' );
$this->assertFalse( $notoptions, 'The notoptions cache should not be set.' );
}

/**
* @covers ::get_option
* @covers ::add_option
Expand Down

0 comments on commit 0c7ddbd

Please sign in to comment.