substr( sanitize_text_field( $cpu ), 0, 80 ), 'os' => substr( sanitize_text_field( $os ), 0, 80 ), ); set_transient( 'velox_hw_info', $hw, DAY_IN_SECONDS ); return $hw; } /** * Best guess whether the site runs on shared hosting, where load and disk are the whole * machine's: open_basedir set or a CloudLinux kernel. Only changes the wording of the Server tab; * hosts can override the guess with the velox_shared_hosting filter. */ public static function shared_hosting() { $shared = '' !== (string) ini_get( 'open_basedir' ) || ( function_exists( 'php_uname' ) && false !== stripos( php_uname( 'r' ), 'lve' ) ); return (bool) apply_filters( 'velox_shared_hosting', $shared ); } private static function memory() { if ( ! is_readable( '/proc/meminfo' ) ) { return null; } $info = (string) @file_get_contents( '/proc/meminfo' ); if ( ! preg_match( '/^MemTotal:\s+(\d+)/m', $info, $t ) ) { return null; } $total = (int) $t[1] * 1024; $avail = preg_match( '/^MemAvailable:\s+(\d+)/m', $info, $a ) ? (int) $a[1] * 1024 : 0; return array( 'total' => $total, 'used' => max( 0, $total - $avail ), 'pct' => $total > 0 ? round( 100 * ( $total - $avail ) / $total ) : 0, ); } private static function disk() { $total = @disk_total_space( VELOX_DIR ); $free = @disk_free_space( VELOX_DIR ); if ( ! $total || false === $free ) { return null; } return array( 'total' => (float) $total, 'used' => (float) $total - (float) $free, 'pct' => round( 100 * ( $total - $free ) / $total ), ); } private static function opcache() { if ( ! function_exists( 'opcache_get_status' ) ) { return null; } $st = @opcache_get_status( false ); if ( ! is_array( $st ) || empty( $st['opcache_enabled'] ) ) { return array( 'enabled' => false ); } $mem = isset( $st['memory_usage'] ) ? $st['memory_usage'] : array(); $all = ( isset( $mem['used_memory'] ) ? $mem['used_memory'] : 0 ) + ( isset( $mem['free_memory'] ) ? $mem['free_memory'] : 0 ) + ( isset( $mem['wasted_memory'] ) ? $mem['wasted_memory'] : 0 ); return array( 'enabled' => true, 'hit' => isset( $st['opcache_statistics']['opcache_hit_rate'] ) ? round( (float) $st['opcache_statistics']['opcache_hit_rate'], 1 ) : null, 'mem_pct' => $all > 0 ? round( 100 * $mem['used_memory'] / $all ) : null, ); } /** Database size and its largest tables (MySQL / MariaDB; cached for an hour). */ private static function database() { $cached = get_transient( 'velox_db_size' ); if ( is_array( $cached ) ) { return $cached; } global $wpdb; $wpdb->suppress_errors( true ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- information_schema has no WP API; the result is cached in a transient for an hour. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT TABLE_NAME AS n, (DATA_LENGTH + INDEX_LENGTH) AS s FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s ORDER BY s DESC', DB_NAME ), ARRAY_A ); $wpdb->suppress_errors( false ); if ( ! is_array( $rows ) || ! $rows ) { $out = array(); } else { $total = 0; foreach ( $rows as $r ) { $total += (float) $r['s']; } $top = array(); foreach ( $total > 0 ? array_slice( $rows, 0, 5 ) : array() as $r ) { $top[] = array( 'name' => (string) $r['n'], 'size' => (float) $r['s'], ); } $out = $total <= 0 ? array() : array( 'size' => $total, 'tables' => count( $rows ), 'top' => $top, ); } set_transient( 'velox_db_size', $out, HOUR_IN_SECONDS ); return $out; } /** Current values for the Server tab. */ public static function now() { $cores = self::cores(); $load = function_exists( 'sys_getloadavg' ) ? @sys_getloadavg() : false; $cache = Velox_Cache::stats(); $hour = gmdate( 'YmdH' ); return array( 'cores' => $cores, 'load' => is_array( $load ) ? array_map( function ( $v ) { return round( (float) $v, 2 ); }, $load ) : null, 'load_pct' => is_array( $load ) ? (int) min( 100, round( 100 * $load[0] / $cores ) ) : null, 'memory' => self::memory(), 'disk' => self::disk(), 'opcache' => self::opcache(), 'hardware' => self::hardware(), 'php' => PHP_VERSION, 'php_mem' => (string) ini_get( 'memory_limit' ), 'software' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '', 'db' => self::database(), 'cache' => $cache, 'builds' => self::builds( $hour ), ); } /** Hourly snapshot (runs on the existing hourly AngelForce Cache event). */ public static function snapshot() { $n = self::now(); $prev = gmdate( 'YmdH', time() - HOUR_IN_SECONDS ); $history = get_option( self::OPTION, array() ); $history = is_array( $history ) ? $history : array(); $point = array( 't' => time(), 'load' => $n['load_pct'], 'mem' => $n['memory'] ? $n['memory']['pct'] : null, 'disk' => $n['disk'] ? $n['disk']['pct'] : null, 'builds' => self::builds( $prev ), 'pages' => (int) $n['cache']['pages'], ); $history[] = $point; $history = array_slice( $history, -self::KEEP ); update_option( self::OPTION, $history, false ); // Counter files older than two hours are no longer needed. foreach ( glob( self::meta_dir() . 'build-*.cnt' ) ?: array() as $f ) { if ( preg_match( '/build-(\d{10})\.cnt$/', $f, $m ) && $m[1] < $prev ) { wp_delete_file( $f ); } } } public static function history() { $h = get_option( self::OPTION, array() ); return is_array( $h ) ? $h : array(); } /** * Line chart of one history series over 7 days (inline SVG, no library). * * @param string $key Series key in the history points. * @param string $label Accessible label. * @param int|null $max Fixed top of the scale (null = from the data). * @param string $unit Unit shown in point titles. */ public static function chart_svg( $key, $label, $max = null, $unit = '' ) { $points = array_values( array_filter( self::history(), function ( $p ) use ( $key ) { return isset( $p[ $key ] ) && null !== $p[ $key ]; } ) ); if ( count( $points ) < 2 ) { return ''; } $w = 600; $h = 160; $pad = 14; $t0 = time() - 7 * DAY_IN_SECONDS; $span = 7 * DAY_IN_SECONDS; $vmax = null !== $max ? (float) $max : max( 1, max( wp_list_pluck( $points, $key ) ) * 1.15 ); $x = function ( $t ) use ( $w, $pad, $t0, $span ) { return round( $pad + max( 0, $t - $t0 ) * ( $w - 2 * $pad ) / $span, 1 ); }; $y = function ( $v ) use ( $h, $pad, $vmax ) { return round( $h - $pad - min( 1, $v / $vmax ) * ( $h - 2 * $pad ), 1 ); }; $line = ''; $area = ''; $first = null; $last = null; foreach ( $points as $p ) { $px = $x( $p['t'] ); $py = $y( (float) $p[ $key ] ); $line .= ( '' === $line ? 'M' : 'L' ) . $px . ' ' . $py . ' '; $first = null === $first ? $px : $first; $last = $px; } $area = $line . 'L' . $last . ' ' . ( $h - $pad ) . ' L' . $first . ' ' . ( $h - $pad ) . ' Z'; $svg = ''; foreach ( array( 0.25, 0.5, 0.75 ) as $g ) { $gy = $y( $vmax * $g ); $svg .= ''; } $svg .= ''; $svg .= ''; $tail = end( $points ); $svg .= '' . esc_html( wp_date( 'j M H:i', $tail['t'] ) . ': ' . $tail[ $key ] . $unit ) . ''; $svg .= '' . esc_html( round( $vmax ) . $unit ) . ''; // Day labels along the bottom (site time zone). for ( $d = 6; $d >= 0; $d-- ) { $dt = strtotime( 'today', time() - $d * DAY_IN_SECONDS ); $svg .= '' . esc_html( wp_date( 'D', $dt ) ) . ''; } $svg .= ''; return $svg; } public static function ajax_now() { check_ajax_referer( 'velox_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'forbidden' ), 403 ); } wp_send_json_success( self::now() ); } }