#!/usr/bin/env bash # ============================================================================= # benchmark_network.sh — Network Condition Benchmark for fastSyncAI # # Compares fastSyncAI and rsync under IDENTICAL network conditions # by running both over TCP on the loopback interface with tc netem simulation. # # 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 # # Examples: # ./benchmark_network.sh --lan # ./benchmark_network.sh --wan 200 1 # ./benchmark_network.sh --wan-loss-1 50 # ============================================================================= set -euo pipefail # ── Defaults ────────────────────────────────────────────────────────────── DATA_SIZE_MB=${2:-50} RUN_COUNT=${3:-1} SCENARIO="${1:---lan}" # ── Parse scenario ──────────────────────────────────────────────────────── LATENCY_MS=0 PACKET_LOSS="0%" JITTER_MS="0" case "$SCENARIO" in --compress-test) SCENARIO_NAME="Compression Streaming Test" RUN_COMPRESS_TEST=1 ;; --lan) LATENCY_MS=10; PACKET_LOSS="0.1%"; JITTER_MS=2; SCENARIO_NAME="LAN (10ms RTT, 0.1% loss, ±2ms jitter)" ;; --wan) LATENCY_MS=100; PACKET_LOSS="1%"; JITTER_MS=10; SCENARIO_NAME="WAN (100ms RTT, 1% loss, ±10ms jitter)" ;; --wan-loss-1) LATENCY_MS=100; PACKET_LOSS="1%"; SCENARIO_NAME="WAN + 1% loss" ;; --wan-loss-5) LATENCY_MS=100; PACKET_LOSS="5%"; SCENARIO_NAME="WAN + 5% loss" ;; --wan-jitter) LATENCY_MS=100; JITTER_MS=50; SCENARIO_NAME="WAN + jitter" ;; --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" exit 1 fi ;; *) echo "ERROR: Unknown scenario '$SCENARIO'" echo "Available: --lan, --wan, --wan-loss-1, --wan-loss-5, --wan-jitter, --custom" exit 1 ;; esac # ── Ports ──────────────────────────────────────────────────────────────── FASTSYNC_PORT=19410 RSYNC_PORT=19400 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_SHARE="$TEST_DIR/rsync_share" # ── 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 ──────────────────────────────────────────────────── setup_network() { info "Setting up network: ${SCENARIO_NAME}..." if ! sudo -n true 2>/dev/null; then echo -e "${YELLOW}⚠ sudo required for tc netem${RESET}" fi sudo tc qdisc del dev lo root 2>/dev/null || true local netem_cmd="delay ${LATENCY_MS}ms" [ "$JITTER_MS" -gt 0 ] && netem_cmd="$netem_cmd ${JITTER_MS}ms" [ "$PACKET_LOSS" != "0%" ] && netem_cmd="$netem_cmd loss ${PACKET_LOSS}" sudo tc qdisc add dev lo root netem $netem_cmd || die "tc netem failed" ok "Network active: ${LATENCY_MS}ms RTT, loss=${PACKET_LOSS}, jitter=±${JITTER_MS}ms" } cleanup_network() { sudo tc qdisc del dev lo root 2>/dev/null || true } # ── rsync daemon ──────────────────────────────────────────────────────────── RSYNCD_PID="" start_rsyncd() { # Get absolute path for the share local abs_share abs_share=$(cd "$RSYNC_SHARE" && pwd) # Write config file with absolute path to share cat > "$TEST_DIR/rsyncd.conf" </dev/null cd - >/dev/null sleep 2 # Verify port is listening (rsyncd forks so PID check doesn't work) if ! ss -tlnp 2>/dev/null | grep -q ":$RSYNC_PORT "; then # Try to get PID RSYNCD_PID=$(ss -tlnp 2>/dev/null | grep ":$RSYNC_PORT " | head -1 | grep -oP 'pid=\K[0-9]+' || echo "") die "rsyncd not listening on port $RSYNC_PORT (PID=$RSYNCD_PID)" fi # Get actual PID (use head -1 to handle IPv4/IPv6 duplicate) RSYNCD_PID=$(ss -tlnp 2>/dev/null | grep ":$RSYNC_PORT " | head -1 | grep -oP 'pid=\K[0-9]+' || echo "") ok "rsyncd started on port $RSYNC_PORT (PID=$RSYNCD_PID)" } stop_rsyncd() { [ -n "$RSYNCD_PID" ] && kill "$RSYNCD_PID" 2>/dev/null || true wait "$RSYNCD_PID" 2>/dev/null || true RSYNCD_PID="" } # ── 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." cleanup() { stop_rsyncd cleanup_network pkill -f "fastsync_server" 2>/dev/null || true pkill -f "fastsync_client" 2>/dev/null || true rm -rf "$TEST_DIR" } trap cleanup EXIT # Clean stale processes pkill -f "rsync --daemon" 2>/dev/null || true pkill -f "fastsync_server" 2>/dev/null || true pkill -f "fastsync_client" 2>/dev/null || true cleanup_network 2>/dev/null || true sleep 1 # ── Generate test data ────────────────────────────────────────────────────── generate_data() { info "Generating ~${DATA_SIZE_MB}MB test data..." rm -rf "$TEST_DIR" mkdir -p "$SRC_DIR/small" "$SRC_DIR/medium" "$SRC_DIR/large" "$RSYNC_SHARE" # Small files (~25% of total) local small_count=$(( (DATA_SIZE_MB * 262144) / 1048576 )) [ "$small_count" -lt 10 ] && small_count=10 [ "$small_count" -gt 100 ] && small_count=100 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 (~50% of total) local medium_count=$(( (DATA_SIZE_MB * 524288) / 1048576 )) [ "$medium_count" -lt 5 ] && medium_count=5 [ "$medium_count" -gt 20 ] && medium_count=20 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 (~25%) local large_mb=$(( DATA_SIZE_MB - (small_count * 256 / 1024) - medium_count )) [ "$large_mb" -lt 5 ] && large_mb=5 local large_count=3 [ "$large_mb" -lt 10 ] && large_count=1 for i in $(seq 1 $large_count); do local sz=$(( large_mb * 1048576 / large_count / 1048576 )) [ "$sz" -lt 1 ] && sz=1 dd if=/dev/urandom bs=1M count=$sz of="$SRC_DIR/large/file_$i.bin" 2>/dev/null done # Copy to rsync share cp -r "$SRC_DIR"/* "$RSYNC_SHARE/" local total_files=$(find "$SRC_DIR" -type f | wc -l) local total_size=$(du -sh "$SRC_DIR" | cut -f1) ok "Generated $total_files files, $total_size" } # ── Verify ────────────────────────────────────────────────────────────────── verify() { local src=$1 dst=$2 local sc=$(find "$src" -type f | wc -l) local dc=$(find "$dst" -type f | wc -l) [ "$sc" -ne "$dc" ] && return 1 local ss=$(find "$src" -type f -printf '%s\n' | awk '{sum+=$1} END {print sum}') local ds=$(find "$dst" -type f -printf '%s\n' | awk '{sum+=$1} END {print sum}') [ "$ss" -ne "$ds" ] && return 1 return 0 } # ── Run fastSyncAI ──────────────────────────────────────────────────────── run_fastsync() { local nconn=$1 local use_compress=$2 local dst="$DST_DIR_BASE/fastsync_${nconn}_${use_compress}" local port=$((FASTSYNC_PORT + nconn + (use_compress * 100))) rm -rf "$dst" mkdir -p "$dst" "$SERVER_BIN" -p "$port" -d "$dst" >/dev/null 2>&1 & local spid=$! sleep 1 local output local compress_flag="" [ "$use_compress" -eq 0 ] && compress_flag="-c" output=$(timeout 60 "$CLIENT_BIN" -h "$HOST" -p "$port" -s "$SRC_DIR" -n "$nconn" $compress_flag 2>&1) || true # Extract actual transfer time from client output local transfer_ms=$(echo "$output" | grep -oP 'ms=\K[0-9.]+' || echo "0") local transfer_ms_int=$(echo "$transfer_ms" | awk '{printf "%d", $1+0}') # Use fixed short wait time since timeout already limits client sleep 2 kill "$spid" 2>/dev/null || true wait "$spid" 2>/dev/null || true sync sleep 1 local ms=$(echo "$output" | grep -oP 'ms=\K[0-9.]+' || echo "0") local mbs=$(echo "$output" | grep -oP 'throughput_mbs=\K[0-9.]+' || echo "0") local ok=1 verify "$SRC_DIR" "$dst" || ok=0 echo "${ms}|${mbs}|${ok}" rm -rf "$dst" } # ── Run rsync over TCP ─────────────────────────────────────────────────── run_rsync_tcp() { local use_compress=${1:-0} local dst="$DST_DIR_BASE/rsync_${use_compress}" rm -rf "$dst" mkdir -p "$dst" local start=$(date +%s%N) local compress_flag="" [ "$use_compress" -eq 1 ] && compress_flag="--compress" rsync -a $compress_flag "rsync://$HOST:$RSYNC_PORT/network_bench/" "$dst/" >/dev/null 2>&1 local end=$(date +%s%N) local ms=$(( (end - start) / 1000000 )) local total_bytes=$(find "$SRC_DIR" -type f -printf '%s\n' | awk '{sum+=$1} END {print sum}') local mbs=0 [ "$ms" -gt 0 ] && mbs=$(awk "BEGIN {printf \"%.2f\", $total_bytes / 1048576.0 / ($ms / 1000.0)}") local ok=1 verify "$SRC_DIR" "$dst" || ok=0 echo "${ms}|${mbs}|${ok}" rm -rf "$dst" } # ── Bar chart ───────────────────────────────────────────────────────────── draw_bar() { local val=$1 max=$2 width=30 [ "$max" -eq 0 ] && max=1 # Convert to integers for arithmetic local val_int=$(echo "$val" | awk '{printf "%d", $1+0}') local max_int=$(echo "$max" | awk '{printf "%d", $1+0}') [ "$max_int" -eq 0 ] && max_int=1 local filled=$(( val_int * width / max_int )) [ "$filled" -gt "$width" ] && filled=$width printf -v bar '' for ((i=0; i/dev/null && max=$int done echo $max } MAX_MBS=$(calc_max) [ "$MAX_MBS" -eq 0 ] && MAX_MBS=100 printf " %-22s %-10s %-10s %s\n" "Tool" "Time (ms)" "MB/s" "Throughput" printf " %-22s %-10s %-10s %s\n" "----------------------" "----------" "----------" "------------" for i in "${!FS_COMP_RES[@]}"; do IFS='|' read -r ms mbs ok <<< "${FS_COMP_RES[$i]}" bar=$(draw_bar "$mbs" "$MAX_MBS") v=$([ "$ok" -eq 1 ] && echo "✓" || echo "✗") printf " %-22s %-10s %-10s %s %s\n" "${FS_LABELS[$i]} (LZ4)" "$ms" "$mbs" "$bar" "$v" done for i in "${!FS_RES[@]}"; do IFS='|' read -r ms mbs ok <<< "${FS_RES[$i]}" bar=$(draw_bar "$mbs" "$MAX_MBS") v=$([ "$ok" -eq 1 ] && echo "✓" || echo "✗") printf " %-22s %-10s %-10s %s %s\n" "${FS_LABELS[$i]}" "$ms" "$mbs" "$bar" "$v" done IFS='|' read -r ms mbs ok <<< "${RSYNC_RES[0]}" bar=$(draw_bar "$mbs" "$MAX_MBS") v=$([ "$ok" -eq 1 ] && echo "✓" || echo "✗") printf " %-22s %-10s %-10s %s %s\n" "rsync (TCP)" "$ms" "$mbs" "$bar" "$v" IFS='|' read -r ms mbs ok <<< "${RSYNCC_RES[0]}" bar=$(draw_bar "$mbs" "$MAX_MBS") v=$([ "$ok" -eq 1 ] && echo "✓" || echo "✗") printf " %-22s %-10s %-10s %s %s\n" "rsync+compress (TCP)" "$ms" "$mbs" "$bar" "$v" # Analysis echo "" echo -e "${BOLD}═══════════════════════════════════════════════════════════════════════════╗${RESET}" echo -e "${BOLD} ANALYSIS${RESET}" echo -e "${BOLD}═══════════════════════════════════════════════════════════════════════════╝${RESET}" echo "" # Find best (use compressed results as primary) best_idx=0 best_mbs=0 for i in "${!FS_COMP_RES[@]}"; do this_mbs=$(echo "${FS_COMP_RES[$i]}" | cut -d'|' -f2 | awk '{printf "%d", $1+0}') [ "$this_mbs" -gt "$best_mbs" ] && best_mbs=$this_mbs && best_idx=$i done best_label="${FS_LABELS[$best_idx]} (LZ4)" # Also find best uncompressed for comparison best_no_compress_idx=0 best_no_compress_mbs=0 for i in "${!FS_RES[@]}"; do this_mbs=$(echo "${FS_RES[$i]}" | cut -d'|' -f2 | awk '{printf "%d", $1+0}') [ "$this_mbs" -gt "$best_no_compress_mbs" ] && best_no_compress_mbs=$this_mbs && best_no_compress_idx=$i done best_no_compress_label="${FS_LABELS[$best_no_compress_idx]}" IFS='|' read -r rs_ms rs_mbs rs_ok <<< "${RSYNC_RES[0]}" rs_mbs_int=$(echo "$rs_mbs" | awk '{printf "%d", $1+0}') ok "Best fastSyncAI (LZ4): $best_label (${best_mbs} MB/s)" ok "Best fastSyncAI (no compress): $best_no_compress_label (${best_no_compress_mbs} MB/s)" # Compression benefit if [ "$best_mbs" -gt 0 ] && [ "$best_no_compress_mbs" -gt 0 ]; then if [ "$best_mbs" -gt "$best_no_compress_mbs" ]; then compress_speedup=$(awk "BEGIN {printf \"%.2f\", $best_mbs / ($best_no_compress_mbs == 0 ? 0.01 : $best_no_compress_mbs)}") echo -e " ${GREEN}Compression provides ${compress_speedup}x speedup${RESET}" else echo -e " ${YELLOW}Compression: no benefit (data likely incompressible)${RESET}" fi fi if [ "$rs_mbs_int" -gt 0 ]; then echo "" echo "Speedup vs rsync (both over ${SCENARIO_NAME}):" speedup=$(awk "BEGIN {printf \"%.2f\", $best_mbs / ($rs_mbs_int == 0 ? 0.01 : $rs_mbs_int)}") if [ "$best_mbs" -gt "$rs_mbs_int" ]; then echo -e " ${GREEN}fastSyncAI (LZ4) is ${speedup}x FASTER than rsync${RESET}" elif [ "$rs_mbs_int" -gt "$best_mbs" ]; then speedup=$(awk "BEGIN {printf \"%.2f\", $rs_mbs_int / ($best_mbs == 0 ? 0.01 : $best_mbs)}") echo -e " ${GREEN}rsync is ${speedup}x faster than fastSyncAI (LZ4)${RESET}" else echo -e " Both performed similarly" fi fi echo "" ok "Benchmark complete!" # ============================================================================= # Compression Streaming Test - Tests the new compression optimizations # ============================================================================= # Function to run compression streaming test run_compression_stream_test() { info "Running compression streaming test..." # Create test directory with specific file types local test_dir="$TEST_DIR/compress_test" rm -rf "$test_dir" mkdir -p "$test_dir/src/compressed" "$test_dir/src/text" "$test_dir/src/small" # Generate test files # 1. Already compressed files (should skip compression via type detection) dd if=/dev/urandom bs=1M count=5 of="$test_dir/src/compressed/already_compressed.zip" 2>/dev/null cp "$test_dir/src/compressed/already_compressed.zip" "$test_dir/src/compressed/image.jpg" cp "$test_dir/src/compressed/already_compressed.zip" "$test_dir/src/compressed/video.mp4" # 2. Highly compressible text files for i in $(seq 1 5); do # Generate text-like data (repeated patterns compress well) python3 -c "import os; os.write($i, (b'This is a test line.\n' * 100000))" \ > "$test_dir/src/text/large_text_$i.txt" 2>/dev/null || \ dd if=/dev/zero bs=1M count=1 | tr '\0' 'A' > "$test_dir/src/text/large_text_$i.txt" 2>/dev/null done # 3. Small files (should skip compression via size threshold) for i in $(seq 1 10); do dd if=/dev/urandom bs=512 count=1 of="$test_dir/src/small/tiny_$i.bin" 2>/dev/null done local total_size=$(du -sh "$test_dir/src" | cut -f1) ok "Created compression test data: $total_size" # Run fastSyncAI with compression enabled local dst="$test_dir/dst_compress" local port=$((FASTSYNC_PORT + 999)) "$SERVER_BIN" -p "$port" -d "$dst" >/dev/null 2>&1 & local spid=$! sleep 1 info "Testing with compression enabled..." local start=$(date +%s%N) output=$(timeout 60 "$CLIENT_BIN" -h "$HOST" -p "$port" -s "$test_dir/src" -n 4 2>&1) || true local end=$(date +%s%N) kill "$spid" 2>/dev/null || true wait "$spid" 2>/dev/null || true local elapsed_ms=$(( (end - start) / 1000000 )) local transfer_mb=$(echo "$total_size" | sed 's/M//') local throughput=0 if [ "$elapsed_ms" -gt 0 ]; then throughput=$(awk "BEGIN {printf \"%.2f\", $transfer_mb / ($elapsed_ms / 1000)}") fi # Check if compression was skipped for already-compressed files local skipped_compression=0 if echo "$output" | grep -q "Sent (pipelined+sendfile)"; then skipped_compression=1 fi ok "Compression streaming test: ${throughput} MB/s (${elapsed_ms}ms)" if [ "$skipped_compression" -eq 1 ]; then ok "✓ Compression was intelligently skipped for some files" fi # Cleanup rm -rf "$test_dir" } # Run compression streaming test if requested via RUN_COMPRESS_TEST flag if [ -n "${RUN_COMPRESS_TEST:-}" ]; then info "Starting Compression Streaming Test..." cleanup_network generate_data # Reuse existing data setup_network run_compression_stream_test cleanup exit 0 fi