<!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>[53955] trunk: Site Health: Introduce persistent object cache check.</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/53955">53955</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/53955","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>flixos90</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2022-08-29 16:52:12 +0000 (Mon, 29 Aug 2022)</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: Introduce persistent object cache check.

This changeset adds a new `persistent_object_cache` check which determines whether the site uses a persistent object cache, and if not, recommends it if it is beneficial for the site. A support resource to learn more about object caching has been created and is linked in the check.

A few filters are included for customization of the check, aimed primarily at hosting providers to provide more specific information in regards to their environment:

* `site_status_persistent_object_cache_url` filters the URL to learn more about object caching, so that e.g. a hosting-specific object caching support resource could be linked.
* `site_status_persistent_object_cache_notes` filters the notes added to the check description, so that more fine tuned information on object caching based on the environment can be provided.
* `site_status_should_suggest_persistent_object_cache` is a short-circuit filter which allows using entirely custom logic to determine whether a persistent object cache would make sense for the site.
* `site_status_persistent_object_cache_thresholds` filters the thresholds in the default logic to determine whether a persistent object cache would make sense for the site, which is based on the amount of data in the database.

Note that due to the nature of this check it is only run in production environments.

Props furi3r, tillkruss, spacedmonkey, audrasjb, Clorith.
Fixes <a href="https://core.trac.wordpress.org/ticket/56040">#56040</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="#trunktestsphpunittestssitehealthphp">trunk/tests/phpunit/tests/site-health.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      2022-08-29 14:05:14 UTC (rev 53954)
+++ trunk/src/wp-admin/includes/class-wp-site-health.php        2022-08-29 16:52:12 UTC (rev 53955)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2264,6 +2264,105 @@
</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">+         * Tests if sites uses persistent object cache.
+        *
+        * Checks if site uses persistent object cache or recommends to use it if not.
+        *
+        * @since 6.1.0
+        *
+        * @return array The test result.
+        */
+       public function get_test_persistent_object_cache() {
+               /**
+                * Filters the action URL for the persistent object cache health check.
+                *
+                * @since 6.1.0
+                *
+                * @param string $action_url Learn more link for persistent object cache health check.
+                */
+               $action_url = apply_filters(
+                       'site_status_persistent_object_cache_url',
+                       /* translators: Localized Support reference. */
+                       __( 'https://wordpress.org/support/article/optimization/#object-caching' )
+               );
+
+               $result = array(
+                       'test'        => 'persistent_object_cache',
+                       'status'      => 'good',
+                       'badge'       => array(
+                               'label' => __( 'Performance' ),
+                               'color' => 'blue',
+                       ),
+                       'label'       => __( 'A persistent object cache is being used' ),
+                       'description' => sprintf(
+                               '<p>%s</p>',
+                               __( "A persistent object cache makes your site's database more efficient, resulting in faster load times because WordPress can retrieve your site's content and settings much more quickly." )
+                       ),
+                       'actions'     => sprintf(
+                               '<p><a href="%s" target="_blank" rel="noopener">%s <span class="screen-reader-text">%s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
+                               esc_url( $action_url ),
+                               __( 'Learn more about persistent object caching.' ),
+                               /* translators: Accessibility text. */
+                               __( '(opens in a new tab)' )
+                       ),
+               );
+
+               if ( wp_using_ext_object_cache() ) {
+                       return $result;
+               }
+
+               if ( ! $this->should_suggest_persistent_object_cache() ) {
+                       $result['label'] = __( 'A persistent object cache is not required' );
+
+                       return $result;
+               }
+
+               $available_services = $this->available_object_cache_services();
+
+               $notes = __( 'Your hosting provider can tell you if a persistent object cache can be enabled on your site.' );
+
+               if ( ! empty( $available_services ) ) {
+                       $notes .= ' ' . sprintf(
+                               /* translators: Available object caching services. */
+                               __( 'Your host appears to support the following object caching services: %s.' ),
+                               implode( ', ', $available_services )
+                       );
+               }
+
+               /**
+                * Filters the second paragraph of the health check's description
+                * when suggesting the use of a persistent object cache.
+                *
+                * Hosts may want to replace the notes to recommend their preferred object caching solution.
+                *
+                * Plugin authors may want to append notes (not replace) on why object caching is recommended for their plugin.
+                *
+                * @since 6.1.0
+                *
+                * @param string $notes              The notes appended to the health check description.
+                * @param array  $available_services The list of available persistent object cache services.
+                */
+               $notes = apply_filters( 'site_status_persistent_object_cache_notes', $notes, $available_services );
+
+               $result['status']       = 'recommended';
+               $result['label']        = __( 'You should use a persistent object cache' );
+               $result['description'] .= sprintf(
+                       '<p>%s</p>',
+                       wp_kses(
+                               $notes,
+                               array(
+                                       'a'      => array( 'href' => true ),
+                                       'code'   => true,
+                                       'em'     => true,
+                                       'strong' => true,
+                               )
+                       )
+               );
+
+               return $result;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Return 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">@@ -2383,6 +2482,14 @@
</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">+                // Only check for a persistent object cache in production environments to not unnecessarily promote complicated setups.
+               if ( 'production' === wp_get_environment_type() ) {
+                       $tests['direct']['persistent_object_cache'] = array(
+                               'label' => __( 'Persistent object cache' ),
+                               'test'  => 'persistent_object_cache',
+                       );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><span class="cx" style="display: block; padding: 0 10px">                 * Add or modify which site status tests are run on a site.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2858,4 +2965,127 @@
</span><span class="cx" style="display: block; padding: 0 10px">                return in_array( wp_get_environment_type(), array( 'development', 'local' ), true );
</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">+        /**
+        * Determines whether to suggest using a persistent object cache.
+        *
+        * @since 6.1.0
+        *
+        * @global wpdb $wpdb WordPress database abstraction object.
+        *
+        * @return bool Whether to suggest using a persistent object cache.
+        */
+       public function should_suggest_persistent_object_cache() {
+               global $wpdb;
+
+               if ( is_multisite() ) {
+                       return true;
+               }
+
+               /**
+                * Filters whether to suggest use of a persistent object cache and bypass default threshold checks.
+                *
+                * Using this filter allows to override the default logic, effectively short-circuiting the method.
+                *
+                * @since 6.1.0
+                *
+                * @param bool|null $suggest Boolean to short-circuit, for whether to suggest using a persistent object cache.
+                *                           Default null.
+                */
+               $short_circuit = apply_filters( 'site_status_should_suggest_persistent_object_cache', null );
+               if ( is_bool( $short_circuit ) ) {
+                       return $short_circuit;
+               }
+
+               /**
+                * Filters the thresholds used to determine whether to suggest the use of a persistent object cache.
+                *
+                * @since 6.1.0
+                *
+                * @param array $thresholds The list of threshold names and numbers.
+                */
+               $thresholds = apply_filters(
+                       'site_status_persistent_object_cache_thresholds',
+                       array(
+                               'alloptions_count' => 500,
+                               'alloptions_bytes' => 100000,
+                               'comments_count'   => 1000,
+                               'options_count'    => 1000,
+                               'posts_count'      => 1000,
+                               'terms_count'      => 1000,
+                               'users_count'      => 1000,
+                       )
+               );
+
+               $alloptions = wp_load_alloptions();
+
+               if ( $thresholds['alloptions_count'] < count( $alloptions ) ) {
+                       return true;
+               }
+
+               if ( $thresholds['alloptions_bytes'] < strlen( serialize( $alloptions ) ) ) {
+                       return true;
+               }
+
+               $table_names = implode( "','", array( $wpdb->comments, $wpdb->options, $wpdb->posts, $wpdb->terms, $wpdb->users ) );
+
+               // With InnoDB the `TABLE_ROWS` are estimates, which are accurate enough and faster to retrieve than individual `COUNT()` queries.
+               $results = $wpdb->get_results(
+                       $wpdb->prepare(
+                               // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- This query cannot use interpolation.
+                               "SELECT TABLE_NAME AS 'table', TABLE_ROWS AS 'rows', SUM(data_length + index_length) as 'bytes' FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME IN ('$table_names') GROUP BY TABLE_NAME;",
+                               DB_NAME
+                       ),
+                       OBJECT_K
+               );
+
+               $threshold_map = array(
+                       'comments_count' => $wpdb->comments,
+                       'options_count'  => $wpdb->options,
+                       'posts_count'    => $wpdb->posts,
+                       'terms_count'    => $wpdb->terms,
+                       'users_count'    => $wpdb->users,
+               );
+
+               foreach ( $threshold_map as $threshold => $table ) {
+                       if ( $thresholds[ $threshold ] <= $results[ $table ]->rows ) {
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+
+       /**
+        * Returns a list of available persistent object cache services.
+        *
+        * @since 6.1.0
+        *
+        * @return array The list of available persistent object cache services.
+        */
+       private function available_object_cache_services() {
+               $extensions = array_map(
+                       'extension_loaded',
+                       array(
+                               'APCu'      => 'apcu',
+                               'Redis'     => 'redis',
+                               'Relay'     => 'relay',
+                               'Memcache'  => 'memcache',
+                               'Memcached' => 'memcached',
+                       )
+               );
+
+               $services = array_keys( array_filter( $extensions ) );
+
+               /**
+                * Filters the persistent object cache services available to the user.
+                *
+                * This can be useful to hide or add services not included in the defaults.
+                *
+                * @since 6.1.0
+                *
+                * @param array $services The list of available persistent object cache services.
+                */
+               return apply_filters( 'site_status_available_object_cache_services', $services );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunktestsphpunittestssitehealthphp"></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/site-health.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/site-health.php 2022-08-29 14:05:14 UTC (rev 53954)
+++ trunk/tests/phpunit/tests/site-health.php   2022-08-29 16:52:12 UTC (rev 53955)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -107,4 +107,75 @@
</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">+
+       /**
+        * @group ms-excluded
+        * @ticket 56040
+        */
+       public function test_object_cache_default_thresholds() {
+               $wp_site_health = new WP_Site_Health();
+
+               $this->assertFalse(
+                       $wp_site_health->should_suggest_persistent_object_cache()
+               );
+       }
+
+
+       /**
+        * @group ms-required
+        * @ticket 56040
+        */
+       public function test_object_cache_default_thresholds_on_multisite() {
+               $wp_site_health = new WP_Site_Health();
+               $this->assertTrue(
+                       $wp_site_health->should_suggest_persistent_object_cache()
+               );
+       }
+
+       /**
+        * @ticket 56040
+        */
+       public function test_object_cache_thresholds_check_can_be_bypassed() {
+               $wp_site_health = new WP_Site_Health();
+               add_filter( 'site_status_should_suggest_persistent_object_cache', '__return_true' );
+
+               $this->assertTrue(
+                       $wp_site_health->should_suggest_persistent_object_cache()
+               );
+       }
+
+       /**
+        * @dataProvider thresholds
+        * @ticket 56040
+        */
+       public function test_object_cache_thresholds( $threshold, $count ) {
+               $wp_site_health = new WP_Site_Health();
+               add_filter(
+                       'site_status_persistent_object_cache_thresholds',
+                       function ( $thresholds ) use ( $threshold, $count ) {
+                               return array_merge( $thresholds, array( $threshold => $count ) );
+                       }
+               );
+
+               $this->assertTrue(
+                       $wp_site_health->should_suggest_persistent_object_cache()
+               );
+       }
+
+       /**
+        * Data provider.
+        *
+        * @ticket 56040
+        */
+       public function thresholds() {
+               return array(
+                       array( 'comments_count', 0 ),
+                       array( 'posts_count', 0 ),
+                       array( 'terms_count', 1 ),
+                       array( 'options_count', 100 ),
+                       array( 'users_count', 0 ),
+                       array( 'alloptions_count', 100 ),
+                       array( 'alloptions_bytes', 1000 ),
+               );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre>
</div>
</div>

</body>
</html>