nigig-org/nimanyatta/capture_metrics.sh
andodeki fd8b0632ca
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
Include nimanyatta as normal tree (not embedded git)
2026-09-26 09:29:36 +03:00

294 lines
No EOL
14 KiB
Bash
Executable file

#!/bin/bash
# ═══════════════════════════════════════════════════════════════════════════
# Server Capacity Metrics Capture
# Run this WHILE your load test is running
# ═══════════════════════════════════════════════════════════════════════════
PROCESS_NAME="${1:-broadcast_server}"
DURATION="${2:-60}"
INTERVAL="${3:-1}"
LOGFILE="server_metrics_$(date +%Y%m%d_%H%M%S).txt"
CSVFILE="server_metrics_$(date +%Y%m%d_%H%M%S).csv"
# Find the process
SERVER_PID=$(pgrep -f "$PROCESS_NAME" | head -1)
if [ -z "$SERVER_PID" ]; then
echo "❌ Process '$PROCESS_NAME' not found"
echo "Usage: $0 [process_name] [duration_seconds] [interval_seconds]"
exit 1
fi
echo "═══════════════════════════════════════════════════════════════════"
echo "📊 Server Capacity Metrics Capture"
echo "═══════════════════════════════════════════════════════════════════"
echo "Process: $PROCESS_NAME (PID: $SERVER_PID)"
echo "Duration: ${DURATION}s"
echo "Interval: ${INTERVAL}s"
echo "Log file: $LOGFILE"
echo "CSV file: $CSVFILE"
echo "═══════════════════════════════════════════════════════════════════"
echo ""
# Get baseline memory (before load)
BASELINE_RSS=$(ps -o rss= -p $SERVER_PID 2>/dev/null | tr -d ' ')
BASELINE_RSS_MB=$(echo "scale=2; ${BASELINE_RSS:-0} / 1024" | bc)
echo "Baseline RSS: ${BASELINE_RSS_MB} MB"
echo ""
# Write headers
{
echo "═══════════════════════════════════════════════════════════════════"
echo "SERVER CAPACITY METRICS - $(date)"
echo "Process: $PROCESS_NAME (PID: $SERVER_PID)"
echo "Baseline RSS: ${BASELINE_RSS_MB} MB"
echo "═══════════════════════════════════════════════════════════════════"
echo ""
} > "$LOGFILE"
echo "timestamp,elapsed_sec,cpu_pct,rss_mb,vsz_mb,threads,established,time_wait,fds,total_tcp" > "$CSVFILE"
# Tracking variables for peak/avg calculation
TOTAL_CPU=0
TOTAL_RSS=0
TOTAL_CONNS=0
PEAK_CPU=0
PEAK_RSS=0
PEAK_CONNS=0
SAMPLE_COUNT=0
START_TIME=$(date +%s)
echo "Capturing metrics for ${DURATION}s... (Ctrl+C to stop early)"
echo ""
capture_sample() {
local ELAPSED=$1
# Get CPU and memory from ps (more reliable than top for scripting)
local PS_OUTPUT=$(ps -o %cpu=,rss=,vsz= -p $SERVER_PID 2>/dev/null)
local CPU=$(echo "$PS_OUTPUT" | awk '{print $1}' | tr -d ' ')
local RSS=$(echo "$PS_OUTPUT" | awk '{print $2}' | tr -d ' ')
local VSZ=$(echo "$PS_OUTPUT" | awk '{print $3}' | tr -d ' ')
# Convert to MB
local RSS_MB=$(echo "scale=2; ${RSS:-0} / 1024" | bc)
local VSZ_MB=$(echo "scale=2; ${VSZ:-0} / 1024" | bc)
# Thread count
local THREADS=$(ps -M $SERVER_PID 2>/dev/null | tail -n +2 | wc -l | tr -d ' ')
# Network stats
local ESTABLISHED=$(netstat -an 2>/dev/null | grep -E "\.8080\s" | grep -c ESTABLISHED)
local TIME_WAIT=$(netstat -an 2>/dev/null | grep -E "\.8080\s" | grep -c TIME_WAIT)
local TOTAL_TCP=$(netstat -an 2>/dev/null | grep -c tcp)
# File descriptors
local FDS=$(lsof -p $SERVER_PID 2>/dev/null | wc -l | tr -d ' ')
# Update totals for averages
TOTAL_CPU=$(echo "$TOTAL_CPU + ${CPU:-0}" | bc)
TOTAL_RSS=$(echo "$TOTAL_RSS + ${RSS_MB:-0}" | bc)
TOTAL_CONNS=$((TOTAL_CONNS + ESTABLISHED))
SAMPLE_COUNT=$((SAMPLE_COUNT + 1))
# Update peaks
if (( $(echo "${CPU:-0} > $PEAK_CPU" | bc -l) )); then
PEAK_CPU=${CPU:-0}
fi
if (( $(echo "${RSS_MB:-0} > $PEAK_RSS" | bc -l) )); then
PEAK_RSS=${RSS_MB:-0}
fi
if [ "$ESTABLISHED" -gt "$PEAK_CONNS" ]; then
PEAK_CONNS=$ESTABLISHED
fi
# Timestamp
local TS=$(date +%H:%M:%S)
# Print to console
printf "\r[%s] CPU: %5.1f%% | RSS: %7.2f MB | Conns: %5d | FDs: %5d | Threads: %3d" \
"$TS" "${CPU:-0}" "${RSS_MB:-0}" "$ESTABLISHED" "$FDS" "$THREADS"
# Write to log
{
echo "[$TS] Elapsed: ${ELAPSED}s"
echo " CPU: ${CPU:-0}%"
echo " RSS: ${RSS_MB:-0} MB"
echo " VSZ: ${VSZ_MB:-0} MB"
echo " Threads: $THREADS"
echo " Established: $ESTABLISHED"
echo " TIME_WAIT: $TIME_WAIT"
echo " FDs: $FDS"
echo " Total TCP: $TOTAL_TCP"
echo ""
} >> "$LOGFILE"
# Write to CSV
echo "$TS,$ELAPSED,${CPU:-0},${RSS_MB:-0},${VSZ_MB:-0},$THREADS,$ESTABLISHED,$TIME_WAIT,$FDS,$TOTAL_TCP" >> "$CSVFILE"
}
# Main capture loop
ELAPSED=0
while [ $ELAPSED -lt $DURATION ]; do
# Check if process still exists
if ! kill -0 $SERVER_PID 2>/dev/null; then
echo ""
echo "⚠️ Process $SERVER_PID terminated"
break
fi
capture_sample $ELAPSED
sleep $INTERVAL
ELAPSED=$((ELAPSED + INTERVAL))
done
echo ""
echo ""
# ═══════════════════════════════════════════════════════════════════════════
# Calculate summary statistics
# ═══════════════════════════════════════════════════════════════════════════
if [ $SAMPLE_COUNT -gt 0 ]; then
AVG_CPU=$(echo "scale=2; $TOTAL_CPU / $SAMPLE_COUNT" | bc)
AVG_RSS=$(echo "scale=2; $TOTAL_RSS / $SAMPLE_COUNT" | bc)
AVG_CONNS=$(echo "scale=0; $TOTAL_CONNS / $SAMPLE_COUNT" | bc)
# Calculate per-user metrics (using peak connections as user count)
if [ $PEAK_CONNS -gt 0 ]; then
CPU_PER_USER=$(echo "scale=4; $PEAK_CPU / $PEAK_CONNS" | bc)
RSS_PER_USER_KB=$(echo "scale=2; ($PEAK_RSS - $BASELINE_RSS_MB) * 1024 / $PEAK_CONNS" | bc)
RSS_PER_USER_MB=$(echo "scale=4; ($PEAK_RSS - $BASELINE_RSS_MB) / $PEAK_CONNS" | bc)
else
CPU_PER_USER="N/A"
RSS_PER_USER_KB="N/A"
RSS_PER_USER_MB="N/A"
fi
# Get system info
TOTAL_RAM_MB=$(sysctl -n hw.memsize 2>/dev/null | awk '{print $1/1024/1024}')
CPU_CORES=$(sysctl -n hw.ncpu 2>/dev/null)
FD_LIMIT=$(ulimit -n)
SOMAXCONN=$(sysctl -n kern.ipc.somaxconn 2>/dev/null)
# Calculate projected capacity
if [ "$RSS_PER_USER_KB" != "N/A" ] && [ $(echo "$RSS_PER_USER_KB > 0" | bc) -eq 1 ]; then
# RAM-based max (using 80% of total RAM)
AVAILABLE_RAM=$(echo "scale=0; $TOTAL_RAM_MB * 0.8" | bc)
MAX_USERS_RAM=$(echo "scale=0; ($AVAILABLE_RAM - $BASELINE_RSS_MB) * 1024 / $RSS_PER_USER_KB" | bc)
else
MAX_USERS_RAM="N/A"
fi
if [ "$CPU_PER_USER" != "N/A" ] && [ $(echo "$CPU_PER_USER > 0" | bc) -eq 1 ]; then
# CPU-based max (using 80% of all cores)
AVAILABLE_CPU=$(echo "scale=0; $CPU_CORES * 80" | bc)
MAX_USERS_CPU=$(echo "scale=0; $AVAILABLE_CPU / $CPU_PER_USER" | bc)
else
MAX_USERS_CPU="N/A"
fi
# FD-based max
MAX_USERS_FD=$((FD_LIMIT - 100))
# Overall max is minimum of all limits
# (simplified - just report each separately)
# Generate report
REPORT=$(cat << EOF
═══════════════════════════════════════════════════════════════════════════════
📊 SERVER CAPACITY ANALYSIS REPORT
═══════════════════════════════════════════════════════════════════════════════
SYSTEM INFORMATION
───────────────────────────────────────────────────────────────────────────────
Total RAM: ${TOTAL_RAM_MB} MB
CPU Cores: ${CPU_CORES}
FD Limit: ${FD_LIMIT}
somaxconn: ${SOMAXCONN}
BASELINE METRICS (before load)
───────────────────────────────────────────────────────────────────────────────
Baseline RSS: ${BASELINE_RSS_MB} MB
OBSERVED METRICS (during ${SAMPLE_COUNT} samples over ${DURATION}s)
───────────────────────────────────────────────────────────────────────────────
AVERAGE PEAK
CPU: ${AVG_CPU}% ${PEAK_CPU}%
RSS: ${AVG_RSS} MB ${PEAK_RSS} MB
Connections: ${AVG_CONNS} ${PEAK_CONNS}
PER-USER RESOURCE CONSUMPTION (at peak ${PEAK_CONNS} users)
───────────────────────────────────────────────────────────────────────────────
CPU per user: ${CPU_PER_USER}%
RAM per user: ${RSS_PER_USER_KB} KB (${RSS_PER_USER_MB} MB)
PROJECTED CAPACITY LIMITS
───────────────────────────────────────────────────────────────────────────────
Max users (RAM @ 80%): ${MAX_USERS_RAM}
Max users (CPU @ 80%): ${MAX_USERS_CPU}
Max users (FD limit): ${MAX_USERS_FD}
Max users (somaxconn): ${SOMAXCONN}
RECOMMENDED SERVER SPECS BY USER COUNT
───────────────────────────────────────────────────────────────────────────────
┌────────────┬────────────┬────────────┬────────────────────────────────────┐
│ Users │ RAM Needed │ CPU Cores │ Notes │
├────────────┼────────────┼────────────┼────────────────────────────────────┤
EOF
)
# Calculate spec table dynamically
for USERS in 1000 2500 5000 10000 25000 50000 100000; do
if [ "$RSS_PER_USER_KB" != "N/A" ] && [ "$CPU_PER_USER" != "N/A" ]; then
RAM_NEEDED=$(echo "scale=0; $BASELINE_RSS_MB + ($USERS * $RSS_PER_USER_KB / 1024)" | bc)
CPU_NEEDED=$(echo "scale=1; $USERS * $CPU_PER_USER / 100" | bc)
CPU_CORES_NEEDED=$(echo "scale=0; ($CPU_NEEDED + 0.9) / 1" | bc) # Round up
if [ $CPU_CORES_NEEDED -lt 1 ]; then
CPU_CORES_NEEDED=1
fi
# Add notes based on scale
if [ $USERS -le $PEAK_CONNS ]; then
NOTES="✅ Tested stable"
elif [ $USERS -le 5000 ]; then
NOTES="Likely achievable"
elif [ $USERS -le 20000 ]; then
NOTES="Needs optimization"
else
NOTES="Consider clustering"
fi
REPORT="${REPORT}
│ $(printf '%10d' $USERS) │ $(printf '%7d MB' $RAM_NEEDED) │ $(printf '%10.0f' $CPU_CORES_NEEDED) │ $(printf '%-34s' "$NOTES") │"
fi
done
REPORT="${REPORT}
└────────────┴────────────┴────────────┴────────────────────────────────────┘
FILES GENERATED
───────────────────────────────────────────────────────────────────────────────
Detailed log: $LOGFILE
CSV data: $CSVFILE
═══════════════════════════════════════════════════════════════════════════════
EOF
# Print and save report
echo "$REPORT"
echo "$REPORT" >> "$LOGFILE"
# Also save a summary file
SUMMARY_FILE="capacity_summary_$(date +%Y%m%d_%H%M%S).txt"
echo "$REPORT" > "$SUMMARY_FILE"
echo ""
echo "📁 Summary saved to: $SUMMARY_FILE"
else
echo "❌ No samples captured"
fi