Files
lxs/tools/server-benchmark.sh
hykocx 2a50724328 fix(benchmark): prevent disk exhaustion during benchmark runs
- add EXIT/INT/TERM trap to clean up /tmp artifacts on script exit or interrupt
- introduce DISK_SAFETY_MARGIN_MB constant to reserve 200MB free at all times
- build disk test configurations dynamically based on available space instead of a fixed list
- re-check free space before each test pass and skip configs that no longer fit
- return early with SCORE_DISK_WRITE=0 when no test fits in available space
2026-05-12 22:43:14 -04:00

816 lines
32 KiB
Bash
Executable File

#!/bin/bash
# LXS - Server Benchmark Tool
# Description: Performance testing and benchmarking tool
# Author: LXS
# Date: 2025
# Load LXS common library (colors, separator, run_spinner, loggers)
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
eval "$_lib"
unset _lib
export LXS_LOG_FILE="/tmp/lxs_benchmark.log"
# Always clean up benchmark artifacts on exit, even on error or Ctrl+C. Without
# this, a partial 1-2GB dd file in /tmp can stay behind and fill the disk on
# small servers if the script is re-run.
cleanup_benchmark_artifacts() {
rm -f /tmp/lxs_test_write_* \
/tmp/lxs_test_read_* \
/tmp/lxs_write_result_*.tmp \
/tmp/lxs_read_result_*.tmp \
/tmp/lxs_cpu_result.tmp \
/tmp/lxs_ram_result.tmp \
/tmp/lxs_ping_result.tmp \
/tmp/lxs_ping_*.tmp 2>/dev/null || true
}
trap cleanup_benchmark_artifacts EXIT INT TERM
# Minimum free space (MB) to keep available during disk tests. If /tmp drops
# below this threshold mid-run we abort the remaining passes.
DISK_SAFETY_MARGIN_MB=200
# ═══════════════════════════════════════════════════════════════════════════
# Score Variables
# ═══════════════════════════════════════════════════════════════════════════
SCORE_CPU=0
SCORE_RAM=0
SCORE_DISK_WRITE=0
SCORE_DISK_READ=0
SCORE_NETWORK=0
SCORE_NETWORK_LATENCY=0
# ═══════════════════════════════════════════════════════════════════════════
# Helper Functions
# ═══════════════════════════════════════════════════════════════════════════
show_header() {
show_box_top "SERVER PERFORMANCE BENCHMARK"
}
install_dependencies() {
local deps_installed=true
# Check for sysbench
if ! command -v sysbench &> /dev/null; then
echo -e "${YELLOW}[!] sysbench not found, installing...${NC}"
echo ""
require_disk_space 300 || return 1
if command -v apt-get &> /dev/null; then
run_spinner "Updating package list..." "apt-get update -qq"
run_spinner "Installing sysbench..." "DEBIAN_FRONTEND=noninteractive apt-get install -y -qq sysbench"
elif command -v yum &> /dev/null; then
run_spinner "Installing sysbench..." "yum install -y -q sysbench"
elif command -v dnf &> /dev/null; then
run_spinner "Installing sysbench..." "dnf install -y -q sysbench"
else
echo -e "${RED}[✗] Unable to install sysbench automatically${NC}"
deps_installed=false
fi
# Verify installation
if ! command -v sysbench &> /dev/null; then
deps_installed=false
fi
else
echo -e "${GREEN}[✓] All dependencies ready${NC}"
fi
if [ "$deps_installed" = true ]; then
echo ""
return 0
else
echo -e "${RED}[✗] Failed to install required dependencies${NC}"
return 1
fi
}
# ═══════════════════════════════════════════════════════════════════════════
# System Information
# ═══════════════════════════════════════════════════════════════════════════
show_system_info() {
echo -e "${WHITE}${BOLD}SYSTEM INFORMATION${NC}"
show_separator
local hostname=$(hostname)
local cpu_model=$(grep "model name" /proc/cpuinfo | head -1 | cut -d':' -f2 | xargs)
local cpu_cores=$(nproc)
local total_mem=$(free -h | awk '/^Mem:/ {print $2}')
local os_version=$(lsb_release -ds 2>/dev/null || cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)
local kernel=$(uname -r)
local disk_info=$(df -h / | awk 'NR==2 {print $2}')
echo -e "${GRAY}Hostname:${NC} $hostname"
echo -e "${GRAY}OS:${NC} $os_version"
echo -e "${GRAY}Kernel:${NC} $kernel"
echo -e "${GRAY}CPU:${NC} $cpu_model"
echo -e "${GRAY}vCPU Cores:${NC} $cpu_cores"
echo -e "${GRAY}RAM:${NC} $total_mem"
echo -e "${GRAY}Disk Size:${NC} $disk_info"
echo -e "${GRAY}Date:${NC} $(date '+%Y-%m-%d %H:%M:%S')"
echo ""
}
# ═══════════════════════════════════════════════════════════════════════════
# Benchmark Tests
# ═══════════════════════════════════════════════════════════════════════════
test_cpu() {
echo -e "${WHITE}${BOLD}CPU PERFORMANCE TEST${NC}"
show_separator
local cpu_cores=$(nproc)
# Run sysbench CPU test with spinner
local temp_file="/tmp/lxs_cpu_result.tmp"
echo -e "${PURPLE}[*] Testing CPU performance (prime number calculation)...${NC}"
sysbench cpu --cpu-max-prime=20000 --threads=$cpu_cores run 2>/dev/null > "$temp_file" &
local pid=$!
local spinstr='|/-\'
while kill -0 $pid 2>/dev/null; do
local temp=${spinstr#?}
printf "\r${PURPLE}[%c]${NC} Testing CPU performance (prime number calculation)..." "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep 0.15
done
wait $pid
CPU_RESULT=$(grep "events per second" "$temp_file" | awk '{print $4}')
rm -f "$temp_file"
if [ -z "$CPU_RESULT" ]; then
printf "\r${RED}[✗]${NC} CPU test failed \n"
SCORE_CPU=0
else
printf "\r${GREEN}[✓]${NC} CPU test completed \n"
SCORE_CPU=$(echo "$CPU_RESULT" | awk '{printf "%.0f", $1}')
echo -e "${GREEN} Events per second:${NC} $CPU_RESULT"
echo -e "${CYAN} CPU Score:${NC} ${BOLD}$SCORE_CPU points${NC}"
fi
echo ""
}
test_memory() {
echo -e "${WHITE}${BOLD}MEMORY PERFORMANCE TEST${NC}"
show_separator
local cpu_cores=$(nproc)
# Run sysbench memory test with spinner
local temp_file="/tmp/lxs_ram_result.tmp"
echo -e "${PURPLE}[*] Testing memory performance (transfer speed)...${NC}"
sysbench memory --memory-total-size=5G --threads=$cpu_cores run 2>/dev/null > "$temp_file" &
local pid=$!
local spinstr='|/-\'
while kill -0 $pid 2>/dev/null; do
local temp=${spinstr#?}
printf "\r${PURPLE}[%c]${NC} Testing memory performance (transfer speed)..." "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep 0.15
done
wait $pid
RAM_RESULT=$(grep "transferred" "$temp_file" | awk '{print $(NF-1)}' | sed 's/[^0-9.]//g')
rm -f "$temp_file"
if [ -z "$RAM_RESULT" ]; then
printf "\r${RED}[✗]${NC} Memory test failed \n"
SCORE_RAM=0
else
printf "\r${GREEN}[✓]${NC} Memory test completed \n"
SCORE_RAM=$(echo "$RAM_RESULT" | awk '{printf "%.0f", $1}')
echo -e "${GREEN} Transfer speed:${NC} $RAM_RESULT MiB/sec"
echo -e "${CYAN} RAM Score:${NC} ${BOLD}$SCORE_RAM points${NC}"
fi
echo ""
}
test_disk_write() {
echo -e "${WHITE}${BOLD}DISK WRITE PERFORMANCE TEST${NC}"
show_separator
# Check available disk space in /tmp
local available_space=$(df -BM /tmp | awk 'NR==2 {print $4}' | sed 's/M//')
# Build test configurations dynamically based on free space. Each test needs
# room for its file PLUS the safety margin, otherwise dd can fill the disk
# on small servers (1-2GB VPS). Tests are added from smallest to largest.
local test_configs=()
if [ "$available_space" -gt $((100 + DISK_SAFETY_MARGIN_MB)) ]; then
test_configs+=("100:1M:100MB Sequential")
fi
if [ "$available_space" -gt $((1024 + DISK_SAFETY_MARGIN_MB)) ]; then
test_configs+=("1024:1M:1GB Sequential")
fi
if [ "$available_space" -gt $((2048 + DISK_SAFETY_MARGIN_MB)) ]; then
test_configs+=("2048:1M:2GB Sequential")
fi
if [ ${#test_configs[@]} -eq 0 ]; then
echo -e "${RED}[✗] Not enough free space on /tmp (${available_space}MB) to run any write test${NC}"
SCORE_DISK_WRITE=0
echo ""
return 0
fi
local all_speeds=()
local all_speeds_mb=()
local test_count=0
local spinstr='|/-\'
local test_count_display=$((${#test_configs[@]} * 3))
echo -e "${PURPLE}[*] Running advanced write tests (${#test_configs[@]} sizes x 3 passes = ${test_count_display} tests)...${NC}"
if [ "$available_space" -le 3072 ]; then
echo -e "${YELLOW}[!] Limited disk space (${available_space}MB free), running reduced test set${NC}"
fi
echo ""
# Run tests for each configuration
for config in "${test_configs[@]}"; do
local size_mb=$(echo "$config" | cut -d':' -f1)
local block_size=$(echo "$config" | cut -d':' -f2)
local description=$(echo "$config" | cut -d':' -f3)
local pass_speeds=()
local pass_count=0
# Re-check free space before each config; the previous test may have
# left the filesystem tighter than expected (cache, logs, etc.).
local current_free=$(df -BM /tmp | awk 'NR==2 {print $4}' | sed 's/M//')
if [ "$current_free" -lt $((size_mb + DISK_SAFETY_MARGIN_MB)) ]; then
printf "\r${YELLOW}[!]${NC} ${description}: Skipped (only ${current_free}MB free)\n"
continue
fi
# Run 3 passes for each configuration
for pass in 1 2 3; do
# Clean up before test
rm -f /tmp/lxs_test_write_${size_mb}
sync
# Clear cache
echo 3 > /proc/sys/vm/drop_caches 2>/dev/null
sleep 0.5
local temp_file="/tmp/lxs_write_result_${size_mb}_${pass}.tmp"
# Run dd test with direct I/O (suppress error messages)
(dd if=/dev/zero of=/tmp/lxs_test_write_${size_mb} bs=${block_size} count=${size_mb} oflag=direct 2>"$temp_file" || true) &
local pid=$!
while kill -0 $pid 2>/dev/null; do
local temp=${spinstr#?}
printf "\r${PURPLE}[%c]${NC} Testing ${description} (Pass ${pass}/3)..." "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep 0.15
done
wait $pid
local exit_code=$?
# Parse result
local speed_value=""
local speed_unit=""
if [ $exit_code -eq 0 ]; then
local dd_output=$(cat "$temp_file")
speed_value=$(echo "$dd_output" | tail -1 | grep -Eo '[0-9]+\.?[0-9]* [MGK]B/s' | awk '{print $1}')
speed_unit=$(echo "$dd_output" | tail -1 | grep -Eo '[0-9]+\.?[0-9]* [MGK]B/s' | awk '{print $2}')
fi
# If direct I/O failed, try with sync
if [ -z "$speed_value" ] || [ "$speed_value" = "0" ] || [ $exit_code -ne 0 ]; then
rm -f /tmp/lxs_test_write_${size_mb}
(dd if=/dev/zero of=/tmp/lxs_test_write_${size_mb} bs=${block_size} count=${size_mb} conv=fdatasync 2>"$temp_file" || true) &
pid=$!
while kill -0 $pid 2>/dev/null; do
local temp=${spinstr#?}
printf "\r${PURPLE}[%c]${NC} Testing ${description} (Pass ${pass}/3)..." "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep 0.15
done
wait $pid
sync
local dd_output=$(cat "$temp_file")
speed_value=$(echo "$dd_output" | tail -1 | grep -Eo '[0-9]+\.?[0-9]* [MGK]B/s' | awk '{print $1}')
speed_unit=$(echo "$dd_output" | tail -1 | grep -Eo '[0-9]+\.?[0-9]* [MGK]B/s' | awk '{print $2}')
fi
rm -f "$temp_file"
# Convert to MB/s
local speed_mb=0
if [ ! -z "$speed_value" ] && [ "$speed_value" != "0" ]; then
if [ "$speed_unit" = "GB/s" ]; then
speed_mb=$(echo "$speed_value" | awk '{printf "%.2f", $1 * 1024}')
elif [ "$speed_unit" = "KB/s" ]; then
speed_mb=$(echo "$speed_value" | awk '{printf "%.2f", $1 / 1024}')
else
speed_mb=$(echo "$speed_value" | awk '{printf "%.2f", $1}')
fi
pass_speeds+=($speed_mb)
((pass_count++))
fi
# Clean up test file
rm -f /tmp/lxs_test_write_${size_mb}
done
# Calculate average for this configuration
if [ $pass_count -gt 0 ]; then
local sum=0
for speed in "${pass_speeds[@]}"; do
sum=$(echo "$sum + $speed" | bc -l)
done
local avg=$(echo "scale=2; $sum / $pass_count" | bc -l)
printf "\r${GREEN}[✓]${NC} ${description}: ${GREEN}${avg} MB/s${NC} (avg of ${pass_count} passes)\n"
all_speeds_mb+=($avg)
((test_count++))
else
printf "\r${YELLOW}[!]${NC} ${description}: Test failed\n"
fi
done
echo ""
# Calculate final score
if [ $test_count -gt 0 ]; then
local total_speed=0
for speed in "${all_speeds_mb[@]}"; do
total_speed=$(echo "$total_speed + $speed" | bc -l)
done
local avg_speed=$(echo "scale=2; $total_speed / $test_count" | bc -l)
SCORE_DISK_WRITE=$(echo "$avg_speed" | awk '{printf "%.0f", $1}')
echo -e "${CYAN} Average Write Speed:${NC} ${BOLD}${avg_speed} MB/s${NC}"
echo -e "${CYAN} Write Score:${NC} ${BOLD}$SCORE_DISK_WRITE points${NC}"
# Quality assessment
if (( $(echo "$avg_speed >= 1000" | bc -l) )); then
echo -e "${GREEN} Quality:${NC} Excellent (NVMe SSD)"
elif (( $(echo "$avg_speed >= 400" | bc -l) )); then
echo -e "${GREEN} Quality:${NC} Very Good (SATA SSD)"
elif (( $(echo "$avg_speed >= 100" | bc -l) )); then
echo -e "${CYAN} Quality:${NC} Good (Fast HDD/Basic SSD)"
else
echo -e "${YELLOW} Quality:${NC} Standard (HDD)"
fi
else
echo -e "${RED}[✗] All write tests failed${NC}"
SCORE_DISK_WRITE=0
fi
echo ""
}
test_disk_read() {
echo -e "${WHITE}${BOLD}DISK READ PERFORMANCE TEST${NC}"
show_separator
# Check available disk space in /tmp
local available_space=$(df -BM /tmp | awk 'NR==2 {print $4}' | sed 's/M//')
# Build test configurations dynamically based on free space (see write test
# for rationale). The read test also writes a source file first, so the
# space requirement is the same as the write test.
local test_configs=()
if [ "$available_space" -gt $((100 + DISK_SAFETY_MARGIN_MB)) ]; then
test_configs+=("100:1M:100MB Sequential")
fi
if [ "$available_space" -gt $((1024 + DISK_SAFETY_MARGIN_MB)) ]; then
test_configs+=("1024:1M:1GB Sequential")
fi
if [ "$available_space" -gt $((2048 + DISK_SAFETY_MARGIN_MB)) ]; then
test_configs+=("2048:1M:2GB Sequential")
fi
if [ ${#test_configs[@]} -eq 0 ]; then
echo -e "${RED}[✗] Not enough free space on /tmp (${available_space}MB) to run any read test${NC}"
SCORE_DISK_READ=0
echo ""
return 0
fi
local all_speeds_mb=()
local test_count=0
local spinstr='|/-\'
local test_count_display=$((${#test_configs[@]} * 3))
echo -e "${PURPLE}[*] Running advanced read tests (${#test_configs[@]} sizes x 3 passes = ${test_count_display} tests)...${NC}"
if [ "$available_space" -le 3072 ]; then
echo -e "${YELLOW}[!] Limited disk space (${available_space}MB free), running reduced test set${NC}"
fi
echo ""
# Run tests for each configuration
for config in "${test_configs[@]}"; do
local size_mb=$(echo "$config" | cut -d':' -f1)
local block_size=$(echo "$config" | cut -d':' -f2)
local description=$(echo "$config" | cut -d':' -f3)
# Re-check free space before each config (same rationale as write test).
local current_free=$(df -BM /tmp | awk 'NR==2 {print $4}' | sed 's/M//')
if [ "$current_free" -lt $((size_mb + DISK_SAFETY_MARGIN_MB)) ]; then
printf "\r${YELLOW}[!]${NC} ${description}: Skipped (only ${current_free}MB free)\n"
continue
fi
# Create test file if it doesn't exist
if [ ! -f /tmp/lxs_test_read_${size_mb} ]; then
dd if=/dev/zero of=/tmp/lxs_test_read_${size_mb} bs=${block_size} count=${size_mb} 2>/dev/null || {
# If file creation fails, skip this test and remove any partial file
rm -f /tmp/lxs_test_read_${size_mb}
continue
}
sync
fi
local pass_speeds=()
local pass_count=0
# Run 3 passes for each configuration
for pass in 1 2 3; do
# Clear cache before each test
sync
echo 3 > /proc/sys/vm/drop_caches 2>/dev/null
sleep 0.5
local temp_file="/tmp/lxs_read_result_${size_mb}_${pass}.tmp"
# Run dd read test with direct I/O (suppress error messages)
(dd if=/tmp/lxs_test_read_${size_mb} of=/dev/null bs=${block_size} iflag=direct 2>"$temp_file" || true) &
local pid=$!
while kill -0 $pid 2>/dev/null; do
local temp=${spinstr#?}
printf "\r${PURPLE}[%c]${NC} Testing ${description} (Pass ${pass}/3)..." "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep 0.15
done
wait $pid
local exit_code=$?
# Parse result
local speed_value=""
local speed_unit=""
if [ $exit_code -eq 0 ]; then
local dd_output=$(cat "$temp_file")
speed_value=$(echo "$dd_output" | tail -1 | grep -Eo '[0-9]+\.?[0-9]* [MGK]B/s' | awk '{print $1}')
speed_unit=$(echo "$dd_output" | tail -1 | grep -Eo '[0-9]+\.?[0-9]* [MGK]B/s' | awk '{print $2}')
fi
# If direct I/O failed, try without it
if [ -z "$speed_value" ] || [ "$speed_value" = "0" ] || [ $exit_code -ne 0 ]; then
# Clear cache again
sync
echo 3 > /proc/sys/vm/drop_caches 2>/dev/null
(dd if=/tmp/lxs_test_read_${size_mb} of=/dev/null bs=${block_size} 2>"$temp_file" || true) &
pid=$!
while kill -0 $pid 2>/dev/null; do
local temp=${spinstr#?}
printf "\r${PURPLE}[%c]${NC} Testing ${description} (Pass ${pass}/3)..." "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep 0.15
done
wait $pid
local dd_output=$(cat "$temp_file")
speed_value=$(echo "$dd_output" | tail -1 | grep -Eo '[0-9]+\.?[0-9]* [MGK]B/s' | awk '{print $1}')
speed_unit=$(echo "$dd_output" | tail -1 | grep -Eo '[0-9]+\.?[0-9]* [MGK]B/s' | awk '{print $2}')
fi
rm -f "$temp_file"
# Convert to MB/s
local speed_mb=0
if [ ! -z "$speed_value" ] && [ "$speed_value" != "0" ]; then
if [ "$speed_unit" = "GB/s" ]; then
speed_mb=$(echo "$speed_value" | awk '{printf "%.2f", $1 * 1024}')
elif [ "$speed_unit" = "KB/s" ]; then
speed_mb=$(echo "$speed_value" | awk '{printf "%.2f", $1 / 1024}')
else
speed_mb=$(echo "$speed_value" | awk '{printf "%.2f", $1}')
fi
pass_speeds+=($speed_mb)
((pass_count++))
fi
done
# Calculate average for this configuration
if [ $pass_count -gt 0 ]; then
local sum=0
for speed in "${pass_speeds[@]}"; do
sum=$(echo "$sum + $speed" | bc -l)
done
local avg=$(echo "scale=2; $sum / $pass_count" | bc -l)
printf "\r${GREEN}[✓]${NC} ${description}: ${GREEN}${avg} MB/s${NC} (avg of ${pass_count} passes)\n"
all_speeds_mb+=($avg)
((test_count++))
else
printf "\r${YELLOW}[!]${NC} ${description}: Test failed\n"
fi
# Clean up test file
rm -f /tmp/lxs_test_read_${size_mb}
done
echo ""
# Calculate final score
if [ $test_count -gt 0 ]; then
local total_speed=0
for speed in "${all_speeds_mb[@]}"; do
total_speed=$(echo "$total_speed + $speed" | bc -l)
done
local avg_speed=$(echo "scale=2; $total_speed / $test_count" | bc -l)
SCORE_DISK_READ=$(echo "$avg_speed" | awk '{printf "%.0f", $1}')
echo -e "${CYAN} Average Read Speed:${NC} ${BOLD}${avg_speed} MB/s${NC}"
echo -e "${CYAN} Read Score:${NC} ${BOLD}$SCORE_DISK_READ points${NC}"
# Quality assessment
if (( $(echo "$avg_speed >= 2000" | bc -l) )); then
echo -e "${GREEN} Quality:${NC} Excellent (High-end NVMe SSD)"
elif (( $(echo "$avg_speed >= 500" | bc -l) )); then
echo -e "${GREEN} Quality:${NC} Very Good (NVMe/Fast SATA SSD)"
elif (( $(echo "$avg_speed >= 150" | bc -l) )); then
echo -e "${CYAN} Quality:${NC} Good (SATA SSD)"
else
echo -e "${YELLOW} Quality:${NC} Standard (HDD)"
fi
else
echo -e "${RED}[✗] All read tests failed${NC}"
SCORE_DISK_READ=0
fi
echo ""
}
test_network() {
echo -e "${WHITE}${BOLD}NETWORK PERFORMANCE TEST${NC}"
show_separator
# Test ping to common servers with spinner
local temp_file="/tmp/lxs_ping_result.tmp"
echo -e "${PURPLE}[*] Testing network latency and speed...${NC}"
ping -c 5 8.8.8.8 2>/dev/null > "$temp_file" &
local pid=$!
local spinstr='|/-\'
while kill -0 $pid 2>/dev/null; do
local temp=${spinstr#?}
printf "\r${PURPLE}[%c]${NC} Testing network latency and speed..." "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep 0.15
done
wait $pid
local ping_test=$(tail -1 "$temp_file" | awk -F '/' '{print $5}')
rm -f "$temp_file"
if [ -z "$ping_test" ]; then
printf "\r${YELLOW}[!]${NC} Network test skipped (no connectivity) \n"
SCORE_NETWORK=0
else
printf "\r${GREEN}[✓]${NC} Network test completed \n"
# Convert latency to score (lower is better, so invert)
# Score = 1000 / latency (max 100 points for <10ms)
SCORE_NETWORK=$(echo "$ping_test" | awk '{score = 1000 / $1; if (score > 100) score = 100; printf "%.0f", score}')
echo -e "${GREEN} Average latency to 8.8.8.8:${NC} $ping_test ms"
echo -e "${CYAN} Network Score:${NC} ${BOLD}$SCORE_NETWORK points${NC}"
fi
echo ""
}
test_network_latency() {
echo -e "${WHITE}${BOLD}GEOGRAPHIC NETWORK LATENCY TEST${NC}"
show_separator
# Define test locations with multiple servers for redundancy
# Format: "City|Server1,Server2,Server3"
local test_locations=(
"Montreal|ec2.ca-central-1.amazonaws.com,objectstorage.ca-montreal-1.oraclecloud.com"
"Toronto|objectstorage.ca-toronto-1.oraclecloud.com,tor-ca-ping.vultr.com,speedtest-tor1.digitalocean.com,speedtest.toronto1.linode.com"
"Vancouver|ec2.us-west-2.amazonaws.com"
"New York|ec2.us-east-1.amazonaws.com,speedtest-nyc1.digitalocean.com"
"Texas|tx-us-ping.vultr.com"
)
local total_latency=0
local successful_tests=0
local failed_count=0
echo -e "${PURPLE}[*] Testing latency to multiple geographic locations...${NC}"
echo ""
# Test each location
for location in "${test_locations[@]}"; do
local city=$(echo "$location" | cut -d'|' -f1)
local servers=$(echo "$location" | cut -d'|' -f2)
local city_latency=""
local city_success=false
# Try each server for the city until one succeeds
IFS=',' read -ra SERVER_ARRAY <<< "$servers"
for server in "${SERVER_ARRAY[@]}"; do
local temp_file="/tmp/lxs_ping_${city}_${server}.tmp"
# Ping with timeout (3 pings for faster results)
timeout 8 ping -c 3 -W 2 "$server" 2>/dev/null > "$temp_file" &
local pid=$!
local spinstr='|/-\'
while kill -0 $pid 2>/dev/null; do
local temp=${spinstr#?}
printf "\r${PURPLE}[%c]${NC} Testing ${city}..." "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep 0.1
done
wait $pid
local exit_code=$?
# Parse result
local avg_latency=$(tail -1 "$temp_file" 2>/dev/null | awk -F '/' '{print $5}')
rm -f "$temp_file"
# Check if we got a valid result
if [ ! -z "$avg_latency" ] && [ "$avg_latency" != "0" ] && [ $exit_code -eq 0 ]; then
city_latency="$avg_latency"
city_success=true
break
fi
done
# Display result for this city
if [ "$city_success" = true ]; then
printf "\r${GREEN}[✓]${NC} %-12s ${GREEN}%6s ms${NC}\n" "$city:" "$city_latency"
total_latency=$(echo "$total_latency + $city_latency" | bc -l 2>/dev/null || echo "$total_latency")
((successful_tests++))
else
printf "\r${YELLOW}[!]${NC} %-12s ${YELLOW}Unreachable${NC}\n" "$city:"
((failed_count++))
fi
done
echo ""
# Calculate score
if [ $successful_tests -gt 0 ]; then
local avg_total=$(echo "scale=2; $total_latency / $successful_tests" | bc -l)
# Score calculation: Lower latency = higher score
# Perfect score (100) for avg latency <= 20ms
# Score decreases as latency increases
# Formula: max(0, 100 - ((avg_latency - 20) * 0.5))
SCORE_NETWORK_LATENCY=$(echo "$avg_total" | awk '{
if ($1 <= 20) score = 100;
else if ($1 >= 220) score = 0;
else score = 100 - (($1 - 20) * 0.5);
printf "%.0f", score
}')
local total_locations=$((successful_tests + failed_count))
echo -e "${CYAN} Average Latency:${NC} ${avg_total} ms (${successful_tests}/${total_locations} locations)"
echo -e "${CYAN} Latency Score:${NC} ${BOLD}$SCORE_NETWORK_LATENCY points${NC}"
# Provide context on latency quality
if (( $(echo "$avg_total <= 50" | bc -l) )); then
echo -e "${GREEN} Quality:${NC} Excellent latency across regions"
elif (( $(echo "$avg_total <= 100" | bc -l) )); then
echo -e "${CYAN} Quality:${NC} Good latency across regions"
elif (( $(echo "$avg_total <= 150" | bc -l) )); then
echo -e "${YELLOW} Quality:${NC} Average latency across regions"
else
echo -e "${RED} Quality:${NC} High latency detected"
fi
else
echo -e "${RED}[✗] All latency tests failed${NC}"
SCORE_NETWORK_LATENCY=0
fi
echo ""
}
# ═══════════════════════════════════════════════════════════════════════════
# Results Display
# ═══════════════════════════════════════════════════════════════════════════
show_results() {
show_separator
echo -e "${WHITE}${BOLD}BENCHMARK RESULTS${NC}"
show_separator
echo ""
echo -e "${GRAY}CPU Score:${NC} ${BOLD}$SCORE_CPU${NC} points"
echo -e "${GRAY}RAM Score:${NC} ${BOLD}$SCORE_RAM${NC} points"
echo -e "${GRAY}Disk Write Score:${NC} ${BOLD}$SCORE_DISK_WRITE${NC} points"
echo -e "${GRAY}Disk Read Score:${NC} ${BOLD}$SCORE_DISK_READ${NC} points"
echo -e "${GRAY}Network Score:${NC} ${BOLD}$SCORE_NETWORK${NC} points"
echo -e "${GRAY}Network Latency Score:${NC} ${BOLD}$SCORE_NETWORK_LATENCY${NC} points"
echo ""
show_separator
# Calculate final score with weighted average
# CPU = 30%, RAM = 20%, Disk Write = 15%, Disk Read = 15%, Network = 10%, Latency = 10%
SCORE_FINAL=$(awk "BEGIN {printf \"%.0f\", ($SCORE_CPU * 0.30) + ($SCORE_RAM * 0.20) + ($SCORE_DISK_WRITE * 0.15) + ($SCORE_DISK_READ * 0.15) + ($SCORE_NETWORK * 0.10) + ($SCORE_NETWORK_LATENCY * 0.10)}")
echo -e "${WHITE}${BOLD}FINAL BENCHMARK SCORE${NC}"
show_separator
echo ""
echo -e "${GREEN}${BOLD}$SCORE_FINAL points${NC}"
echo ""
show_separator
echo ""
# Performance rating
if [ "$SCORE_FINAL" -ge 5000 ]; then
echo -e "${GREEN}${BOLD}Performance Rating: [★★★★★] Exceptional${NC}"
elif [ "$SCORE_FINAL" -ge 3000 ]; then
echo -e "${GREEN}${BOLD}Performance Rating: [★★★★☆] Excellent${NC}"
elif [ "$SCORE_FINAL" -ge 2000 ]; then
echo -e "${CYAN}${BOLD}Performance Rating: [★★★☆☆] Good${NC}"
elif [ "$SCORE_FINAL" -ge 1000 ]; then
echo -e "${YELLOW}${BOLD}Performance Rating: [★★☆☆☆] Average${NC}"
else
echo -e "${RED}${BOLD}Performance Rating: [★☆☆☆☆] Below Average${NC}"
fi
echo ""
echo -e "${GRAY}Higher scores indicate better performance.${NC}"
echo ""
}
# ═══════════════════════════════════════════════════════════════════════════
# Main Execution
# ═══════════════════════════════════════════════════════════════════════════
main() {
clear
show_header
echo ""
# Show system information
show_system_info
# Install dependencies
if ! install_dependencies; then
exit 1
fi
echo -e "${YELLOW}${BOLD}[!] Starting benchmark tests...${NC}"
echo -e "${YELLOW} This may take a few minutes.${NC}"
echo ""
# Run all tests
test_cpu
test_memory
test_disk_write
test_disk_read
test_network
test_network_latency
# Display results
show_results
# Wait for user input before exiting
echo ""
read -p "Press Enter to return to menu..."
}
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo ""
echo -e "${RED}${BOLD}[✗] ERROR: This script must be run as root${NC}"
echo ""
echo -e "${YELLOW}Please run with: sudo $0${NC}"
echo ""
exit 1
fi
# Execute main function
main
exit 75