#!/bin/bash # Show RAM usage for containers # Usage: ./show-ram.sh [node-path] # Without argument: shows RAM per node and total server RAM # With argument: shows containers for specific node set -euo pipefail BASEPATH="$(dirname "$0")" source "$BASEPATH/.env" NODE_PATH="${1:-}" # Function to convert memory string to MiB to_mib() { local val="$1" local num=$(echo "$val" | sed 's/[^0-9.]//g') if [ -z "$num" ] || [ "$num" = "0" ]; then echo "0" return fi if echo "$val" | grep -qi "GiB"; then echo "$num * 1024" | bc -l | awk '{printf "%.2f", $1}' elif echo "$val" | grep -qi "MiB"; then echo "$num" | awk '{printf "%.2f", $1}' elif echo "$val" | grep -qi "KiB"; then echo "$num / 1024" | bc -l | awk '{printf "%.2f", $1}' else echo "$num" | awk '{printf "%.2f", $1}' fi } # Function to format MiB to human readable format_size() { local mib="$1" # Handle empty or zero values if [ -z "$mib" ] || [ "$mib" = "0" ] || [ "$mib" = "0.00" ]; then echo "0 MiB" return fi # Compare as float using awk if awk "BEGIN {exit !($mib >= 1024)}"; then echo "scale=2; $mib / 1024" | bc -l | xargs printf "%.2f GiB" else printf "%.0f MiB" "$mib" fi } # Cache for docker stats (container_id -> mem_usage) declare -A DOCKER_STATS_CACHE # Cache for service -> container_id mapping declare -A SERVICE_TO_CID # Initialize caches - fetch all container stats once init_docker_caches() { # Get all running container IDs with their service labels while IFS=$'\t' read -r cid service; do [ -z "$cid" ] && continue if [ -n "$service" ]; then SERVICE_TO_CID["$service"]="$cid" fi done < <(docker ps --format "{{.ID}}\t{{.Label \"com.docker.compose.service\"}}" 2>/dev/null || true) # Get stats for all containers at once (much faster than individual calls) # docker stats can take multiple container IDs, but we'll get all stats in one call if [ ${#SERVICE_TO_CID[@]} -gt 0 ]; then local all_cids=($(printf '%s\n' "${SERVICE_TO_CID[@]}" | sort -u)) if [ ${#all_cids[@]} -gt 0 ]; then # Call docker stats once with all container IDs while IFS=$'\t' read -r cid mem_usage; do [ -z "$cid" ] && continue DOCKER_STATS_CACHE["$cid"]="$mem_usage" done < <(docker stats --no-stream --format "{{.ID}}\t{{.MemUsage}}" "${all_cids[@]}" 2>/dev/null || true) fi fi } # Function to get RAM for a compose file by matching service names get_compose_ram() { local compose_file="$1" local total=0 # Get service names defined in this compose file local services services=$(cat "$compose_file" 2>/dev/null | yaml2json - 2>/dev/null | jq -r '.services | keys[]' 2>/dev/null || echo "") [ -z "$services" ] && echo "0" && return for service in $services; do # Use cached container ID local cid="${SERVICE_TO_CID[$service]:-}" [ -z "$cid" ] && continue # Use cached memory usage local mem_usage="${DOCKER_STATS_CACHE[$cid]:-}" [ -z "$mem_usage" ] && continue # Extract just the used memory (before the /) mem_usage=$(echo "$mem_usage" | awk -F'/' '{print $1}' | xargs) [ -z "$mem_usage" ] && continue local mem_mib mem_mib=$(to_mib "$mem_usage") total=$(echo "$total + $mem_mib" | bc -l | awk '{printf "%.2f", $1}') done echo "$total" } # Blacklist - skip infrastructure compose files blacklist=("drpc.yml" "drpc-free.yml" "drpc-home.yml" "base.yml" "rpc.yml" "monitoring.yml" "ftp.yml" "backup-http.yml") is_blacklisted() { local part="$1" for word in "${blacklist[@]}"; do if echo "$part" | grep -qE "$word"; then return 0 fi done return 1 } if [ -z "$NODE_PATH" ]; then # No argument: show RAM per node using COMPOSE_FILE from .env echo "RAM usage per node:" echo "========================================" # Initialize caches once - this batches all docker calls init_docker_caches IFS=':' read -ra parts <<< "$COMPOSE_FILE" declare -A node_ram total_mib=0 # Export caches to temp files for parallel processing temp_dir=$(mktemp -d) trap "rm -rf $temp_dir" EXIT # Write service->cid mapping to file for service in "${!SERVICE_TO_CID[@]}"; do echo "$service|${SERVICE_TO_CID[$service]}" >> "$temp_dir/service_cid.map" done # Write cid->mem_usage mapping to file for cid in "${!DOCKER_STATS_CACHE[@]}"; do echo "$cid|${DOCKER_STATS_CACHE[$cid]}" >> "$temp_dir/cid_mem.map" done # Process nodes in parallel using background jobs pids=() for part in "${parts[@]}"; do # Skip blacklisted files is_blacklisted "$part" && continue compose_file="${BASEPATH}/${part}" [ ! -f "$compose_file" ] && continue # Get node path (remove .yml extension) node_path="${part%.yml}" # Process in background for parallel execution ( # Rebuild caches in this subprocess from temp files declare -A local_service_cid declare -A local_cid_mem while IFS='|' read -r service cid; do [ -n "$service" ] && [ -n "$cid" ] && local_service_cid["$service"]="$cid" done < "$temp_dir/service_cid.map" 2>/dev/null || true while IFS='|' read -r cid mem; do [ -n "$cid" ] && [ -n "$mem" ] && local_cid_mem["$cid"]="$mem" done < "$temp_dir/cid_mem.map" 2>/dev/null || true # Get service names from compose file services=$(cat "$compose_file" 2>/dev/null | yaml2json - 2>/dev/null | jq -r '.services | keys[]' 2>/dev/null || echo "") [ -z "$services" ] && exit 0 total=0 for service in $services; do cid="${local_service_cid[$service]:-}" [ -z "$cid" ] && continue mem_usage="${local_cid_mem[$cid]:-}" [ -z "$mem_usage" ] && continue mem_usage=$(echo "$mem_usage" | awk -F'/' '{print $1}' | xargs) [ -z "$mem_usage" ] && continue mem_mib=$(to_mib "$mem_usage") total=$(echo "$total + $mem_mib" | bc -l | awk '{printf "%.2f", $1}') done if [ "$total" != "0" ]; then # Sanitize node_path for use as filename (replace / with _) safe_node_path=$(echo "$node_path" | tr '/' '_') echo "$total|$node_path" > "$temp_dir/result_$safe_node_path" fi ) & pids+=($!) done # Wait for all background jobs to complete for pid in "${pids[@]}"; do wait "$pid" 2>/dev/null || true done # Collect results result_count=0 for result_file in "$temp_dir"/result_*; do [ -f "$result_file" ] || continue IFS='|' read -r ram_mib node_path < "$result_file" [ -z "$ram_mib" ] || [ "$ram_mib" = "0" ] && continue node_ram["$node_path"]="$ram_mib" total_mib=$(echo "$total_mib + $ram_mib" | bc -l | awk '{printf "%.2f", $1}') result_count=$((result_count + 1)) done if [ "$result_count" -eq 0 ]; then echo "No running nodes found" exit 0 fi # Sort by RAM usage and display for node_path in "${!node_ram[@]}"; do echo "${node_ram[$node_path]} $node_path" done | sort -rn | while IFS=' ' read -r mib name; do printf "%-55s %s\n" "$name" "$(format_size $mib)" done echo "========================================" echo "Total container RAM: $(format_size $total_mib)" # Show server total RAM total_server_ram=$(free -h | awk '/^Mem:/ {print $2}') used_server_ram=$(free -h | awk '/^Mem:/ {print $3}') echo "Server RAM: ${used_server_ram} / ${total_server_ram}" else # Specific node - path maps directly to compose file COMPOSE_FILE_PATH="${BASEPATH}/${NODE_PATH}.yml" if [ ! -f "$COMPOSE_FILE_PATH" ]; then echo "Error: Compose file not found: $COMPOSE_FILE_PATH" exit 1 fi # Initialize caches once init_docker_caches # Get service names from compose file services=$(cat "$COMPOSE_FILE_PATH" 2>/dev/null | yaml2json - 2>/dev/null | jq -r '.services | keys[]' 2>/dev/null || echo "") if [ -z "$services" ]; then echo "No services found in $NODE_PATH" exit 0 fi echo "RAM usage for $NODE_PATH:" echo "----------------------------------------" total_mib=0 for service in $services; do # Use cached container ID cid="${SERVICE_TO_CID[$service]:-}" [ -z "$cid" ] && continue # Use cached memory usage mem_info="${DOCKER_STATS_CACHE[$cid]:-}" if [ -z "$mem_info" ]; then # Fallback: get stats for this specific container if not in cache mem_info=$(docker stats --no-stream --format "{{.MemUsage}}\t{{.MemPerc}}" "$cid" 2>/dev/null || echo "") [ -z "$mem_info" ] && continue fi mem_usage=$(echo "$mem_info" | awk -F'\t' '{print $1}' | awk -F'/' '{print $1}' | xargs) mem_perc=$(echo "$mem_info" | awk -F'\t' '{print $2}' | xargs) # If we don't have percentage, try to get it separately if [ -z "$mem_perc" ]; then mem_perc=$(docker stats --no-stream --format "{{.MemPerc}}" "$cid" 2>/dev/null || echo "") fi printf "%-40s %s\t%s\n" "$service" "$mem_usage" "$mem_perc" mem_mib=$(to_mib "$mem_usage") total_mib=$(echo "$total_mib + $mem_mib" | bc -l | awk '{printf "%.2f", $1}') done echo "----------------------------------------" echo "Total: $(format_size $total_mib)" fi