<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[56595] trunk: Options, Meta APIs: Optimize get_option by relocating notoptions cache lookup.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/56595">56595</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/56595","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>spacedmonkey</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2023-09-15 16:13:52 +0000 (Fri, 15 Sep 2023)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Options, Meta APIs: Optimize get_option by relocating notoptions cache 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 <a href="https://core.trac.wordpress.org/ticket/58277">#58277</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesoptionphp">trunk/src/wp-includes/option.php</a></li>
<li><a href="#trunktestsphpunittestsoptionoptionphp">trunk/tests/phpunit/tests/option/option.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesoptionphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/option.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/option.php  2023-09-15 10:45:18 UTC (rev 56594)
+++ trunk/src/wp-includes/option.php    2023-09-15 16:13:52 UTC (rev 56595)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -161,33 +161,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">        $passed_default = func_num_args() > 1;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        if ( ! wp_installing() ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // 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 );
-               }
-
</del><span class="cx" style="display: block; padding: 0 10px">                 $alloptions = wp_load_alloptions();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( isset( $alloptions[ $option ] ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -196,6 +169,31 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $value = wp_cache_get( $option, 'options' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( false === $value ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                // 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 );
+                               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                 $row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $option ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                // Has to be get_row() instead of get_var() because of funkiness with 0, false, null values.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -203,10 +201,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        $value = $row->option_value;
</span><span class="cx" style="display: block; padding: 0 10px">                                        wp_cache_add( $option, $value, 'options' );
</span><span class="cx" style="display: block; padding: 0 10px">                                } else { // Option does not exist, so we must cache its non-existence.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        if ( ! is_array( $notoptions ) ) {
-                                               $notoptions = array();
-                                       }
-
</del><span class="cx" style="display: block; padding: 0 10px">                                         $notoptions[ $option ] = true;
</span><span class="cx" style="display: block; padding: 0 10px">                                        wp_cache_set( 'notoptions', $notoptions, 'options' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunktestsphpunittestsoptionoptionphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/option/option.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/option/option.php       2023-09-15 10:45:18 UTC (rev 56594)
+++ trunk/tests/phpunit/tests/option/option.php 2023-09-15 16:13:52 UTC (rev 56595)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -101,7 +101,63 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @ticket 58277
+        *
</ins><span class="cx" style="display: block; padding: 0 10px">          * @covers ::get_option
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         */
+       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
</ins><span class="cx" style="display: block; padding: 0 10px">          * @covers ::add_option
</span><span class="cx" style="display: block; padding: 0 10px">         * @covers ::delete_option
</span><span class="cx" style="display: block; padding: 0 10px">         * @covers ::update_option
</span></span></pre>
</div>
</div>

</body>
</html>