diff --git a/collectors/cache.php b/collectors/object-cache.php similarity index 78% rename from collectors/cache.php rename to collectors/object-cache.php index c05239451..fd863f5fd 100644 --- a/collectors/cache.php +++ b/collectors/object-cache.php @@ -12,9 +12,9 @@ /** * @extends QM_DataCollector */ -class QM_Collector_Cache extends QM_DataCollector { +class QM_Collector_Object_Cache extends QM_DataCollector { - public $id = 'cache'; + public $id = 'object-cache'; public function get_storage(): QM_Data { return new QM_Data_Cache(); @@ -26,8 +26,9 @@ public function get_storage(): QM_Data { public function process() { global $wp_object_cache; - $this->data->has_object_cache = (bool) wp_using_ext_object_cache(); + $this->data->has = (bool) wp_using_ext_object_cache(); $this->data->cache_hit_percentage = 0; + $this->data->cache_extensions = array(); if ( is_object( $wp_object_cache ) ) { $object_vars = get_object_vars( $wp_object_cache ); @@ -95,7 +96,7 @@ public function process() { $this->data->display_hit_rate_warning = ( 100 === $this->data->cache_hit_percentage ); if ( function_exists( 'extension_loaded' ) ) { - $this->data->object_cache_extensions = array_map( 'extension_loaded', array( + $this->data->cache_extensions = array_map( 'extension_loaded', array( 'Afterburner' => 'afterburner', 'Relay' => 'relay', 'Redis' => 'redis', @@ -103,16 +104,7 @@ public function process() { 'Memcache' => 'memcache', 'APCu' => 'apcu', ) ); - $this->data->opcode_cache_extensions = array_map( 'extension_loaded', array( - 'APC' => 'APC', - 'Zend OPcache' => 'Zend OPcache', - ) ); - } else { - $this->data->object_cache_extensions = array(); - $this->data->opcode_cache_extensions = array(); } - - $this->data->has_opcode_cache = array_filter( $this->data->opcode_cache_extensions ) ? true : false; } } @@ -122,9 +114,9 @@ public function process() { * @param QueryMonitor $qm * @return array */ -function register_qm_collector_cache( array $collectors, QueryMonitor $qm ) { - $collectors['cache'] = new QM_Collector_Cache(); +function register_qm_collector_object_cache( array $collectors, QueryMonitor $qm ) { + $collectors['object-cache'] = new QM_Collector_Object_Cache(); return $collectors; } -add_filter( 'qm/collectors', 'register_qm_collector_cache', 20, 2 ); +add_filter( 'qm/collectors', 'register_qm_collector_object_cache', 20, 2 ); diff --git a/collectors/opcode-cache.php b/collectors/opcode-cache.php new file mode 100644 index 000000000..f1813896c --- /dev/null +++ b/collectors/opcode-cache.php @@ -0,0 +1,171 @@ + + */ +class QM_Collector_Opcode_Cache extends QM_DataCollector { + + public $id = 'opcode-cache'; + + public function get_storage(): QM_Data { + return new QM_Data_Cache(); + } + + /** + * @return void + */ + public function process() { + $this->data->has = false; + $this->data->cache_hit_percentage = 0; + $this->data->cache_extensions = array(); + + if ( function_exists( 'extension_loaded' ) ) { + $this->data->cache_extensions = array_map( 'extension_loaded', array( + 'APC' => 'APC', + 'Zend OPcache' => 'Zend OPcache', + ) ); + } + + if ( isset( $this->data->cache_extensions['APC'] ) && $this->data->cache_extensions['APC'] ) { + $enabled = ini_get( 'apc.enabled' ); + $enabled = $enabled === '1' || $enabled === 'On'; + + if ( function_exists( 'apc_cache_info' ) ) { + $stats = apc_cache_info(); + if ( is_array( $stats ) ) { + $this->data->stats = $stats; + if ( isset( $this->data->stats['num_hits'] ) ) { + $this->data->stats['cache_hits'] = (int) $this->data->stats['num_hits']; + } + if ( isset( $stats['num_misses'] ) ) { + $this->data->stats['cache_misses'] = (int) $this->data->stats['num_misses']; + } + } + } + } else { + $enabled = ini_get( 'opcache.enable' ); + $enabled = $enabled === '1' || $enabled === 'On'; + + $restrict_api = ini_get( 'opcache.restrict_api' ); + $api_available = true; + if ( ! empty( $restrict_api ) ) { + $restrict_api = trailingslashit( $restrict_api ); + if ( strpos( __DIR__, $restrict_api ) !== 0 ) { + $api_available = false; + } + } + + if ( $enabled ) { + $memory_used = -1; + $memory_limit = QM_Util::convert_hr_to_bytes( ini_get( 'opcache.memory_consumption' ) . 'M' ); // memory_consumption is in MB + $this->data->usage_meters['opcode-memory'] = array( + 'type' => 'bytes', + 'used' => $memory_used, + 'limit' => $memory_limit, + 'label' => __( 'Memory', 'query-monitor' ), + ); + + $interned_strings_used = -1; + $interned_strings_limit = QM_Util::convert_hr_to_bytes( ini_get( 'opcache.interned_strings_buffer' ) . 'M' ); // interned_strings_buffer is in MB as well + $this->data->usage_meters['strings-memory'] = array( + 'type' => 'bytes', + 'used' => $interned_strings_used, + 'limit' => $interned_strings_limit, + 'label' => __( 'Interned Strings Memory', 'query-monitor' ), + ); + + $cached_files = -1; + $max_files = ini_get( 'opcache.max_accelerated_files' ); + // According to the documentation: The actual value used will be the first number in the set of prime numbers below which is greater than the specified value. + // https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.max-accelerated-files + $real_max_files = array( 223, 463, 983, 1979, 3907, 7963, 16229, 32531, 65407, 130987, 262237, 524521, 1048793 ); + foreach ( $real_max_files as $v ) { + if ( $max_files <= $v ) { + $real_max_files = $v; + break; + } + } + + $this->data->usage_meters['cached-scripts'] = array( + 'type' => 'count', + 'used' => $cached_files, + 'limit' => $real_max_files, + 'label' => __( 'Cached scripts', 'query-monitor' ), + ); + } + + if ( $enabled && $api_available && function_exists( 'opcache_get_status' ) ) { + $full_status = opcache_get_status( true ); + + if ( is_array( $full_status ) ) { + if ( isset( $full_status['opcache_statistics'] ) ) { + $this->data->stats = $full_status['opcache_statistics']; + + // Opcache stats are reflecting the hits/misses since the web server started. + // We would need to correlate with the included files to get a more accurate hit/miss count. + if ( function_exists( 'get_included_files' ) ) { + $files_included = get_included_files(); + $files_hit = count( array_intersect_key( $full_status['scripts'], array_flip( $files_included ) ) ); + $files_missed = count( $files_included ) - $files_hit; + + $this->data->stats['cache_hits'] = $files_hit; + $this->data->stats['cache_misses'] = $files_missed; + } + } + + if ( isset( $full_status['memory_usage'] ) ) { + $memory_used = (int) $full_status['memory_usage']['used_memory'] + (int) $full_status['memory_usage']['wasted_memory']; + $memory_free = (int) $full_status['memory_usage']['free_memory']; + $memory_limit = $memory_used + $memory_free; + $this->data->usage_meters['opcode-memory']['used'] = $memory_used; + $this->data->usage_meters['opcode-memory']['limit'] = $memory_limit; + } + + if ( isset( $full_status['interned_strings_usage'] ) ) { + $this->data->usage_meters['strings-memory']['used'] = (int) $full_status['interned_strings_usage']['used_memory']; + $this->data->usage_meters['strings-memory']['limit'] = (int) $full_status['interned_strings_usage']['buffer_size']; + } + + $cached_files = isset( $full_status['opcache_statistics']['num_cached_scripts'] ) ? (int) $full_status['opcache_statistics']['num_cached_scripts'] : 0; + + if ( $cached_files ) { + $this->data->usage_meters['cached-scripts']['used'] = $cached_files; + } + } + } + } + + $this->data->has = $enabled; + + if ( ! empty( $this->data->stats['cache_hits'] ) ) { + $total = $this->data->stats['cache_hits']; + + if ( ! empty( $this->data->stats['cache_misses'] ) ) { + $total += $this->data->stats['cache_misses']; + } + + $this->data->cache_hit_percentage = ( 100 / $total ) * $this->data->stats['cache_hits']; + } + } +} + +/** + * @param array $collectors + * @param QueryMonitor $qm + * @return array + */ +function register_qm_collector_opcode_cache( array $collectors, QueryMonitor $qm ) { + $collectors['opcode-cache'] = new QM_Collector_Opcode_Cache(); + return $collectors; +} + +add_filter( 'qm/collectors', 'register_qm_collector_opcode_cache', 20, 2 ); diff --git a/data/cache.php b/data/cache.php index 9234830e5..6b2b47d5a 100644 --- a/data/cache.php +++ b/data/cache.php @@ -9,18 +9,13 @@ class QM_Data_Cache extends QM_Data { /** * @var bool */ - public $has_object_cache; + public $has; /** * @var bool */ public $display_hit_rate_warning; - /** - * @var bool - */ - public $has_opcode_cache; - /** * @var int */ @@ -34,11 +29,11 @@ class QM_Data_Cache extends QM_Data { /** * @var array */ - public $object_cache_extensions; + public $cache_extensions; /** - * @var array + * @var array */ - public $opcode_cache_extensions; + public $usage_meters = array(); } diff --git a/output/html/overview.php b/output/html/overview.php index 5f4c4a0c0..569ee512c 100644 --- a/output/html/overview.php +++ b/output/html/overview.php @@ -52,8 +52,11 @@ public function output() { /** @var QM_Collector_Raw_Request|null $raw_request */ $raw_request = QM_Collectors::get( 'raw_request' ); - /** @var QM_Collector_Cache|null $cache */ - $cache = QM_Collectors::get( 'cache' ); + /** @var QM_Collector_Object_Cache|null $object_cache */ + $object_cache = QM_Collectors::get( 'object-cache' ); + + /** @var QM_Collector_Opcode_Cache|null $opcode_cache */ + $opcode_cache = QM_Collectors::get( 'opcode-cache' ); /** @var QM_Collector_HTTP|null $http */ $http = QM_Collectors::get( 'http' ); @@ -260,9 +263,9 @@ public function output() { echo '
'; echo '

' . esc_html__( 'Object Cache', 'query-monitor' ) . '

'; - if ( $cache ) { + if ( $object_cache ) { /** @var QM_Data_Cache $cache_data */ - $cache_data = $cache->get_data(); + $cache_data = $object_cache->get_data(); if ( ! empty( $cache_data->stats ) && ! empty( $cache_data->cache_hit_percentage ) ) { $cache_hit_percentage = $cache_data->cache_hit_percentage; @@ -278,7 +281,7 @@ public function output() { echo '

'; } - if ( $cache_data->has_object_cache ) { + if ( $cache_data->has ) { echo '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo self::build_link( @@ -293,7 +296,7 @@ public function output() { echo esc_html__( 'Persistent object cache plugin not in use', 'query-monitor' ); echo '

'; - $potentials = array_filter( $cache_data->object_cache_extensions ); + $potentials = array_filter( $cache_data->cache_extensions ); if ( ! empty( $potentials ) ) { foreach ( $potentials as $name => $value ) { @@ -334,15 +337,97 @@ public function output() { echo '
'; - if ( $cache ) { + echo '
'; + echo '

' . esc_html__( 'Opcode Cache', 'query-monitor' ) . '

'; + + if ( $opcode_cache ) { /** @var QM_Data_Cache $cache_data */ - $cache_data = $cache->get_data(); + $cache_data = $opcode_cache->get_data(); - echo '
'; - echo '

' . esc_html__( 'Opcode Cache', 'query-monitor' ) . '

'; + if ( ! empty( $cache_data->stats ) && ! empty( $cache_data->cache_hit_percentage ) ) { + $cache_hit_percentage = $cache_data->cache_hit_percentage; - if ( $cache_data->has_opcode_cache ) { - foreach ( array_filter( $cache_data->opcode_cache_extensions ) as $opcache_name => $opcache_state ) { + echo '

'; + echo esc_html( sprintf( + /* translators: 1: Cache hit rate percentage, 2: number of cache hits, 3: number of cache misses */ + __( '%1$s%% hit rate (%2$s hits, %3$s misses)', 'query-monitor' ), + number_format_i18n( $cache_hit_percentage, 1 ), + number_format_i18n( $cache_data->stats['cache_hits'], 0 ), + number_format_i18n( $cache_data->stats['cache_misses'], 0 ) + ) ); + echo '

'; + } elseif ( $cache_data->has ) { + echo '

'; + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo QueryMonitor::icon( 'warning' ); + echo esc_html__( 'Opcode cache statistics are not available', 'query-monitor' ); + echo '

'; + } + + foreach ( $cache_data->usage_meters as $meter_name => $meter ) { + echo '

'; + echo '' . esc_html( $meter['label'] ) . '
'; + switch ( $meter['type'] ) { + case 'bytes': + if ( $meter['used'] >= 0 ) { + echo esc_html( sprintf( + /* translators: 1: Memory used in bytes, 2: Memory used in megabytes */ + __( '%1$s bytes (%2$s MB)', 'query-monitor' ), + number_format_i18n( $meter['used'] ), + number_format_i18n( ( $meter['used'] / 1024 / 1024 ), 1 ) + ) ); + if ( $meter['limit'] >= 0 ) { + $usage_percentage = ( $meter['used'] / $meter['limit'] ) * 100; + echo '
'; + echo esc_html( sprintf( + /* translators: 1: Percentage of limit used, 2: Bytes meter server limit in megabytes */ + __( '%1$s%% of %2$s MB server limit', 'query-monitor' ), + number_format_i18n( $usage_percentage, 1 ), + number_format_i18n( $meter['limit'] / 1024 / 1024 ) + ) ); + echo ''; + } + } elseif ( $meter['limit'] >= 0 ) { + echo ''; + echo esc_html( sprintf( + /* translators: 1: Bytes meter server limit in megabytes */ + __( '%1$s MB server limit', 'query-monitor' ), + number_format_i18n( $meter['limit'] / 1024 / 1024 ) + ) ); + echo ''; + } + break; + case 'count': + if ( $meter['used'] >= 0 ) { + echo esc_html( number_format_i18n( $meter['used'] ) ); + + if ( $meter['limit'] >= 0 ) { + $usage_percentage = ( $meter['used'] / $meter['limit'] ) * 100; + echo '
'; + echo esc_html( sprintf( + /* translators: 1: Percentage of limit used, 2: Counter meter server limit */ + __( '%1$s%% of %2$s server limit', 'query-monitor' ), + number_format_i18n( $usage_percentage, 1 ), + number_format_i18n( $meter['limit'] ) + ) ); + echo ''; + } + } elseif ( $meter['limit'] >= 0 ) { + echo ''; + echo esc_html( sprintf( + /* translators: 1: Counter meter server limit */ + __( '%1$s server limit', 'query-monitor' ), + number_format_i18n( $meter['limit'] ) + ) ); + echo ''; + } + break; + } + echo '

'; + } + + if ( $cache_data->has ) { + foreach ( array_filter( $cache_data->cache_extensions ) as $opcache_name => $opcache_state ) { echo '

'; echo esc_html( sprintf( /* translators: %s: Name of cache driver */ @@ -357,14 +442,36 @@ public function output() { echo QueryMonitor::icon( 'warning' ); echo esc_html__( 'Opcode cache not in use', 'query-monitor' ); echo '

'; - echo '

'; - echo esc_html__( 'Speak to your web host about enabling an opcode cache such as OPcache.', 'query-monitor' ); - echo '

'; - } - echo '
'; + $potentials = array_filter( $cache_data->cache_extensions ); + + if ( ! empty( $potentials ) ) { + foreach ( $potentials as $name => $value ) { + echo '

'; + echo esc_html( + sprintf( + /* translators: 1: PHP extension name */ + __( 'The %1$s opcode extension for PHP is installed but is not enabled. Speak to your web host about enabling it.', 'query-monitor' ), + esc_html( $name ) + ) + ); + echo '

'; + break; + } + } else { + echo '

'; + echo esc_html__( 'Speak to your web host about installing and enabling an opcode cache such as OPcache.', 'query-monitor' ); + echo '

'; + } + } + } else { + echo '

'; + echo esc_html__( 'Opcode cache statistics are not available', 'query-monitor' ); + echo '

'; } + echo '
'; + $this->after_non_tabular_output(); } @@ -395,7 +502,6 @@ public function admin_title( array $title ) { return $title; } - } /** diff --git a/output/raw/cache.php b/output/raw/object-cache.php similarity index 70% rename from output/raw/cache.php rename to output/raw/object-cache.php index dd7aecbd6..1501b5d62 100644 --- a/output/raw/cache.php +++ b/output/raw/object-cache.php @@ -5,12 +5,12 @@ * @package query-monitor */ -class QM_Output_Raw_Cache extends QM_Output_Raw { +class QM_Output_Raw_Object_Cache extends QM_Output_Raw { /** * Collector instance. * - * @var QM_Collector_Cache Collector. + * @var QM_Collector_Object_Cache Collector. */ protected $collector; @@ -49,12 +49,12 @@ public function get_output() { * @param QM_Collectors $collectors * @return array */ -function register_qm_output_raw_cache( array $output, QM_Collectors $collectors ) { - $collector = QM_Collectors::get( 'cache' ); +function register_qm_output_raw_object_cache( array $output, QM_Collectors $collectors ) { + $collector = QM_Collectors::get( 'object-cache' ); if ( $collector ) { - $output['cache'] = new QM_Output_Raw_Cache( $collector ); + $output['object-cache'] = new QM_Output_Raw_Object_Cache( $collector ); } return $output; } -add_filter( 'qm/outputter/raw', 'register_qm_output_raw_cache', 30, 2 ); +add_filter( 'qm/outputter/raw', 'register_qm_output_raw_object_cache', 30, 2 );