diff --git a/lib/php-analyzer.sh b/lib/php-analyzer.sh index ee98909..9988ff4 100644 --- a/lib/php-analyzer.sh +++ b/lib/php-analyzer.sh @@ -8,6 +8,29 @@ _LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$_LIB_DIR/php-detector.sh" 2>/dev/null || { echo "ERROR: php-detector.sh not found"; return 1; } source "$_LIB_DIR/system-detect.sh" 2>/dev/null || { echo "ERROR: system-detect.sh not found"; return 1; } +# ============================================================================ +# HELPER FUNCTIONS - PURE BASH OPTIMIZATIONS +# ============================================================================ + +# Extract field from pipe-delimited string (pure bash - no cut command) +# Usage: get_field "value1|value2|value3" 2 +# Returns: value2 +get_field() { + local input="$1" + local field_num="$2" + local temp="$input" + local i=1 + + # Skip to the desired field + while [ $i -lt "$field_num" ]; do + temp="${temp#*|}" + i=$((i + 1)) + done + + # Extract the field + echo "${temp%%|*}" +} + # ============================================================================ # ERROR LOG ANALYSIS # ============================================================================ @@ -232,8 +255,8 @@ calculate_optimal_max_children() { memory_stats=$(calculate_memory_per_process "$username") local avg_kb process_count - avg_kb=$(echo "$memory_stats" | cut -d'|' -f1) - process_count=$(echo "$memory_stats" | cut -d'|' -f2) + avg_kb=$(get_field "$memory_stats" 1) + process_count=$(get_field "$memory_stats" 2) if [ "$avg_kb" -eq 0 ]; then echo "0|No active processes to measure" @@ -251,7 +274,11 @@ calculate_optimal_max_children() { local avg_mb=$((avg_kb / 1024)) # Calculate max children (with 20% safety buffer) - local theoretical_max=$((available_mb / avg_mb)) + # Protect against division by zero + local theoretical_max=5 + if [ "$avg_mb" -gt 0 ]; then + theoretical_max=$((available_mb / avg_mb)) + fi local recommended=$((theoretical_max * 80 / 100)) # Sanity checks @@ -348,6 +375,160 @@ calculate_avg_requests_per_minute() { echo "$avg_per_min|Last $hours hours" } +# Advanced per-domain traffic analysis with 7-day patterns and bot filtering +# Usage: analyze_domain_traffic_advanced [days] +# Returns: avg_rpm|peak_rpm|95th_percentile|total_requests|legit_percentage|bot_percentage +analyze_domain_traffic_advanced() { + local domain="$1" + local username="$2" + local days="${3:-7}" + + # Source bot signatures if not already loaded + if [ -z "${LEGIT_BOTS+x}" ]; then + local lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + source "$lib_dir/bot-signatures.sh" 2>/dev/null || { + echo "0|0|0|0|100|0|ERROR: bot-signatures.sh not found" + return 1 + } + fi + + # Find domain access logs (cPanel format) + local access_log="/home/$username/access-logs/$domain" + + # Fallback to other common locations + if [ ! -f "$access_log" ]; then + access_log=$(find /home/"$username"/*/logs -name "$domain*access*log" 2>/dev/null | head -1) + fi + + if [ ! -f "$access_log" ]; then + echo "0|0|0|0|100|0|No access log found for $domain" + return 1 + fi + + # Calculate time window cutoff date (YYYYMMDD format for fast string comparison) + local cutoff_date=$(date -d "$days days ago" +%Y%m%d 2>/dev/null || date -v-${days}d +%Y%m%d 2>/dev/null) + + # Arrays to store per-minute request counts and categorization + declare -A requests_per_minute + local total_requests=0 + local legit_requests=0 + local bot_requests=0 + local skipped_old=0 + + # Process access log + # Apache log format: IP - - [DD/Mon/YYYY:HH:MM:SS +ZONE] "REQUEST" status size "referer" "user-agent" + # OPTIMIZED: Pure bash parsing - no external commands in loop + while IFS= read -r line; do + # Extract timestamp using pure bash - format: [DD/Mon/YYYY:HH:MM:SS +ZONE] + # Find position of '[' and ']' + [[ "$line" =~ \[([^]]+)\] ]] || continue + local timestamp_raw="${BASH_REMATCH[1]}" + + # Parse: DD/Mon/YYYY:HH:MM:SS +ZONE → extract components + # Using bash parameter expansion (no external commands!) + local date_part="${timestamp_raw%%:*}" # DD/Mon/YYYY + local time_part="${timestamp_raw#*:}" # HH:MM:SS +ZONE + time_part="${time_part%% *}" # HH:MM:SS (remove timezone) + + # Parse date: DD/Mon/YYYY + local day="${date_part%%/*}" # DD + local temp="${date_part#*/}" # Mon/YYYY + local mon="${temp%%/*}" # Mon + local year="${temp#*/}" # YYYY + + # Parse time: HH:MM:SS + local hour="${time_part%%:*}" # HH + local temp2="${time_part#*:}" # MM:SS + local minute="${temp2%%:*}" # MM + + # Convert month name to number (no change - this is already optimal) + case "$mon" in + Jan) month="01" ;; Feb) month="02" ;; Mar) month="03" ;; + Apr) month="04" ;; May) month="05" ;; Jun) month="06" ;; + Jul) month="07" ;; Aug) month="08" ;; Sep) month="09" ;; + Oct) month="10" ;; Nov) month="11" ;; Dec) month="12" ;; + *) continue ;; + esac + + # Create minute bucket: YYYYMMDDHHMM + local minute_bucket="${year}${month}${day}${hour}${minute}" + + # Skip old entries by comparing YYYYMMDD strings (fast, no date command needed) + local log_date="${year}${month}${day}" + if [ "$log_date" -lt "$cutoff_date" ]; then + skipped_old=$((skipped_old + 1)) + continue + fi + + # Extract user agent using pure bash regex (last quoted string in line) + local user_agent="" + if [[ "$line" =~ \"([^\"]+)\"[[:space:]]*$ ]]; then + user_agent="${BASH_REMATCH[1]}" + fi + + total_requests=$((total_requests + 1)) + requests_per_minute[$minute_bucket]=$((${requests_per_minute[$minute_bucket]:-0} + 1)) + + # Classify traffic (legitimate vs bot) - optimized with short-circuit + # Check most common bots first to avoid unnecessary function calls + local is_bot=0 + + # Quick common bot check using string matching (faster than function calls) + if [[ "$user_agent" == *"bot"* ]] || [[ "$user_agent" == *"Bot"* ]] || \ + [[ "$user_agent" == *"crawler"* ]] || [[ "$user_agent" == *"spider"* ]] || \ + [[ "$user_agent" == *"Google"* ]] || [[ "$user_agent" == *"Bing"* ]]; then + is_bot=1 + fi + + if [ "$is_bot" -eq 1 ]; then + bot_requests=$((bot_requests + 1)) + else + legit_requests=$((legit_requests + 1)) + fi + + done < <(tail -50000 "$access_log" 2>/dev/null) # Limit to last 50k lines for performance + + # If no requests found + if [ "$total_requests" -eq 0 ]; then + echo "0|0|0|0|100|0|No requests in last $days days" + return 0 + fi + + # Calculate statistics + local total_minutes=${#requests_per_minute[@]} + [ "$total_minutes" -eq 0 ] && total_minutes=1 + + local avg_rpm=$((total_requests / total_minutes)) + + # Calculate peak and 95th percentile + local -a rpm_values=() + for minute in "${!requests_per_minute[@]}"; do + rpm_values+=("${requests_per_minute[$minute]}") + done + + # Sort values + IFS=$'\n' rpm_sorted=($(sort -n <<<"${rpm_values[*]}")) + unset IFS + + local peak_rpm=${rpm_sorted[-1]:-0} + + # Calculate 95th percentile index + local count=${#rpm_sorted[@]} + local percentile_95_index=$(( count * 95 / 100 )) + [ "$percentile_95_index" -ge "$count" ] && percentile_95_index=$((count - 1)) + local rpm_95th=${rpm_sorted[$percentile_95_index]:-0} + + # Calculate percentages + local legit_pct=100 + local bot_pct=0 + if [ "$total_requests" -gt 0 ]; then + legit_pct=$((legit_requests * 100 / total_requests)) + bot_pct=$((bot_requests * 100 / total_requests)) + fi + + echo "$avg_rpm|$peak_rpm|$rpm_95th|$total_requests|$legit_pct|$bot_pct" +} + # ============================================================================ # OPCACHE ANALYSIS # ============================================================================ @@ -459,7 +640,7 @@ detect_php_config_issues() { local max_children_errors max_children_errors=$(analyze_max_children_errors "$username" 7) local error_count - error_count=$(echo "$max_children_errors" | grep "TOTAL" | cut -d'|' -f1) + error_count=$(get_field "$(echo "$max_children_errors" | grep "TOTAL")" 1) if [ -n "$error_count" ] && [ "$error_count" -gt 0 ]; then issues+="CAPACITY|CRITICAL|pm.max_children limit reached $error_count times in last 7 days|Increase pm.max_children setting"$'\n' @@ -469,7 +650,7 @@ detect_php_config_issues() { local memory_errors memory_errors=$(analyze_memory_exhausted_errors "$username" 7) local memory_error_count - memory_error_count=$(echo "$memory_errors" | grep "TOTAL" | cut -d'|' -f1) + memory_error_count=$(get_field "$(echo "$memory_errors" | grep "TOTAL")" 1) if [ "$memory_error_count" -gt 0 ]; then issues+="MEMORY|HIGH|Memory exhausted errors occurred $memory_error_count times in last 7 days|Increase memory_limit or optimize code"$'\n' @@ -479,8 +660,8 @@ detect_php_config_issues() { local opcache_status opcache_status=$(analyze_opcache_effectiveness "$username") local status hit_rate - status=$(echo "$opcache_status" | cut -d'|' -f1) - hit_rate=$(echo "$opcache_status" | cut -d'|' -f2) + status=$(get_field "$opcache_status" 1) + hit_rate=$(get_field "$opcache_status" 2) if [ "$status" = "DISABLED" ]; then issues+="PERFORMANCE|HIGH|OPcache is disabled|Enable OPcache for 40-70% performance improvement"$'\n' @@ -598,9 +779,9 @@ analyze_domain_php() { local memory_stats memory_stats=$(calculate_memory_per_process "$username") local avg_kb process_count total_mb - avg_kb=$(echo "$memory_stats" | cut -d'|' -f1) - process_count=$(echo "$memory_stats" | cut -d'|' -f2) - total_mb=$(echo "$memory_stats" | cut -d'|' -f3) + avg_kb=$(get_field "$memory_stats" 1) + process_count=$(get_field "$memory_stats" 2) + total_mb=$(get_field "$memory_stats" 3) echo " Current Processes: $process_count" echo " Avg Memory/Process: $((avg_kb / 1024))MB" @@ -612,11 +793,11 @@ analyze_domain_php() { local opcache_status opcache_status=$(analyze_opcache_effectiveness "$username") local status hit_rate memory_used cached_scripts recommendation - status=$(echo "$opcache_status" | cut -d'|' -f1) - hit_rate=$(echo "$opcache_status" | cut -d'|' -f2) - memory_used=$(echo "$opcache_status" | cut -d'|' -f3) - cached_scripts=$(echo "$opcache_status" | cut -d'|' -f4) - recommendation=$(echo "$opcache_status" | cut -d'|' -f5) + status=$(get_field "$opcache_status" 1) + hit_rate=$(get_field "$opcache_status" 2) + memory_used=$(get_field "$opcache_status" 3) + cached_scripts=$(get_field "$opcache_status" 4) + recommendation=$(get_field "$opcache_status" 5) echo " Status: $status" if [ "$status" = "ENABLED" ]; then @@ -641,10 +822,10 @@ analyze_domain_php() { echo "ERROR ANALYSIS (Last 7 days):" local memory_errors max_children_errors timeout_errors slow_requests - memory_errors=$(analyze_memory_exhausted_errors "$username" 7 | grep "TOTAL" | cut -d'|' -f1) - max_children_errors=$(analyze_max_children_errors "$username" 7 | grep "TOTAL" | cut -d'|' -f1) - timeout_errors=$(analyze_execution_timeout_errors "$username" 7 | grep "TOTAL" | cut -d'|' -f1) - slow_requests=$(analyze_slow_requests "$username" 7 5 | grep "TOTAL" | cut -d'|' -f1) + memory_errors=$(get_field "$(analyze_memory_exhausted_errors "$username" 7 | grep "TOTAL")" 1) + max_children_errors=$(get_field "$(analyze_max_children_errors "$username" 7 | grep "TOTAL")" 1) + timeout_errors=$(get_field "$(analyze_execution_timeout_errors "$username" 7 | grep "TOTAL")" 1) + slow_requests=$(get_field "$(analyze_slow_requests "$username" 7 5 | grep "TOTAL")" 1) echo " Memory Exhausted: $memory_errors" echo " Max Children Reached: $max_children_errors" @@ -671,8 +852,8 @@ analyze_domain_php() { local optimal_max_children optimal_max_children=$(calculate_optimal_max_children "$username" 1024) local recommended reason - recommended=$(echo "$optimal_max_children" | cut -d'|' -f1) - reason=$(echo "$optimal_max_children" | cut -d'|' -f2) + recommended=$(get_field "$optimal_max_children" 1) + reason=$(get_field "$optimal_max_children" 2) local current_max_children if [ -n "$pool_config" ] && [ -f "$pool_config" ]; then @@ -786,7 +967,7 @@ calculate_server_memory_capacity() { local avg_kb=0 local memory_stats memory_stats=$(calculate_memory_per_process "$username") - avg_kb=$(echo "$memory_stats" | cut -d'|' -f1) + avg_kb=$(get_field "$memory_stats" 1) if [ -z "$avg_kb" ] || [ "$avg_kb" -eq 0 ]; then # No active processes, estimate 50MB per process (conservative) @@ -806,6 +987,17 @@ calculate_server_memory_capacity() { done <<< "$user_domains" done <<< "$users" + # Add MySQL memory usage to total + local mysql_memory_mb=0 + local mysql_status + local mysql_info + mysql_info=$(detect_mysql_memory_usage 2>/dev/null) + if [ $? -eq 0 ]; then + mysql_memory_mb=$(echo "$mysql_info" | cut -d'|' -f3) + mysql_status=$(echo "$mysql_info" | cut -d'|' -f4) + total_required_mb=$((total_required_mb + mysql_memory_mb)) + fi + # Calculate percentage local percentage=$((total_required_mb * 100 / total_ram_mb)) @@ -822,7 +1014,11 @@ calculate_server_memory_capacity() { fi # Return formatted result - first line is summary - echo "$total_required_mb|$total_ram_mb|$percentage|$status|$pool_count pools|$total_max_children total max_children" + if [ "$mysql_memory_mb" -gt 0 ]; then + echo "$total_required_mb|$total_ram_mb|$percentage|$status|$pool_count pools|$total_max_children max_children|MySQL: ${mysql_memory_mb}MB" + else + echo "$total_required_mb|$total_ram_mb|$percentage|$status|$pool_count pools|$total_max_children max_children" + fi # Return details as additional lines (not to stderr) echo "$details" @@ -843,6 +1039,15 @@ calculate_balanced_memory_allocation() { reserved_mb=$((total_ram_mb * 20 / 100)) [ "$reserved_mb" -lt 2048 ] && reserved_mb=2048 + # Account for MySQL memory usage + local mysql_memory_mb=0 + local mysql_info + mysql_info=$(detect_mysql_memory_usage 2>/dev/null) + if [ $? -eq 0 ]; then + mysql_memory_mb=$(echo "$mysql_info" | cut -d'|' -f3) + reserved_mb=$((reserved_mb + mysql_memory_mb)) + fi + local available_mb=$((total_ram_mb - reserved_mb)) # Get all users with FPM pools @@ -875,16 +1080,21 @@ calculate_balanced_memory_allocation() { pool_count=$((pool_count + 1)) pool_config_file[$username]="$pool_config" - # Get current max_children - local max_children - max_children=$(grep "^pm.max_children" "$pool_config" | awk -F'=' '{print $2}' | tr -d ' ') + # Get current max_children (pure bash - no external commands) + local max_children="" + while IFS='=' read -r key value; do + if [[ "$key" =~ ^[[:space:]]*pm\.max_children ]]; then + max_children="${value// /}" # Remove all spaces + break + fi + done < "$pool_config" pool_max[$username]=$max_children # Get average memory per process local memory_stats memory_stats=$(calculate_memory_per_process "$username") - local avg_kb - avg_kb=$(echo "$memory_stats" | cut -d'|' -f1) + # Pure bash field extraction (no cut command) + local avg_kb="${memory_stats%%|*}" if [ "$avg_kb" -eq 0 ]; then avg_kb=$((50 * 1024)) # Default 50MB @@ -892,9 +1102,11 @@ calculate_balanced_memory_allocation() { pool_memory[$username]=$((avg_kb / 1024)) - # Get traffic stats + # Get traffic stats (pure bash field extraction) local traffic - traffic=$(calculate_avg_requests_per_minute "$username" 24 2>/dev/null | cut -d'|' -f1 || echo "1") + local traffic_result + traffic_result=$(calculate_avg_requests_per_minute "$username" 24 2>/dev/null || echo "1") + traffic="${traffic_result%%|*}" # Extract first field without cut [ "$traffic" -eq 0 ] && traffic=1 # Minimum 1 req/min pool_traffic[$username]=$traffic @@ -916,11 +1128,20 @@ calculate_balanced_memory_allocation() { local current_max=${pool_max[$username]} # Calculate proportional share of available memory based on traffic - local traffic_percentage=$((traffic * 100 / total_traffic)) + local traffic_percentage + if [ "$total_traffic" -gt 0 ]; then + traffic_percentage=$((traffic * 100 / total_traffic)) + else + traffic_percentage=$((100 / pool_count)) # Equal distribution if no traffic + fi local allocated_mb=$((available_mb * traffic_percentage / 100)) # Calculate recommended max_children for this allocation - local recommended_max=$((allocated_mb / avg_mb)) + # Protect against division by zero + local recommended_max=5 + if [ "$avg_mb" -gt 0 ]; then + recommended_max=$((allocated_mb / avg_mb)) + fi # Apply limits [ "$recommended_max" -lt 5 ] && recommended_max=5 # Minimum 5 @@ -940,6 +1161,278 @@ calculate_balanced_memory_allocation() { done } +# Calculate optimal memory allocation per DOMAIN (cPanel-specific) +# Usage: calculate_balanced_memory_allocation_per_domain +# Returns: recommendations for each domain to fit within system limits +calculate_balanced_memory_allocation_per_domain() { + echo "Calculating per-domain balanced memory allocation (cPanel)..." >&2 + + # Verify this is cPanel + if [ "$SYS_CONTROL_PANEL" != "cpanel" ]; then + echo "ERROR|This function only supports cPanel. Use calculate_balanced_memory_allocation for other panels." + return 1 + fi + + # Get total system memory + local total_ram_mb + total_ram_mb=$(free -m | awk '/^Mem:/ {print $2}') + + # Reserve memory for system (minimum 2GB or 20% of RAM, whichever is higher) + local reserved_mb + reserved_mb=$((total_ram_mb * 20 / 100)) + [ "$reserved_mb" -lt 2048 ] && reserved_mb=2048 + + # Account for MySQL memory usage + local mysql_memory_mb=0 + local mysql_info + mysql_info=$(detect_mysql_memory_usage 2>/dev/null) + if [ $? -eq 0 ]; then + mysql_memory_mb=$(echo "$mysql_info" | cut -d'|' -f3) + reserved_mb=$((reserved_mb + mysql_memory_mb)) + fi + + local available_mb=$((total_ram_mb - reserved_mb)) + + # Get all users + local users + users=$(list_all_users) + + if [ -z "$users" ]; then + echo "ERROR|No users found" + return 1 + fi + + # Collect per-domain pool data + declare -A domain_traffic # Avg requests per minute + declare -A domain_memory # Avg MB per process + declare -A domain_max # Current max_children + declare -A domain_pool_config # Pool config path + declare -A domain_username # Username for domain + declare -A domain_php_version # PHP version + + local total_traffic=0 + local pool_count=0 + + # Iterate through all users and their domains + while IFS= read -r username; do + [ -z "$username" ] && continue + + # Get all domains for this user + local user_domains + user_domains=$(get_user_domains "$username") + + while IFS= read -r domain; do + [ -z "$domain" ] && continue + + # Detect PHP version for this domain + local php_version + php_version=$(detect_php_version_for_domain "$domain" "$username") + + # Find pool config (domain-specific for cPanel) + local pool_config + pool_config=$(find_fpm_pool_config "$username" "$domain" "$php_version") + + # Skip if no pool config found (domain may not use FPM) + [ -z "$pool_config" ] || [ ! -f "$pool_config" ] && continue + + pool_count=$((pool_count + 1)) + domain_pool_config[$domain]="$pool_config" + domain_username[$domain]="$username" + domain_php_version[$domain]="$php_version" + + # Get current max_children + local max_children + # Read max_children (pure bash - no external commands) + max_children="" + while IFS='=' read -r key value; do + if [[ "$key" =~ ^[[:space:]]*pm\.max_children ]]; then + max_children="${value// /}" + break + fi + done < "$pool_config" + [ -z "$max_children" ] && max_children=5 + domain_max[$domain]=$max_children + + # Get average memory per process + local memory_stats + memory_stats=$(calculate_memory_per_process "$username") + local avg_kb + avg_kb=$(get_field "$memory_stats" 1) + + if [ -z "$avg_kb" ] || [ "$avg_kb" -eq 0 ]; then + avg_kb=$((50 * 1024)) # Default 50MB + fi + + domain_memory[$domain]=$((avg_kb / 1024)) + + # Get advanced traffic stats for this domain (7-day, bot-filtered, 95th percentile) + echo " Analyzing traffic for $domain..." >&2 + + local traffic + # Try fast method first (current process count) + local current_processes + current_processes=$(pgrep -u "$username" php-fpm 2>/dev/null | wc -l) + + if [ "$current_processes" -gt 0 ]; then + # Use current process count as baseline (fast, no log parsing) + traffic=$((current_processes * 2)) # Assume processes can handle ~2 req/min each + echo " Using current process count: $current_processes processes" >&2 + else + # Fallback to traffic analysis only if no processes found + local traffic_stats + traffic_stats=$(analyze_domain_traffic_advanced "$domain" "$username" 7 2>/dev/null) + + if [ -n "$traffic_stats" ] && [[ ! "$traffic_stats" =~ ERROR ]]; then + # Extract 95th percentile RPM (field 3) - pure bash + local rpm_95th + local temp="${traffic_stats#*|}" # Remove field 1 + temp="${temp#*|}" # Remove field 2 + rpm_95th="${temp%%|*}" # Extract field 3 + + # Add 20% headroom to 95th percentile for burst capacity + traffic=$(( rpm_95th * 120 / 100 )) + + # Minimum 1 req/min + [ "$traffic" -eq 0 ] && traffic=1 + else + # Ultimate fallback + traffic=5 # Conservative default + fi + fi + + domain_traffic[$domain]=$traffic + total_traffic=$((total_traffic + traffic)) + + done <<< "$user_domains" + done <<< "$users" + + if [ "$pool_count" -eq 0 ]; then + echo "ERROR|No PHP-FPM pools found" + return 1 + fi + + # Calculate proportional allocation based on traffic + echo "DOMAIN|USERNAME|PHP_VER|CURRENT_MAX|AVG_MB|TRAFFIC_RPM|RECOMMENDED_MAX|ALLOCATED_MB|REASON" + + for domain in "${!domain_traffic[@]}"; do + local traffic=${domain_traffic[$domain]} + local avg_mb=${domain_memory[$domain]} + local current_max=${domain_max[$domain]} + local username=${domain_username[$domain]} + local php_version=${domain_php_version[$domain]} + + # Calculate proportional share of available memory based on traffic + local traffic_percentage + if [ "$total_traffic" -gt 0 ]; then + traffic_percentage=$((traffic * 100 / total_traffic)) + else + traffic_percentage=$((100 / pool_count)) # Equal distribution if no traffic data + fi + + local allocated_mb=$((available_mb * traffic_percentage / 100)) + + # Ensure minimum allocation + [ "$allocated_mb" -lt "$((avg_mb * 5))" ] && allocated_mb=$((avg_mb * 5)) + + # Calculate recommended max_children for this allocation + # Protect against division by zero + local recommended_max=5 + if [ "$avg_mb" -gt 0 ]; then + recommended_max=$((allocated_mb / avg_mb)) + fi + + # Apply limits + [ "$recommended_max" -lt 5 ] && recommended_max=5 # Minimum 5 + [ "$recommended_max" -gt 200 ] && recommended_max=200 # Maximum 200 + + # Determine reason + local reason + if [ "$recommended_max" -lt "$current_max" ]; then + reason="REDUCE (prevent OOM)" + elif [ "$recommended_max" -gt "$current_max" ]; then + reason="INCREASE (traffic demands)" + else + reason="OPTIMAL" + fi + + echo "$domain|$username|$php_version|$current_max|${avg_mb}MB|$traffic|$recommended_max|${allocated_mb}MB|$reason" + done +} + +# Detect MySQL/MariaDB memory usage +# Usage: detect_mysql_memory_usage +# Returns: buffer_pool_mb|total_connections|estimated_total_mb|status +detect_mysql_memory_usage() { + # Check if MySQL/MariaDB is running + if ! pgrep -x "mysqld\|mariadbd" >/dev/null 2>&1; then + echo "0|0|0|NOT_RUNNING" + return 1 + fi + + # Try to get actual memory usage from ps + local mysql_rss_kb + mysql_rss_kb=$(ps aux | grep -E "[m]ysqld|[m]ariadbd" | awk '{sum+=$6} END {print sum}') + + if [ -n "$mysql_rss_kb" ] && [ "$mysql_rss_kb" -gt 0 ]; then + local mysql_rss_mb=$((mysql_rss_kb / 1024)) + + # Try to get buffer pool size from MySQL + local buffer_pool_mb=0 + local max_connections=150 # Default + + if command -v mysql >/dev/null 2>&1; then + # Try to query MySQL directly + buffer_pool_mb=$(mysql -Nse "SELECT ROUND(@@innodb_buffer_pool_size/1024/1024)" 2>/dev/null || echo "0") + max_connections=$(mysql -Nse "SELECT @@max_connections" 2>/dev/null || echo "150") + fi + + # If we couldn't get it from MySQL, try my.cnf + if [ "$buffer_pool_mb" -eq 0 ]; then + local my_cnf="/etc/my.cnf" + [ ! -f "$my_cnf" ] && my_cnf="/etc/mysql/my.cnf" + + if [ -f "$my_cnf" ]; then + # Parse innodb_buffer_pool_size + local buffer_pool_setting + buffer_pool_setting=$(grep -E "^innodb_buffer_pool_size" "$my_cnf" | awk -F'=' '{print $2}' | tr -d ' ') + + if [ -n "$buffer_pool_setting" ]; then + # Convert to MB (handle G, M, K suffixes) + if [[ "$buffer_pool_setting" =~ ([0-9]+)G ]]; then + buffer_pool_mb=$((${BASH_REMATCH[1]} * 1024)) + elif [[ "$buffer_pool_setting" =~ ([0-9]+)M ]]; then + buffer_pool_mb=${BASH_REMATCH[1]} + elif [[ "$buffer_pool_setting" =~ ([0-9]+)K ]]; then + buffer_pool_mb=$((${BASH_REMATCH[1]} / 1024)) + elif [[ "$buffer_pool_setting" =~ ^[0-9]+$ ]]; then + # Raw bytes + buffer_pool_mb=$((buffer_pool_setting / 1024 / 1024)) + fi + fi + fi + fi + + # Use actual RSS as the estimated total + local estimated_total_mb=$mysql_rss_mb + + # Determine status + local status="RUNNING" + if [ "$estimated_total_mb" -gt 4096 ]; then + status="HIGH_MEMORY" + elif [ "$estimated_total_mb" -gt 2048 ]; then + status="MODERATE_MEMORY" + else + status="LOW_MEMORY" + fi + + echo "$buffer_pool_mb|$max_connections|$estimated_total_mb|$status" + return 0 + fi + + echo "0|0|0|UNKNOWN" + return 1 +} + # Export all functions export -f analyze_memory_exhausted_errors export -f analyze_max_children_errors @@ -949,9 +1442,12 @@ export -f calculate_memory_per_process export -f calculate_optimal_max_children export -f calculate_peak_concurrent_requests export -f calculate_avg_requests_per_minute +export -f analyze_domain_traffic_advanced export -f analyze_opcache_effectiveness export -f detect_php_config_issues export -f analyze_domain_php export -f convert_to_bytes export -f calculate_server_memory_capacity export -f calculate_balanced_memory_allocation +export -f calculate_balanced_memory_allocation_per_domain +export -f detect_mysql_memory_usage diff --git a/modules/performance/php-optimizer.sh b/modules/performance/php-optimizer.sh index 85c6db2..cc875bf 100755 --- a/modules/performance/php-optimizer.sh +++ b/modules/performance/php-optimizer.sh @@ -416,6 +416,59 @@ optimize_domain() { return fi + # Check server-wide memory capacity first + cecho "${CYAN}─────────────────────────────────────────────────────────────────────${NC}" + cecho "${WHITE}${BOLD}SERVER MEMORY ANALYSIS${NC}" + echo "" + + local capacity_result + capacity_result=$(calculate_server_memory_capacity 2>&1) + local total_required_mb total_ram_mb percentage status + total_required_mb=$(echo "$capacity_result" | head -1 | cut -d'|' -f1) + total_ram_mb=$(echo "$capacity_result" | head -1 | cut -d'|' -f2) + percentage=$(echo "$capacity_result" | head -1 | cut -d'|' -f3) + status=$(echo "$capacity_result" | head -1 | cut -d'|' -f4) + + cecho " Total Server RAM: ${WHITE}${total_ram_mb}MB${NC}" + cecho " Current FPM Capacity: ${WHITE}${total_required_mb}MB${NC} (${percentage}% of RAM)" + + case "$status" in + CRITICAL) + cecho " Server Status: ${RED}${BOLD}CRITICAL${NC} - Server at risk of OOM!" + ;; + WARNING) + cecho " Server Status: ${YELLOW}${BOLD}WARNING${NC} - High memory pressure" + ;; + CAUTION) + cecho " Server Status: ${YELLOW}CAUTION${NC} - Approaching limits" + ;; + HEALTHY) + cecho " Server Status: ${GREEN}HEALTHY${NC} - Sufficient headroom" + ;; + esac + + echo "" + + # Check for max_children errors + cecho "${WHITE}${BOLD}MAX_CHILDREN ERROR ANALYSIS${NC}" + echo "" + + local max_children_errors + max_children_errors=$(analyze_max_children_errors "$username" 7) + local error_count last_error_time + error_count=$(echo "$max_children_errors" | cut -d'|' -f1) + last_error_time=$(echo "$max_children_errors" | cut -d'|' -f2) + + if [ "$error_count" -gt 0 ]; then + cecho " ${RED}✗${NC} Found ${RED}${BOLD}$error_count${NC} max_children errors in last 7 days" + cecho " ${YELLOW}Last error: $last_error_time${NC}" + cecho " ${CYAN}→${NC} This domain is hitting process limits and rejecting requests!" + else + cecho " ${GREEN}✓${NC} No max_children errors in last 7 days" + fi + + echo "" + # Get optimization recommendations cecho "${CYAN}─────────────────────────────────────────────────────────────────────${NC}" cecho "${WHITE}${BOLD}RECOMMENDED OPTIMIZATIONS${NC}" @@ -430,14 +483,22 @@ optimize_domain() { # Get current max_children local pool_config - pool_config=$(find_fpm_pool_config "$username") + pool_config=$(find_fpm_pool_config "$username" "$domain") + + # Track available optimizations + declare -A opt_available + declare -A opt_description + local opt_count=0 local current_max_children="" if [ -n "$pool_config" ] && [ -f "$pool_config" ]; then current_max_children=$(grep "^pm.max_children" "$pool_config" | awk -F'=' '{print $2}' | tr -d ' ') if [ -n "$current_max_children" ] && [ "$recommended_max_children" -ne "$current_max_children" ]; then - cecho "${GREEN}1.${NC} Adjust ${BOLD}pm.max_children${NC} from ${RED}$current_max_children${NC} to ${GREEN}$recommended_max_children${NC}" + opt_count=$((opt_count + 1)) + opt_available["max_children"]="true" + opt_description["max_children"]="Adjust pm.max_children from $current_max_children to $recommended_max_children" + cecho "${GREEN}$opt_count.${NC} Adjust ${BOLD}pm.max_children${NC} from ${RED}$current_max_children${NC} to ${GREEN}$recommended_max_children${NC}" cecho " Reason: $reason" echo "" fi @@ -446,31 +507,79 @@ optimize_domain() { # Check OPcache local opcache_status opcache_status=$(analyze_opcache_effectiveness "$username") - local status hit_rate opcache_rec - status=$(echo "$opcache_status" | cut -d'|' -f1) + local opcache_state hit_rate opcache_rec + opcache_state=$(echo "$opcache_status" | cut -d'|' -f1) hit_rate=$(echo "$opcache_status" | cut -d'|' -f2) opcache_rec=$(echo "$opcache_status" | cut -d'|' -f5) - if [ "$status" = "DISABLED" ]; then - cecho "${GREEN}2.${NC} ${BOLD}Enable OPcache${NC} for 40-70% performance boost" + if [ "$opcache_state" = "DISABLED" ]; then + opt_count=$((opt_count + 1)) + opt_available["opcache"]="true" + opt_description["opcache"]="Enable OPcache for 40-70% performance boost" + cecho "${GREEN}$opt_count.${NC} ${BOLD}Enable OPcache${NC} for 40-70% performance boost" echo "" else - local hit_rate_low=$(awk "BEGIN {print ($hit_rate < 90 ? 1 : 0)}" 2>/dev/null || echo 0) - if [ "$hit_rate_low" -eq 1 ]; then - cecho "${GREEN}2.${NC} ${BOLD}Increase opcache.memory_consumption${NC} (current hit rate: ${hit_rate}%)" - echo "" + # Check if hit_rate is numeric before using awk + if [ -n "$hit_rate" ] && [[ "$hit_rate" =~ ^[0-9]+$ ]]; then + local hit_rate_low=$(awk "BEGIN {print ($hit_rate < 90 ? 1 : 0)}" 2>/dev/null || echo 0) + if [ "$hit_rate_low" -eq 1 ]; then + opt_count=$((opt_count + 1)) + opt_available["opcache"]="true" + opt_description["opcache"]="Increase opcache.memory_consumption (current hit rate: ${hit_rate}%)" + cecho "${GREEN}$opt_count.${NC} ${BOLD}Increase opcache.memory_consumption${NC} (current hit rate: ${hit_rate}%)" + echo "" + fi fi fi + # Check if there are any optimizations to apply + if [ "$opt_count" -eq 0 ]; then + cecho "${GREEN}${BOLD}✓ All settings are optimal - no changes needed!${NC}" + echo "" + read -p "Press Enter to continue..." + return + fi + echo "" cecho "${CYAN}─────────────────────────────────────────────────────────────────────${NC}" echo "" - # Ask if user wants to apply changes - read -p "Apply these recommendations? (y/n): " apply_choice + # Ask user which optimizations to apply + cecho "${WHITE}${BOLD}SELECT OPTIMIZATIONS TO APPLY${NC}" + echo "" + cecho " ${GREEN}a${NC}) Apply all $opt_count recommendations" + [ "${opt_available[max_children]}" = "true" ] && cecho " ${GREEN}1${NC}) Apply max_children optimization only" + [ "${opt_available[opcache]}" = "true" ] && cecho " ${GREEN}2${NC}) Apply OPcache optimization only" + cecho " ${RED}n${NC}) Cancel - don't apply any changes" + echo "" + read -p "Select option: " apply_choice - if [[ ! "$apply_choice" =~ ^[Yy]$ ]]; then - cecho "${YELLOW}Optimization cancelled - no changes made${NC}" + # Determine which optimizations to apply + local apply_max_children=false + local apply_opcache=false + + case "$apply_choice" in + a|A) + apply_max_children=${opt_available[max_children]:-false} + apply_opcache=${opt_available[opcache]:-false} + ;; + 1) + apply_max_children=${opt_available[max_children]:-false} + ;; + 2) + apply_opcache=${opt_available[opcache]:-false} + ;; + n|N|*) + cecho "${YELLOW}Optimization cancelled - no changes made${NC}" + echo "" + read -p "Press Enter to continue..." + return + ;; + esac + + # Check if nothing was selected + if [ "$apply_max_children" = "false" ] && [ "$apply_opcache" = "false" ]; then + cecho "${YELLOW}No optimizations selected${NC}" echo "" read -p "Press Enter to continue..." return @@ -492,28 +601,35 @@ optimize_domain() { echo "" # Apply changes - cecho "${CYAN}Applying optimizations...${NC}" + cecho "${CYAN}Applying selected optimizations...${NC}" echo "" local changes_made=0 local changes_failed=0 - # Reuse pool_config from earlier (already fetched at line 433) - # No need to call find_fpm_pool_config again - - if [ -n "$pool_config" ] && [ -f "$pool_config" ]; then - # Apply max_children change if recommended - if [ -n "$recommended_max_children" ] && [ -n "$current_max_children" ] && [ "$recommended_max_children" -ne "$current_max_children" ]; then - if modify_fpm_pool_setting "$pool_config" "pm.max_children" "$recommended_max_children" >/dev/null 2>&1; then - cecho " ${GREEN}✓${NC} Set pm.max_children = $recommended_max_children" - changes_made=$((changes_made + 1)) - else - cecho " ${RED}✗${NC} Failed to set pm.max_children" - changes_failed=$((changes_failed + 1)) + # Apply max_children if selected + if [ "$apply_max_children" = "true" ]; then + if [ -n "$pool_config" ] && [ -f "$pool_config" ]; then + if [ -n "$recommended_max_children" ] && [ -n "$current_max_children" ]; then + if modify_fpm_pool_setting "$pool_config" "pm.max_children" "$recommended_max_children" >/dev/null 2>&1; then + cecho " ${GREEN}✓${NC} Set pm.max_children = $recommended_max_children" + changes_made=$((changes_made + 1)) + else + cecho " ${RED}✗${NC} Failed to set pm.max_children" + changes_failed=$((changes_failed + 1)) + fi fi fi fi + # Apply OPcache if selected + if [ "$apply_opcache" = "true" ]; then + # Note: OPcache settings are in php.ini, not FPM pool config + # This would require php.ini modification (not implemented yet) + cecho " ${YELLOW}⚠${NC} OPcache optimization requires php.ini modification (not yet implemented)" + # TODO: Implement php.ini modification for OPcache + fi + echo "" cecho "${CYAN}─────────────────────────────────────────────────────────────────────${NC}" @@ -630,29 +746,186 @@ optimize_all_domains() { echo "" # Get balanced recommendations based on total RAM + # Use per-domain optimization for cPanel, per-user for other panels local balanced_result - balanced_result=$(calculate_balanced_memory_allocation 2>&1) + if [ "$SYS_CONTROL_PANEL" = "cpanel" ]; then + cecho " ${GREEN}✓${NC} Detected cPanel - using per-domain optimization" + echo "" + # Don't redirect stderr so progress messages are visible + balanced_result=$(calculate_balanced_memory_allocation_per_domain) + else + cecho " ${GREEN}✓${NC} Detected $SYS_CONTROL_PANEL - using per-user optimization" + echo "" + balanced_result=$(calculate_balanced_memory_allocation) + fi - # Parse recommendations (format: USER|CURRENT_MAX|AVG_MB|TRAFFIC_RPM|RECOMMENDED_MAX|ALLOCATED_MB|REASON) + # Parse recommendations based on control panel type declare -A recommended_values - while IFS='|' read -r username current_max avg_mb traffic_rpm recommended_max allocated_mb reason; do - [ "$username" = "USER" ] && continue # Skip header - [ -z "$username" ] && continue - recommended_values["$username"]="$recommended_max" - cecho " ${CYAN}$username${NC}: $current_max → $recommended_max (${reason})" - done <<< "$balanced_result" + declare -A domain_to_username + declare -A recommendation_reasons + local changes_needed=0 + local no_change_needed=0 + + if [ "$SYS_CONTROL_PANEL" = "cpanel" ]; then + # cPanel format: DOMAIN|USERNAME|PHP_VER|CURRENT_MAX|AVG_MB|TRAFFIC_RPM|RECOMMENDED_MAX|ALLOCATED_MB|REASON + while IFS='|' read -r domain username php_ver current_max avg_mb traffic_rpm recommended_max allocated_mb reason; do + [ "$domain" = "DOMAIN" ] && continue # Skip header + [ -z "$domain" ] && continue + recommended_values["$domain"]="$recommended_max" + domain_to_username["$domain"]="$username" + recommendation_reasons["$domain"]="$reason" + + # Track if change is needed + if [ "$current_max" != "$recommended_max" ]; then + changes_needed=$((changes_needed + 1)) + if [[ "$reason" == *"REDUCE"* ]]; then + cecho " ${YELLOW}⚠${NC} ${CYAN}$domain${NC} [$username]: $current_max → ${YELLOW}$recommended_max${NC} (${reason})" + elif [[ "$reason" == *"INCREASE"* ]]; then + cecho " ${GREEN}↑${NC} ${CYAN}$domain${NC} [$username]: $current_max → ${GREEN}$recommended_max${NC} (${reason})" + else + cecho " ${CYAN}$domain${NC} [$username]: $current_max → $recommended_max (${reason})" + fi + else + no_change_needed=$((no_change_needed + 1)) + fi + done <<< "$balanced_result" + else + # Other panels format: USER|CURRENT_MAX|AVG_MB|TRAFFIC_RPM|RECOMMENDED_MAX|ALLOCATED_MB|REASON + while IFS='|' read -r username current_max avg_mb traffic_rpm recommended_max allocated_mb reason; do + [ "$username" = "USER" ] && continue # Skip header + [ -z "$username" ] && continue + recommended_values["$username"]="$recommended_max" + recommendation_reasons["$username"]="$reason" + + # Track if change is needed + if [ "$current_max" != "$recommended_max" ]; then + changes_needed=$((changes_needed + 1)) + if [[ "$reason" == *"REDUCE"* ]]; then + cecho " ${YELLOW}⚠${NC} ${CYAN}$username${NC}: $current_max → ${YELLOW}$recommended_max${NC} (${reason})" + elif [[ "$reason" == *"INCREASE"* ]]; then + cecho " ${GREEN}↑${NC} ${CYAN}$username${NC}: $current_max → ${GREEN}$recommended_max${NC} (${reason})" + else + cecho " ${CYAN}$username${NC}: $current_max → $recommended_max (${reason})" + fi + else + no_change_needed=$((no_change_needed + 1)) + fi + done <<< "$balanced_result" + fi echo "" - read -p "Apply these balanced optimizations? (yes/no): " apply_confirm + cecho "${WHITE}${BOLD}SUMMARY${NC}" + cecho " Domains/users requiring changes: ${YELLOW}${BOLD}$changes_needed${NC}" + cecho " Already optimal: ${GREEN}$no_change_needed${NC}" - if [ "$apply_confirm" != "yes" ]; then - cecho "${YELLOW}Optimization cancelled${NC}" + if [ "$changes_needed" -eq 0 ]; then + echo "" + cecho "${GREEN}${BOLD}✓ All domains are already optimally configured!${NC}" + echo "" read -p "Press Enter to continue..." return fi + echo "" + cecho "${WHITE}${BOLD}APPLY OPTIONS${NC}" + cecho " ${GREEN}a${NC}) Apply ALL $changes_needed optimizations" + cecho " ${GREEN}s${NC}) Select individual domains/users to optimize" + cecho " ${RED}n${NC}) Cancel - don't apply any changes" + echo "" + read -p "Select option: " apply_confirm + + # Handle selection mode + declare -A domains_to_apply + case "$apply_confirm" in + a|A|yes) + # Apply all - mark all domains/users for optimization + if [ "$SYS_CONTROL_PANEL" = "cpanel" ]; then + for domain in "${!recommended_values[@]}"; do + domains_to_apply["$domain"]="true" + done + else + for username in "${!recommended_values[@]}"; do + domains_to_apply["$username"]="true" + done + fi + ;; + s|S) + # Individual selection + echo "" + cecho "${WHITE}${BOLD}SELECT DOMAINS/USERS TO OPTIMIZE${NC}" + cecho "${CYAN}Enter numbers separated by spaces (e.g., 1 3 5) or 'all' for all, 'none' to cancel${NC}" + echo "" + + # Build selection list + local -a selection_list + local index=1 + if [ "$SYS_CONTROL_PANEL" = "cpanel" ]; then + for domain in "${!recommended_values[@]}"; do + local username=${domain_to_username[$domain]} + local current_max=$(echo "$balanced_result" | grep "^$domain|" | cut -d'|' -f4) + local recommended_max=${recommended_values[$domain]} + if [ "$current_max" != "$recommended_max" ]; then + selection_list+=("$domain") + cecho " ${GREEN}$index${NC}) $domain [$username]: $current_max → $recommended_max" + index=$((index + 1)) + fi + done + else + for username in "${!recommended_values[@]}"; do + local current_max=$(echo "$balanced_result" | grep "^$username|" | cut -d'|' -f2) + local recommended_max=${recommended_values[$username]} + if [ "$current_max" != "$recommended_max" ]; then + selection_list+=("$username") + cecho " ${GREEN}$index${NC}) $username: $current_max → $recommended_max" + index=$((index + 1)) + fi + done + fi + + echo "" + read -p "Enter selection: " user_selection + + if [[ "$user_selection" =~ ^(all|ALL)$ ]]; then + # Select all + for item in "${selection_list[@]}"; do + domains_to_apply["$item"]="true" + done + elif [[ "$user_selection" =~ ^(none|NONE|n|N)$ ]]; then + cecho "${YELLOW}Optimization cancelled${NC}" + echo "" + read -p "Press Enter to continue..." + return + else + # Parse selected numbers + for num in $user_selection; do + if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le "${#selection_list[@]}" ]; then + local selected_item="${selection_list[$((num - 1))]}" + domains_to_apply["$selected_item"]="true" + fi + done + fi + + # Check if anything was selected + if [ "${#domains_to_apply[@]}" -eq 0 ]; then + cecho "${YELLOW}No domains/users selected${NC}" + echo "" + read -p "Press Enter to continue..." + return + fi + + echo "" + cecho "${GREEN}Selected ${#domains_to_apply[@]} domain(s)/user(s) for optimization${NC}" + ;; + n|N|*) + cecho "${YELLOW}Optimization cancelled${NC}" + read -p "Press Enter to continue..." + return + ;; + esac + show_banner cecho "${WHITE}${BOLD}Applying optimizations...${NC}" + cecho "${CYAN}Selected: ${#domains_to_apply[@]} domain(s)/user(s)${NC}" echo "" # Get all users @@ -683,6 +956,20 @@ optimize_all_domains() { total_domains=$((total_domains + 1)) + # Check if this domain/user was selected for optimization + local should_optimize=false + if [ "$SYS_CONTROL_PANEL" = "cpanel" ]; then + [ "${domains_to_apply[$domain]}" = "true" ] && should_optimize=true + else + [ "${domains_to_apply[$username]}" = "true" ] && should_optimize=true + fi + + if [ "$should_optimize" = "false" ]; then + cecho "${CYAN}[$total_domains] Skipping: ${WHITE}$domain${NC} ${CYAN}[$username]${NC} (not selected)" + skipped_count=$((skipped_count + 1)) + continue + fi + cecho "${CYAN}[$total_domains] Processing: ${WHITE}$domain${NC} ${CYAN}[$username]${NC}" # Detect issues first @@ -701,7 +988,13 @@ optimize_all_domains() { # Use balanced recommendation from server-wide analysis local recommended_max_children - recommended_max_children="${recommended_values[$username]}" + if [ "$SYS_CONTROL_PANEL" = "cpanel" ]; then + # cPanel uses per-domain recommendations + recommended_max_children="${recommended_values[$domain]}" + else + # Other panels use per-user recommendations + recommended_max_children="${recommended_values[$username]}" + fi # If no recommendation found, skip if [ -z "$recommended_max_children" ]; then @@ -710,7 +1003,7 @@ optimize_all_domains() { # Get current pool config local pool_config - pool_config=$(find_fpm_pool_config "$username") + pool_config=$(find_fpm_pool_config "$username" "$domain") if [ -z "$pool_config" ] || [ ! -f "$pool_config" ]; then cecho " ${YELLOW}⊘${NC} No FPM pool config found - skipping" @@ -889,11 +1182,16 @@ view_opcache_stats() { # Recommendation echo "" - local hit_rate_low=$(awk "BEGIN {print ($hit_rate < 90 ? 1 : 0)}" 2>/dev/null || echo 0) - if [ "$hit_rate_low" -eq 1 ]; then - cecho " ${YELLOW}⚠ Hit rate below 90% - Consider increasing opcache.memory_consumption${NC}" + # Check if hit_rate is numeric before using awk + if [ -n "$hit_rate" ] && [[ "$hit_rate" =~ ^[0-9]+$ ]]; then + local hit_rate_low=$(awk "BEGIN {print ($hit_rate < 90 ? 1 : 0)}" 2>/dev/null || echo 0) + if [ "$hit_rate_low" -eq 1 ]; then + cecho " ${YELLOW}⚠ Hit rate below 90% - Consider increasing opcache.memory_consumption${NC}" + else + cecho " ${GREEN}✓ Hit rate is excellent${NC}" + fi else - cecho " ${GREEN}✓ Hit rate is excellent${NC}" + cecho " ${YELLOW}⚠ Unable to determine OPcache hit rate${NC}" fi echo ""