<!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>[58332] trunk: Site Health: Add test for large autoloaded options.</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/58332">58332</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/58332","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>joemcgill</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2024-06-04 14:07:13 +0000 (Tue, 04 Jun 2024)</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'>Site Health: Add test for large autoloaded options.

This adds a new Site Health check that will alert site owners if they are autoloading a large amount of data from the options table, as it could result in poor performance. The issue will be shown if the size of autoloaded options is greater than 800 KB, which can be adjusted using the new `site_status_autoloaded_options_size_limit` filter.

Props mukesh27, joemcgill, rajinsharwar, costdev, audrasjb, krupajnanda, pooja1210, Ankit K Gupta, johnbillion, oglekler.
Fixes <a href="https://core.trac.wordpress.org/ticket/61276">#61276</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpadminincludesclasswpsitehealthphp">trunk/src/wp-admin/includes/class-wp-site-health.php</a></li>
<li><a href="#trunktestsphpunittestsadminwpSiteHealthphp">trunk/tests/phpunit/tests/admin/wpSiteHealth.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpadminincludesclasswpsitehealthphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/class-wp-site-health.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/class-wp-site-health.php      2024-06-04 13:49:31 UTC (rev 58331)
+++ trunk/src/wp-admin/includes/class-wp-site-health.php        2024-06-04 14:07:13 UTC (rev 58332)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2587,6 +2587,104 @@
</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">+         * Calculates total amount of autoloaded data.
+        *
+        * @since 6.6.0
+        *
+        * @return int Autoloaded data in bytes.
+        */
+       public function get_autoloaded_options_size() {
+               $alloptions = wp_load_alloptions();
+
+               $total_length = 0;
+
+               foreach ( $alloptions as $option_name => $option_value ) {
+                       $total_length += strlen( $option_value );
+               }
+
+               return $total_length;
+       }
+
+       /**
+        * Tests the number of autoloaded options.
+        *
+        * @since 6.6.0
+        *
+        * @return array The test results.
+        */
+       public function get_test_autoloaded_options() {
+               $autoloaded_options_size  = $this->get_autoloaded_options_size();
+               $autoloaded_options_count = count( wp_load_alloptions() );
+
+               $base_description = __( 'Autoloaded options are configuration settings for plugins and themes that are automatically loaded with every page load in WordPress. Having too many autoloaded options can slow down your site.' );
+
+               $result = array(
+                       'label'       => __( 'Autoloaded options are acceptable' ),
+                       'status'      => 'good',
+                       'badge'       => array(
+                               'label' => __( 'Performance' ),
+                               'color' => 'blue',
+                       ),
+                       'description' => sprintf(
+                               /* translators: 1: Number of autoloaded options, 2: Autoloaded options size. */
+                               '<p>' . esc_html( $base_description ) . ' ' . __( 'Your site has %1$s autoloaded options (size: %2$s) in the options table, which is acceptable.' ) . '</p>',
+                               $autoloaded_options_count,
+                               size_format( $autoloaded_options_size )
+                       ),
+                       'actions'     => '',
+                       'test'        => 'autoloaded_options',
+               );
+
+               /**
+                * Filters max bytes threshold to trigger warning in Site Health.
+                *
+                * @since 6.6.0
+                *
+                * @param int $limit Autoloaded options threshold size. Default 800000.
+                */
+               $limit = apply_filters( 'site_status_autoloaded_options_size_limit', 800000 );
+
+               if ( $autoloaded_options_size < $limit ) {
+                       return $result;
+               }
+
+               $result['status']      = 'critical';
+               $result['label']       = __( 'Autoloaded options could affect performance' );
+               $result['description'] = sprintf(
+                       /* translators: 1: Number of autoloaded options, 2: Autoloaded options size. */
+                       '<p>' . esc_html( $base_description ) . ' ' . __( 'Your site has %1$s autoloaded options (size: %2$s) in the options table, which could cause your site to be slow. You can review the options being autoloaded in your database and remove any options that are no longer needed by your site.' ) . '</p>',
+                       $autoloaded_options_count,
+                       size_format( $autoloaded_options_size )
+               );
+
+               /**
+                * Filters description to be shown on Site Health warning when threshold is met.
+                *
+                * @since 6.6.0
+                *
+                * @param string $description Description message when autoloaded options bigger than threshold.
+                */
+               $result['description'] = apply_filters( 'site_status_autoloaded_options_limit_description', $result['description'] );
+
+               $result['actions'] = sprintf(
+                       /* translators: 1: HelpHub URL, 2: Link description. */
+                       '<p><a target="_blank" rel="noopener" href="%1$s">%2$s</a></p>',
+                       esc_url( __( 'https://developer.wordpress.org/advanced-administration/performance/optimization/#autoloaded-options' ) ),
+                       __( 'More info about optimizing autoloaded options' )
+               );
+
+               /**
+                * Filters actionable information to tackle the problem. It can be a link to an external guide.
+                *
+                * @since 6.6.0
+                *
+                * @param string $actions Call to Action to be used to point to the right direction to solve the issue.
+                */
+               $result['actions'] = apply_filters( 'site_status_autoloaded_options_action_to_perform', $result['actions'] );
+               return $result;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Returns a set of tests that belong to the site status page.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * Each site status test is defined here, they may be `direct` tests, that run on page load, or `async` tests
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2670,6 +2768,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        'label' => __( 'Available disk space' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                        'test'  => 'available_updates_disk_space',
</span><span class="cx" style="display: block; padding: 0 10px">                                ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                'autoloaded_options' => array(
+                                       'label' => __( 'Autoloaded options' ),
+                                       'test'  => 'autoloaded_options',
+                               ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'async'  => array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'dotorg_communication' => array(
</span></span></pre></div>
<a id="trunktestsphpunittestsadminwpSiteHealthphp"></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/admin/wpSiteHealth.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/admin/wpSiteHealth.php  2024-06-04 13:49:31 UTC (rev 58331)
+++ trunk/tests/phpunit/tests/admin/wpSiteHealth.php    2024-06-04 14:07:13 UTC (rev 58332)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -499,4 +499,80 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        array( 'alloptions_bytes', 1000 ),
</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">+
+       /**
+        * Tests get_test_autoloaded_options() when autoloaded options less than warning size.
+        *
+        * @ticket 61276
+        *
+        * @covers ::get_test_autoloaded_options()
+        */
+       public function test_wp_autoloaded_options_test_no_warning() {
+               $expected_label  = esc_html__( 'Autoloaded options are acceptable' );
+               $expected_status = 'good';
+
+               $result = $this->instance->get_test_autoloaded_options();
+               $this->assertSame( $expected_label, $result['label'], 'The label should indicate that autoloaded options are acceptable.' );
+               $this->assertSame( $expected_status, $result['status'], 'The status should be "good" when autoloaded options are acceptable.' );
+       }
+
+       /**
+        * Tests get_test_autoloaded_options() when autoloaded options more than warning size.
+        *
+        * @ticket 61276
+        *
+        * @covers ::get_test_autoloaded_options()
+        */
+       public function test_wp_autoloaded_options_test_warning() {
+               self::set_autoloaded_option( 800000 );
+
+               $expected_label  = esc_html__( 'Autoloaded options could affect performance' );
+               $expected_status = 'critical';
+
+               $result = $this->instance->get_test_autoloaded_options();
+               $this->assertSame( $expected_label, $result['label'], 'The label should indicate that autoloaded options could affect performance.' );
+               $this->assertSame( $expected_status, $result['status'], 'The status should be "critical" when autoloaded options could affect performance.' );
+       }
+
+       /**
+        * Tests get_autoloaded_options_size().
+        *
+        * @ticket 61276
+        *
+        * @covers ::get_autoloaded_options_size()
+        */
+       public function test_get_autoloaded_options_size() {
+               global $wpdb;
+
+               $autoload_values = wp_autoload_values_to_autoload();
+
+               $autoloaded_options_size = (int) $wpdb->get_var(
+                       $wpdb->prepare(
+                               sprintf(
+                                       "SELECT SUM(LENGTH(option_value)) FROM $wpdb->options WHERE autoload IN (%s)",
+                                       implode( ',', array_fill( 0, count( $autoload_values ), '%s' ) )
+                               ),
+                               $autoload_values
+                       )
+               );
+               $this->assertSame( $autoloaded_options_size, $this->instance->get_autoloaded_options_size(), 'The size of autoloaded options should match the calculated size from the database.' );
+
+               // Add autoload option.
+               $test_option_string       = 'test';
+               $test_option_string_bytes = mb_strlen( $test_option_string, '8bit' );
+               self::set_autoloaded_option( $test_option_string_bytes );
+               $this->assertSame( $autoloaded_options_size + $test_option_string_bytes, $this->instance->get_autoloaded_options_size(), 'The size of autoloaded options should increase by the size of the newly added option.' );
+       }
+
+       /**
+        * Sets a test autoloaded option.
+        *
+        * @param int $bytes bytes to load in options.
+        */
+       public static function set_autoloaded_option( $bytes = 800000 ) {
+               $heavy_option_string = wp_generate_password( $bytes );
+
+               // Force autoloading so that WordPress core does not override it. See https://core.trac.wordpress.org/changeset/57920.
+               add_option( 'test_set_autoloaded_option', $heavy_option_string, '', true );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre>
</div>
</div>

</body>
</html>