Site health:

- Prevent fatal errors from timeouts on the Tools => Site Health => Info tab.
- Use the `get_dirsize()` and `recurse_dirsize()` functions to calculate directory sizes. The results are cached.
- Introduce "timeout protection" in `recurse_dirsize()`.

Props pento, Clorith, xkon, afercia, jeremyfelt, azaozz.
Fixes #46645.

git-svn-id: https://develop.svn.wordpress.org/trunk@45104 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Andrew Ozz 2019-04-02 23:32:31 +00:00
parent f6f02a327f
commit d5d4979921
4 changed files with 159 additions and 142 deletions

View File

@ -32,7 +32,6 @@ class WP_Debug_Data {
*/
static function debug_data( $locale = null ) {
global $wpdb;
if ( ! empty( $locale ) ) {
// Change the language used for translations
if ( function_exists( 'switch_to_locale' ) ) {
@ -312,21 +311,33 @@ class WP_Debug_Data {
);
}
$size_db = WP_Debug_Data::get_database_size();
// Go through the various installation directories and calculate their sizes.
$uploads_dir = wp_upload_dir();
$inaccurate = false;
/*
* We will be using the PHP max execution time to prevent the size calculations
* from causing a timeout. We provide a default value of 30 seconds, as some
* from causing a timeout. The default value is 30 seconds, and some
* hosts do not allow you to read configuration values.
*/
$max_execution_time = 30;
$start_execution_time = microtime( true );
$max_execution_time = 30;
if ( function_exists( 'ini_get' ) ) {
$max_execution_time = ini_get( 'max_execution_time' );
}
// Here 20 seconds is a "sensible default" for how long to make the user wait for the directory size calculation.
// When testing 20 seconds seem enough in nearly all cases. The remaining edge cases are likely testing or development sites
// that have very large number of files, for example `node_modules` in plugins or themes, etc.
if ( $max_execution_time > 20 ) {
$max_execution_time = 20;
} elseif ( $max_execution_time > 2 ) {
// If the max_execution_time is set to lower than 20 seconds, reduce it a bit to prevent
// edge-case timeouts that may happen after the size loop has finished running.
$max_execution_time -= 1;
}
$size_directories = array(
'wordpress' => array(
'path' => ABSPATH,
@ -346,35 +357,44 @@ class WP_Debug_Data {
),
);
$timeout = __( 'The directory size calculation has timed out. Usually caused by a very large number of sub-directories and files.' );
$inaccessible = __( 'The size cannot be calculated. The directory is not accessible. Usually caused by invalid permissions.' );
$size_total = 0;
// Loop over all the directories we want to gather the sizes for.
foreach ( $size_directories as $size => $attributes ) {
/*
* We run a helper function with a RecursiveIterator, which
* may throw an exception if it can't access directories.
*
* If a failure is detected we mark the result as inaccurate.
*/
try {
$calculated_size = WP_Debug_data::get_directory_size( $attributes['path'], $max_execution_time, $start_execution_time );
$dir_size = null; // Default to timeout.
$size_directories[ $size ]['size'] = $calculated_size;
/*
* If the size returned is -1, this means execution has
* exceeded the maximum execution time, also denoting an
* inaccurate value in the end.
*/
if ( -1 === $calculated_size ) {
$inaccurate = true;
}
} catch ( Exception $e ) {
$inaccurate = true;
if ( microtime( true ) - WP_START_TIMESTAMP < $max_execution_time ) {
$dir_size = get_dirsize( $attributes['path'], $max_execution_time );
}
if ( $dir_size === false ) {
// Error reading
$dir_size = $inaccessible;
$size_total = null;
} elseif ( $dir_size === null ) {
// Timeout
$dir_size = $timeout;
$size_total = null;
} else {
$is_subdir = ( strpos( $size_directories[ $size ]['path'], ABSPATH ) === 0 );
if ( $size_total !== null && ( $size === 'wordpress' || ! $is_subdir ) ) {
$size_total += $dir_size;
}
$dir_size = size_format( $dir_size, 2 );
}
$size_directories[ $size ]['size'] = $dir_size;
}
$size_db = WP_Debug_Data::get_database_size();
$size_total = $size_directories['wordpress']['size'] + $size_db;
if ( $size_total !== null && $size_db > 0 ) {
$size_total = size_format( $size_total + $size_db, 2 );
} else {
$size_total = __( 'Total size is not available. Some errors were encountered when determining the size of your installation.' );
}
$info['wp-paths-sizes']['fields'] = array(
array(
@ -383,7 +403,7 @@ class WP_Debug_Data {
),
array(
'label' => __( 'Uploads Directory Size' ),
'value' => ( -1 === $size_directories['uploads']['size'] ? __( 'Unable to determine the size of this directory' ) : size_format( $size_directories['uploads']['size'], 2 ) ),
'value' => $size_directories['uploads']['size'],
),
array(
'label' => __( 'Themes Directory Location' ),
@ -395,7 +415,7 @@ class WP_Debug_Data {
),
array(
'label' => __( 'Themes Directory Size' ),
'value' => ( -1 === $size_directories['themes']['size'] ? __( 'Unable to determine the size of this directory' ) : size_format( $size_directories['themes']['size'], 2 ) ),
'value' => $size_directories['themes']['size'],
),
array(
'label' => __( 'Plugins Directory Location' ),
@ -403,7 +423,7 @@ class WP_Debug_Data {
),
array(
'label' => __( 'Plugins Directory Size' ),
'value' => ( -1 === $size_directories['plugins']['size'] ? __( 'Unable to determine the size of this directory' ) : size_format( $size_directories['plugins']['size'], 2 ) ),
'value' => $size_directories['plugins']['size'],
),
array(
'label' => __( 'WordPress Directory Location' ),
@ -411,7 +431,7 @@ class WP_Debug_Data {
),
array(
'label' => __( 'WordPress Directory Size' ),
'value' => size_format( $size_directories['wordpress']['size'], 2 ),
'value' => $size_directories['wordpress']['size'],
),
array(
'label' => __( 'Database size' ),
@ -419,11 +439,7 @@ class WP_Debug_Data {
),
array(
'label' => __( 'Total installation size' ),
'value' => sprintf(
'%s%s',
size_format( $size_total, 2 ),
( false === $inaccurate ? '' : __( '- Some errors, likely caused by invalid permissions, were encountered when determining the size of your installation. This means the values represented may be inaccurate.' ) )
),
'value' => $size_total,
),
);
@ -934,36 +950,6 @@ class WP_Debug_Data {
return $return;
}
/**
* Return the size of a directory, including all subdirectories.
*
* @since 5.2.0
*
* @param string $path The directory to check.
* @param string|int $max_execution_time How long a PHP script can run on this host.
* @param float $start_execution_time When we started executing this section of the script.
*
* @return int The directory size, in bytes.
*/
public static function get_directory_size( $path, $max_execution_time, $start_execution_time ) {
$size = 0;
foreach ( new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $path ) ) as $file ) {
// Check if the maximum execution time is a value considered "infinite".
if ( 0 !== $max_execution_time && -1 !== $max_execution_time ) {
$runtime = ( microtime( true ) - $start_execution_time );
// If the script has been running as long, or longer, as it is allowed, return a failure message.
if ( $runtime >= $max_execution_time ) {
return -1;
}
}
$size += $file->getSize();
}
return $size;
}
/**
* Fetch the total size of all the database tables for the active database user.
*
@ -982,6 +968,6 @@ class WP_Debug_Data {
}
}
return $size;
return (int) $size;
}
}

View File

@ -29,6 +29,11 @@ function wp_initial_constants() {
define( 'TB_IN_BYTES', 1024 * GB_IN_BYTES );
/**#@-*/
// Start of run timestamp.
if ( ! defined( 'WP_START_TIMESTAMP' ) ) {
define( 'WP_START_TIMESTAMP', microtime( true ) );
}
$current_limit = @ini_get( 'memory_limit' );
$current_limit_int = wp_convert_hr_to_bytes( $current_limit );

View File

@ -7006,3 +7006,103 @@ function wp_direct_php_update_button() {
);
echo '</p>';
}
/**
* Get the size of a directory.
*
* A helper function that is used primarily to check whether
* a blog has exceeded its allowed upload space.
*
* @since MU (3.0.0)
*
* @param string $directory Full path of a directory.
* @param int $max_execution_time Maximum time to run before giving up. In seconds.
* The timeout is global and is measured from the moment WordPress started to load.
* @return int|false|null Size in MB if a valid directory. False if not. Null if timeout.
*/
function get_dirsize( $directory, $max_execution_time = null ) {
$dirsize = get_transient( 'dirsize_cache' );
if ( is_array( $dirsize ) && isset( $dirsize[ $directory ]['size'] ) ) {
return $dirsize[ $directory ]['size'];
}
if ( ! is_array( $dirsize ) ) {
$dirsize = array();
}
// Exclude individual site directories from the total when checking the main site of a network
// as they are subdirectories and should not be counted.
if ( is_multisite() && is_main_site() ) {
$dirsize[ $directory ]['size'] = recurse_dirsize( $directory, $directory . '/sites', $max_execution_time );
} else {
$dirsize[ $directory ]['size'] = recurse_dirsize( $directory, null, $max_execution_time );
}
set_transient( 'dirsize_cache', $dirsize, HOUR_IN_SECONDS );
return $dirsize[ $directory ]['size'];
}
/**
* Get the size of a directory recursively.
*
* Used by get_dirsize() to get a directory's size when it contains
* other directories.
*
* @since MU (3.0.0)
* @since 4.3.0 $exclude parameter added.
*
* @param string $directory Full path of a directory.
* @param string $exclude Optional. Full path of a subdirectory to exclude from the total.
* @param int $max_execution_time Maximum time to run before giving up. In seconds.
* The timeout is global and is measured from the moment WordPress started to load.
* @return int|false|null Size in MB if a valid directory. False if not. Null if timeout.
*/
function recurse_dirsize( $directory, $exclude = null, $max_execution_time = null ) {
$size = 0;
$directory = untrailingslashit( $directory );
if ( ! file_exists( $directory ) || ! is_dir( $directory ) || ! is_readable( $directory ) || $directory === $exclude ) {
return false;
}
if ( ! $max_execution_time ) {
// Keep the previous behavior but attempt to prevent fatal errors from timeout.
if ( function_exists( 'ini_get' ) ) {
$max_execution_time = ini_get( 'max_execution_time' );
} else {
// Use PHP default.
$max_execution_time = 30;
}
// Leave 1 second "buffer" for other operations if $max_execution_time has reasonable value.
if ( $max_execution_time > 10 ) {
$max_execution_time -= 1;
}
}
if ( $handle = opendir( $directory ) ) {
while ( ( $file = readdir( $handle ) ) !== false ) {
$path = $directory . '/' . $file;
if ( $file != '.' && $file != '..' ) {
if ( is_file( $path ) ) {
$size += filesize( $path );
} elseif ( is_dir( $path ) ) {
$handlesize = recurse_dirsize( $path, $exclude, $max_execution_time );
if ( $handlesize > 0 ) {
$size += $handlesize;
}
}
if ( microtime( true ) - WP_START_TIMESTAMP > $max_execution_time ) {
// Time exceeded. Give up instead of risking a fatal timeout.
$size = null;
break;
}
}
}
closedir( $handle );
}
return $size;
}

View File

@ -1777,80 +1777,6 @@ function get_most_recent_post_of_user( $user_id ) {
// Misc functions
/**
* Get the size of a directory.
*
* A helper function that is used primarily to check whether
* a blog has exceeded its allowed upload space.
*
* @since MU (3.0.0)
*
* @param string $directory Full path of a directory.
* @return int Size of the directory in MB.
*/
function get_dirsize( $directory ) {
$dirsize = get_transient( 'dirsize_cache' );
if ( is_array( $dirsize ) && isset( $dirsize[ $directory ]['size'] ) ) {
return $dirsize[ $directory ]['size'];
}
if ( ! is_array( $dirsize ) ) {
$dirsize = array();
}
// Exclude individual site directories from the total when checking the main site,
// as they are subdirectories and should not be counted.
if ( is_main_site() ) {
$dirsize[ $directory ]['size'] = recurse_dirsize( $directory, $directory . '/sites' );
} else {
$dirsize[ $directory ]['size'] = recurse_dirsize( $directory );
}
set_transient( 'dirsize_cache', $dirsize, HOUR_IN_SECONDS );
return $dirsize[ $directory ]['size'];
}
/**
* Get the size of a directory recursively.
*
* Used by get_dirsize() to get a directory's size when it contains
* other directories.
*
* @since MU (3.0.0)
* @since 4.3.0 $exclude parameter added.
*
* @param string $directory Full path of a directory.
* @param string $exclude Optional. Full path of a subdirectory to exclude from the total.
* @return int|false Size in MB if a valid directory. False if not.
*/
function recurse_dirsize( $directory, $exclude = null ) {
$size = 0;
$directory = untrailingslashit( $directory );
if ( ! file_exists( $directory ) || ! is_dir( $directory ) || ! is_readable( $directory ) || $directory === $exclude ) {
return false;
}
if ( $handle = opendir( $directory ) ) {
while ( ( $file = readdir( $handle ) ) !== false ) {
$path = $directory . '/' . $file;
if ( $file != '.' && $file != '..' ) {
if ( is_file( $path ) ) {
$size += filesize( $path );
} elseif ( is_dir( $path ) ) {
$handlesize = recurse_dirsize( $path, $exclude );
if ( $handlesize > 0 ) {
$size += $handlesize;
}
}
}
}
closedir( $handle );
}
return $size;
}
/**
* Check an array of MIME types against a whitelist.
*