#!/usr/bin/env bash # ============================================================================= # benchmark_network.sh — Network Condition Benchmark for fastSyncAI # # Tests fastSyncAI and other tools under various realistic network conditions: # - LAN (low latency, high bandwidth) # - WAN (high latency, moderate bandwidth) # - WAN with packet loss # - WAN with jitter # # All network-based tools (fastsync, rsync, rclone) are tested with the SAME # network conditions applied via tc netem on the loopback interface. # # Usage: # ./benchmark_network.sh [SCENARIO] [DATA_SIZE_MB] [RUN_COUNT] # # Scenarios: # --lan : 10ms RTT, no loss (typical LAN) # --wan : 100ms RTT, no loss (typical WAN) # --wan-loss-1 : 100ms RTT, 1% packet loss # --wan-loss-5 : 100ms RTT, 5% packet loss # --wan-jitter : 100ms RTT, +/-50ms jitter # --custom LAT_ms : Custom latency (e.g., --custom 50 for 50ms RTT) # # Examples: # ./benchmark_network.sh --lan # LAN test, 100MB, 3 runs # ./benchmark_network.sh --wan 200 1 # WAN test, 200MB, 1 run # ./benchmark_network.sh --wan-loss-1 50 # WAN with 1% loss, 50MB, 3 runs # ./benchmark_network.sh --wan-jitter 100 5 # WAN with jitter, 100MB, 5 runs # ./benchmark_network.sh --custom 50 100 3 # Custom 50ms RTT, 100MB, 3 runs # ============================================================================= set -euo pipefail # ── Defaults ────────────────────────────────────────────────────────────── DATA_SIZE_MB=${2:-100} RUN_COUNT=${3:-3} # Network scenario configuration SCENARIO="${1:---lan}" # ── Parse scenario ──────────────────────────────────────────────────────── LATENCY_MS=0 PACKET_LOSS="0%" JITTER_MS="0" BANDWIDTH_MBPS="0" # 0 = unlimited case "$SCENARIO" in --lan) LATENCY_MS=10 SCENARIO_NAME="LAN (10ms RTT)" ;; --wan) LATENCY_MS=100 SCENARIO_NAME="WAN (100ms RTT)" ;; --wan-loss-1) LATENCY_MS=100 PACKET_LOSS="1%" SCENARIO_NAME="WAN with 1% loss (100ms RTT)" ;; --wan-loss-5) LATENCY_MS=100 PACKET_LOSS="5%" SCENARIO_NAME="WAN with 5% loss (100ms RTT)" ;; --wan-loss-10) LATENCY_MS=100 PACKET_LOSS="10%" SCENARIO_NAME="WAN with 10% loss (100ms RTT)" ;; --wan-jitter) LATENCY_MS=100 JITTER_MS=50 SCENARIO_NAME="WAN with jitter (100ms +/-50ms RTT)" ;; --custom) if [ -n "${2:-}" ] && [[ "${2}" =~ ^[0-9]+$ ]]; then LATENCY_MS="$2" DATA_SIZE_MB=${3:-100} RUN_COUNT=${4:-3} SCENARIO_NAME="Custom (${LATENCY_MS}ms RTT)" else echo "ERROR: --custom requires latency value in ms" echo "Usage: ./benchmark_network.sh --custom LATENCY_MS [DATA_SIZE_MB] [RUN_COUNT]" exit 1 fi ;; *) echo "ERROR: Unknown scenario '$SCENARIO'" echo "Available scenarios: --lan, --wan, --wan-loss-1, --wan-loss-5, --wan-loss-10, --wan-jitter, --custom" exit 1 ;; esac # ── Ports ──────────────────────────────────────────────────────────────── FASTSYNC_PORT=19399 RSYNC_PORT=19400 RCLONE_PORT=19401 HOST="127.0.0.1" SERVER_BIN="./fastsync_server" CLIENT_BIN="./fastsync_client" # ── Directories ──────────────────────────────────────────────────────────── TEST_DIR="network_bench_$$" SRC_DIR="$TEST_DIR/src" DST_DIR_BASE="$TEST_DIR/dst" RSYNC_MODULE_DIR="$TEST_DIR/rsync_module" # ── Colours ──────────────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' # ── Helpers ──────────────────────────────────────────────────────────────── die() { echo -e "${RED}ERROR: $*${RESET}" >&2; exit 1; } info() { echo -e "${CYAN}▶ $*${RESET}"; } ok() { echo -e "${GREEN}✓ $*${RESET}"; } warn() { echo -e "${YELLOW}⚠ $*${RESET}"; } # ── Network simulation (tc netem) ──────────────────────────────────────── setup_network() { info "Setting up network simulation: ${SCENARIO_NAME}..." if ! sudo -n true 2>/dev/null; then echo -e "${YELLOW}⚠ sudo required for tc netem — enter password if prompted${RESET}" fi # Clean existing qdisc sudo tc qdisc del dev lo root 2>/dev/null || true # Build netem command local netem_cmd="delay ${LATENCY_MS}ms" if [ "$JITTER_MS" -gt 0 ]; then netem_cmd="$netem_cmd ${JITTER_MS}ms" fi if [ "$PACKET_LOSS" != "0%" ]; then netem_cmd="$netem_cmd loss ${PACKET_LOSS}" fi # Apply network conditions sudo tc qdisc add dev lo root netem $netem_cmd || \ die "tc netem failed. Install iproute2 or check parameters." ok "Network simulation active: latency=${LATENCY_MS}ms, loss=${PACKET_LOSS}, jitter=±${JITTER_MS}ms" } cleanup_network() { sudo tc qdisc del dev lo root 2>/dev/null || true } # ── Sanity checks ────────────────────────────────────────────────────────── [ -x "$SERVER_BIN" ] || die "Server binary not found: $SERVER_BIN — run 'make' first." [ -x "$CLIENT_BIN" ] || die "Client binary not found: $CLIENT_BIN — run 'make' first." command -v rsync >/dev/null 2>&1 || die "rsync not found. Install rsync first." # Check for optional tools HAS_RCLONE=$(command -v rclone >/dev/null 2>&1 && echo "1" || echo "0") # Variable to track rsyncd PID RSYNCD_PID="" cleanup() { cleanup_network # Kill rsyncd if running if [ -n "$RSYNCD_PID" ] && kill -0 "$RSYNCD_PID" 2>/dev/null; then kill "$RSYNCD_PID" 2>/dev/null || true wait "$RSYNCD_PID" 2>/dev/null || true fi pkill -f "fastsync_server" 2>/dev/null || true pkill -f "fastsync_client" 2>/dev/null || true rm -rf "$TEST_DIR" } trap cleanup EXIT # ── Start rsync daemon ──────────────────────────────────────────────────── start_rsyncd() { # Create rsyncd config file local config_file="$TEST_DIR/rsyncd.conf" cat > "$config_file" < /dev/null 2>&1 & RSYNCD_PID=$! sleep 0.5 # Verify it's running if ! kill -0 "$RSYNCD_PID" 2>/dev/null; then die "Failed to start rsync daemon" fi ok "rsync daemon started on port $RSYNC_PORT" } # ── Generate test data ────────────────────────────────────────────────────── generate_test_data() { info "Generating ~${DATA_SIZE_MB}MB test data..." rm -rf "$TEST_DIR" mkdir -p "$SRC_DIR/small" "$SRC_DIR/medium" "$SRC_DIR/large" mkdir -p "$RSYNC_MODULE_DIR" # Small files: many small files to test overhead (~25% of total) local small_count=$(( (DATA_SIZE_MB * 1048576 / 4) / 262144 )) [ "$small_count" -lt 10 ] && small_count=10 [ "$small_count" -gt 100 ] && small_count=100 info " Creating $small_count small files (256KB each)..." for i in $(seq 1 $small_count); do dd if=/dev/urandom bs=256K count=1 of="$SRC_DIR/small/file_$i.bin" 2>/dev/null done # Medium files: 1MB each (~50% of total) local medium_count=$(( (DATA_SIZE_MB * 1048576 / 2) / 1048576 )) [ "$medium_count" -lt 5 ] && medium_count=5 [ "$medium_count" -gt 20 ] && medium_count=20 info " Creating $medium_count medium files (1MB each)..." for i in $(seq 1 $medium_count); do dd if=/dev/urandom bs=1M count=1 of="$SRC_DIR/medium/file_$i.bin" 2>/dev/null done # Large files: remaining ~25% local large_size=$(( DATA_SIZE_MB - (small_count * 256 / 1024) - medium_count )) [ "$large_size" -lt 5 ] && large_size=5 local large_count=3 [ "$large_size" -lt 10 ] && large_count=1 info " Creating $large_count large files (~${large_size}MB total)..." for i in $(seq 1 $large_count); do local this_size=$(( large_size * 1048576 / large_count / 1048576 )) [ "$this_size" -lt 1 ] && this_size=1 dd if=/dev/urandom bs=1M count=$this_size of="$SRC_DIR/large/file_$i.bin" 2>/dev/null done # Also copy data to rsync module directory info " Copying data to rsync module directory..." cp -r "$SRC_DIR"/* "$RSYNC_MODULE_DIR/" 2>/dev/null local total_files total_size total_files=$(find "$SRC_DIR" -type f | wc -l) total_size=$(du -sh "$SRC_DIR" | cut -f1) ok "Generated $total_files files, $total_size total" } # ── Verify sync ──────────────────────────────────────────────────────────── verify_sync() { local src_dir=$1 local dst_dir=$2 local src_count=$(find "$src_dir" -type f | wc -l) local dst_count=$(find "$dst_dir" -type f | wc -l) [ "$src_count" -ne "$dst_count" ] && return 1 local src_size=$(find "$src_dir" -type f -exec stat -c %s {} \; | awk '{sum+=$1} END {print sum}') local dst_size=$(find "$dst_dir" -type f -exec stat -c %s {} \; | awk '{sum+=$1} END {print sum}') [ "$src_size" -ne "$dst_size" ] && return 1 return 0 } # ── Run fastSyncAI test ──────────────────────────────────────────────────── run_fastsync() { local nconn=$1 local dst_dir="$DST_DIR_BASE/fastsync_${nconn}" local server_port=$((FASTSYNC_PORT + nconn)) rm -rf "$dst_dir" mkdir -p "$dst_dir" # Start server "$SERVER_BIN" -p "$server_port" -d "$dst_dir" > /dev/null 2>&1 & local srv_pid=$! sleep 0.2 # Run client local client_output client_output=$(timeout 300 "$CLIENT_BIN" -h "$HOST" -p "$server_port" -s "$SRC_DIR" -n "$nconn" 2>&1) || true # Wait for completion sleep 3 kill "$srv_pid" 2>/dev/null || true wait "$srv_pid" 2>/dev/null || true sync # Extract results local ms mbs ms=$(echo "$client_output" | grep "^BENCH:" | grep -oP 'ms=\K[0-9.]+' || echo "0") mbs=$(echo "$client_output" | grep "^BENCH:" | grep -oP 'throughput_mbs=\K[0-9.]+' || echo "0") local verified=1 if ! verify_sync "$SRC_DIR" "$dst_dir"; then verified=0 fi echo "${ms}|${mbs}|${verified}" # Cleanup rm -rf "$dst_dir" } # ── Run rsync test over TCP (through rsyncd) ────────────────────────────── run_rsync_tcp() { local use_compress=${1:-0} local dst_dir="$DST_DIR_BASE/rsync_${use_compress}" rm -rf "$dst_dir" mkdir -p "$dst_dir" local start end start=$(date +%s%N) # Use rsync:// protocol to go through TCP on loopback (experiences network simulation) if [ "$use_compress" -eq 1 ]; then rsync -a --compress "rsync://$HOST:$RSYNC_PORT/test_module/" "$dst_dir/" > /dev/null 2>&1 else rsync -a "rsync://$HOST:$RSYNC_PORT/test_module/" "$dst_dir/" > /dev/null 2>&1 fi end=$(date +%s%N) local elapsed_ms=$(( (end - start) / 1000000 )) # Calculate throughput local total_bytes total_bytes=$(find "$SRC_DIR" -type f -exec stat -c %s {} \; | awk '{sum+=$1} END {print sum}') local mbs=0 if [ "$elapsed_ms" -gt 0 ]; then mbs=$(awk "BEGIN {printf \"%.2f\", $total_bytes / 1048576.0 / ($elapsed_ms / 1000.0)}") fi local verified=1 if ! verify_sync "$SRC_DIR" "$dst_dir"; then verified=0 fi echo "${elapsed_ms}|${mbs}|${verified}" # Cleanup rm -rf "$dst_dir" } # ── Run cp test (local baseline - no network) ────────────────────────────── run_cp() { local dst_dir="$DST_DIR_BASE/cp" rm -rf "$dst_dir" mkdir -p "$dst_dir" local start end start=$(date +%s%N) cp -r "$SRC_DIR"/* "$dst_dir/" 2>/dev/null end=$(date +%s%N) local elapsed_ms=$(( (end - start) / 1000000 )) local total_bytes total_bytes=$(find "$SRC_DIR" -type f -exec stat -c %s {} \; | awk '{sum+=$1} END {print sum}') local mbs=0 if [ "$elapsed_ms" -gt 0 ]; then mbs=$(awk "BEGIN {printf \"%.2f\", $total_bytes / 1048576.0 / ($elapsed_ms / 1000.0)}") fi local verified=1 if ! verify_sync "$SRC_DIR" "$dst_dir"; then verified=0 fi echo "${elapsed_ms}|${mbs}|${verified}" rm -rf "$dst_dir" } # ── Run rclone test over TCP (if available) ──────────────────────────────── run_rclone_tcp() { if [ "$HAS_RCLONE" -eq 0 ]; then echo "0|0|0" return fi local dst_dir="$DST_DIR_BASE/rclone" rm -rf "$dst_dir" mkdir -p "$dst_dir" # Start rclone serve in background rclone serve http --addr "$HOST:$RCLONE_PORT" --baseurl / "$SRC_DIR" > /dev/null 2>&1 & local rclone_pid=$! sleep 1 local start end start=$(date +%s%N) # Use HTTP which goes through TCP on loopback rclone copy "http://$HOST:$RCLONE_PORT" "$dst_dir" -q --transfers 4 --checkers 8 2>/dev/null || true end=$(date +%s%N) kill "$rclone_pid" 2>/dev/null || true wait "$rclone_pid" 2>/dev/null || true local elapsed_ms=$(( (end - start) / 1000000 )) local total_bytes total_bytes=$(find "$SRC_DIR" -type f -exec stat -c %s {} \; | awk '{sum+=$1} END {print sum}') local mbs=0 if [ "$elapsed_ms" -gt 0 ]; then mbs=$(awk "BEGIN {printf \"%.2f\", $total_bytes / 1048576.0 / ($elapsed_ms / 1000.0)}") fi local verified=1 if ! verify_sync "$SRC_DIR" "$dst_dir"; then verified=0 fi echo "${elapsed_ms}|${mbs}|${verified}" rm -rf "$dst_dir" } # ── Bar chart helper ──────────────────────────────────────────────────────── draw_bar() { local val=$1 local max=$2 local width=30 [ "$max" -eq 0 ] && max=1 local filled filled=$(LC_ALL=C awk "BEGIN{printf \"%d\", ($val * $width / $max)}") [ "$filled" -gt "$width" ] && filled=$width local bar="" for ((i=0; i/dev/null generate_test_data echo "" # Connection counts to test for fastSyncAI CONN_COUNTS=(1 2 4 8 16) # Initialize result arrays declare -a FS_RESULTS RS_RESULTS RSC_RESULTS CP_RESULTS RCLONE_RESULTS FS_LABELS=("fastSyncAI (1 conn)" "fastSyncAI (2 conn)" "fastSyncAI (4 conn)" "fastSyncAI (8 conn)" "fastSyncAI (16 conn)") # ── Phase 1: Network-based tools (all experience network simulation) ────────── info "Testing tools over simulated network ${SCENARIO_NAME}..." # Test fastSyncAI with different connection counts for nconn in "${CONN_COUNTS[@]}"; do echo -n " Testing fastSyncAI ${nconn} connections... " result=$(run_fastsync "$nconn") IFS='|' read -r ms mbs verified <<< "$result" FS_RESULTS+=("$ms|$mbs|$verified") if [ "$verified" -eq 1 ]; then ok "${mbs} MB/s (${ms}ms)" else warn "${mbs} MB/s (${ms}ms) - VERIFICATION FAILED" fi done # Test rsync over TCP (through rsyncd on loopback - experiences network simulation) echo -n " Testing rsync over TCP... " result=$(run_rsync_tcp 0) IFS='|' read -r ms mbs verified <<< "$result" RS_RESULTS=("$ms|$mbs|$verified") if [ "$verified" -eq 1 ]; then ok "${mbs} MB/s (${ms}ms)" else warn "${mbs} MB/s (${ms}ms) - VERIFICATION FAILED" fi # Test rsync with compression over TCP echo -n " Testing rsync + compress over TCP... " result=$(run_rsync_tcp 1) IFS='|' read -r ms mbs verified <<< "$result" RSC_RESULTS=("$ms|$mbs|$verified") if [ "$verified" -eq 1 ]; then ok "${mbs} MB/s (${ms}ms)" else warn "${mbs} MB/s (${ms}ms) - VERIFICATION FAILED" fi # Test rclone over TCP (if available) if [ "$HAS_RCLONE" -eq 1 ]; then echo -n " Testing rclone over TCP... " result=$(run_rclone_tcp) IFS='|' read -r ms mbs verified <<< "$result" RCLONE_RESULTS=("$ms|$mbs|$verified") if [ "$verified" -eq 1 ]; then ok "${mbs} MB/s (${ms}ms)" else warn "${mbs} MB/s (${ms}ms) - VERIFICATION FAILED" fi else warn "rclone not installed - skipping" fi # Cleanup network simulation (for local cp baseline) cleanup_network # ── Phase 2: Local baseline (cp - no network) ──────────────────────────────── echo -n " Testing cp (local baseline)... " result=$(run_cp) IFS='|' read -r ms mbs verified <<< "$result" CP_RESULTS=("$ms|$mbs|$verified") if [ "$verified" -eq 1 ]; then ok "${mbs} MB/s (${ms}ms)" else warn "${mbs} MB/s (${ms}ms) - VERIFICATION FAILED" fi echo "" echo -e "${BOLD}═══════════════════════════════════════════════════════════════════════════╗${RESET}" echo -e "${BOLD} NETWORK CONDITION RESULTS${RESET}" echo -e "${BOLD} Scenario: ${SCENARIO_NAME}${RESET}" echo -e "${BOLD} (All TCP tools tested with network simulation)${RESET}" echo -e "${BOLD}═══════════════════════════════════════════════════════════════════════════╝${RESET}" echo "" # ── Calculate max MB/s for bar scaling ──────────────────────────────────── calc_max_mbs() { local max=0 for res in "${FS_RESULTS[@]}" "${RS_RESULTS[@]}" "${RSC_RESULTS[@]}" "${CP_RESULTS[@]}" "${RCLONE_RESULTS[@]}"; do local mbs=$(echo "$res" | cut -d'|' -f2) local int_mbs=$(echo "$mbs" | awk '{printf "%d", $1+0}') [ "$int_mbs" -gt "$max" ] && max=$int_mbs done echo $max } MAX_MBS=$(calc_max_mbs) [ "$MAX_MBS" -eq 0 ] && MAX_MBS=100 # ── Print fastSyncAI results ──────────────────────────────────────────────── echo " === fastSyncAI (Multi-connection, over TCP with ${SCENARIO_NAME}) ===" printf " %-30s %-10s %-10s %s %s\n" "Configuration" "Time (ms)" "MB/s" "Throughput" "Verified" printf " %-30s %-10s %-10s %s %s\n" "------------------------------" "----------" "----------" "-----------" "--------" for i in "${!FS_RESULTS[@]}"; do IFS='|' read -r ms mbs verified <<< "${FS_RESULTS[$i]}" bar=$(draw_bar "$mbs" "$MAX_MBS") verified_str=$([ "$verified" -eq 1 ] && echo "✓" || echo "✗") printf " %-30s %-10s %-10s %s %s\n" "${FS_LABELS[$i]}" "$ms" "$mbs" "$bar" "$verified_str" done # ── Print other tool results (TCP-based, with network simulation) ──────────── echo "" echo " === Other Network Tools (over TCP with ${SCENARIO_NAME}) ===" printf " %-30s %-10s %-10s %s %s\n" "Tool" "Time (ms)" "MB/s" "Throughput" "Verified" printf " %-30s %-10s %-10s %s %s\n" "------------------------------" "----------" "----------" "-----------" "--------" # rsync IFS='|' read -r ms mbs verified <<< "${RS_RESULTS[0]}" bar=$(draw_bar "$mbs" "$MAX_MBS") verified_str=$([ "$verified" -eq 1 ] && echo "✓" || echo "✗") printf " %-30s %-10s %-10s %s %s\n" "rsync (TCP)" "$ms" "$mbs" "$bar" "$verified_str" # rsync + compress IFS='|' read -r ms mbs verified <<< "${RSC_RESULTS[0]}" bar=$(draw_bar "$mbs" "$MAX_MBS") verified_str=$([ "$verified" -eq 1 ] && echo "✓" || echo "✗") printf " %-30s %-10s %-10s %s %s\n" "rsync + compress (TCP)" "$ms" "$mbs" "$bar" "$verified_str" # rclone if [ "${RCLONE_RESULTS[0]}" != "0|0|0" ]; then IFS='|' read -r ms mbs verified <<< "${RCLONE_RESULTS[0]}" bar=$(draw_bar "$mbs" "$MAX_MBS") verified_str=$([ "$verified" -eq 1 ] && echo "✓" || echo "✗") printf " %-30s %-10s %-10s %s %s\n" "rclone (TCP)" "$ms" "$mbs" "$bar" "$verified_str" fi # cp (local baseline - separate section) echo "" echo " === Local Baseline (no network) ===" IFS='|' read -r ms mbs verified <<< "${CP_RESULTS[0]}" bar=$(draw_bar "$mbs" "$MAX_MBS") verified_str=$([ "$verified" -eq 1 ] && echo "✓" || echo "✗") printf " %-30s %-10s %-10s %s %s\n" "cp (local baseline)" "$ms" "$mbs" "$bar" "$verified_str" # ── Find best configuration and speedup analysis ──────────────────────────── echo "" echo -e "${BOLD}═══════════════════════════════════════════════════════════════════════════╗${RESET}" echo -e "${BOLD} ANALYSIS${RESET}" echo -e "${BOLD}═══════════════════════════════════════════════════════════════════════════╝${RESET}" echo "" # Find best fastSyncAI config best_fs_idx=0 best_fs_mbs=0 for i in "${!FS_RESULTS[@]}"; do IFS='|' read -r ms mbs verified <<< "${FS_RESULTS[$i]}" mbs_int=$(echo "$mbs" | awk '{printf "%d", $1+0}') if [ "$mbs_int" -gt "$best_fs_mbs" ]; then best_fs_mbs=$mbs_int best_fs_idx=$i fi done # Get rsync MB/s for comparison IFS='|' read -r rs_ms rs_mbs rs_verified <<< "${RS_RESULTS[0]}" ok "Best fastSyncAI config: ${FS_LABELS[$best_fs_idx]} (${best_fs_mbs} MB/s)" # Speedup vs rsync (both over same network conditions) echo "" echo "Speedup vs rsync (both over ${SCENARIO_NAME}):" if [ "$(awk "BEGIN {print ($rs_mbs > 0) ? 1 : 0}")" = "1" ]; then best_fs_result="${FS_RESULTS[$best_fs_idx]}" IFS='|' read -r fs_ms fs_mbs fs_verified <<< "$best_fs_result" speedup=$(awk "BEGIN {printf \"%.2f\", $fs_mbs / ($rs_mbs == 0 ? 0.01 : $rs_mbs)}") if [ "$(awk "BEGIN {print ($fs_mbs > $rs_mbs) ? 1 : 0}")" = "1" ]; then echo -e " ${GREEN}fastSyncAI (best) is ${speedup}x faster than rsync${RESET}" elif [ "$(awk "BEGIN {print ($rs_mbs > $fs_mbs) ? 1 : 0}")" = "1" ]; then speedup_inv=$(awk "BEGIN {printf \"%.2f\", $rs_mbs / ($fs_mbs == 0 ? 0.01 : $fs_mbs)}") echo -e " ${GREEN}rsync is ${speedup_inv}x faster than fastSyncAI (best)${RESET}" else echo -e " Both performed similarly" fi fi # Compare to local cp to show network overhead echo "" echo "Network overhead (vs local cp):" IFS='|' read -r cp_ms cp_mbs cp_verified <<< "${CP_RESULTS[0]}" if [ "$(awk "BEGIN {print ($cp_mbs > 0) ? 1 : 0}")" = "1" ]; then overhead_pct=$(awk "BEGIN {printf \"%.1f\", (1 - $best_fs_mbs / $cp_mbs) * 100}") ratio=$(awk "BEGIN {printf \"%.2f\", $cp_mbs / ($best_fs_mbs == 0 ? 0.01 : $best_fs_mbs)}") echo -e " cp is ${ratio}x faster than best fastSyncAI (${overhead_pct}% overhead from network simulation)" fi echo "" ok "Network benchmark complete!"