Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions src/wp-admin/includes/class-wp-site-health.php
Original file line number Diff line number Diff line change
Expand Up @@ -2253,6 +2253,105 @@ public function get_test_authorization_header() {
return $result;
}

/**
* 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 persistent object cache can be enabled on your site.' );

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.

To me this sounds incorrect.

Suggested change
$notes = __( 'Your hosting provider can tell you if persistent object cache can be enabled on your site.' );
$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 );
Comment thread
felixarntz marked this conversation as resolved.

$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;
}

/**
* Return a set of tests that belong to the site status page.
*
Expand Down Expand Up @@ -2373,6 +2472,14 @@ public static function get_tests() {
);
}

// Only check for a persistent object cache in production environments to not unnecessarily promote complicated setups.
if ( 'production' === wp_get_environment_type() ) {
Comment thread
manuelRod marked this conversation as resolved.
Comment thread
manuelRod marked this conversation as resolved.
$tests['direct']['persistent_object_cache'] = array(
Comment thread
felixarntz marked this conversation as resolved.
'label' => __( 'Persistent object cache' ),
'test' => 'persistent_object_cache',
);
}

/**
* Add or modify which site status tests are run on a site.
*
Expand Down Expand Up @@ -2848,4 +2955,123 @@ public function is_development_environment() {
return in_array( wp_get_environment_type(), array( 'development', 'local' ), true );
}

/**
* 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 to force suggestion to use a persistent object cache and bypass threshold checks.
*
* @since 6.1.0
*
* @param bool $suggest Whether to suggest using a persistent object cache.
*/
if ( apply_filters( 'site_status_suggest_persistent_object_cache', false ) ) {

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.

@manuelRod A bit late, but I am just realizing that this should rather follow a more appropriate short-circuiting behavior: Right now, the only way to short-circuit would be to actually recommend a persistent object cache. But it also needs to be possible to run custom logic that would result in not recommending a persistent object cache. So I think the default value here should be null, and any boolean result should cause the method to return early with that result.

This is a trivial change, which is compatible with the current behavior, it just expands it to also allow returning false to short-circuit.

return true;
}

/**
* 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 );
}

}
71 changes: 71 additions & 0 deletions tests/phpunit/tests/site-health.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,75 @@ public function data_cron_health_checks() {
),
);
}

/**
* @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_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 ),
);
}
}