Files
Oxicloud/bench.sh
T

420 lines
18 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# ============================================================================
# OxiCloud Performance Benchmark v3
# Detailed RAM (RSS/VmPeak/VmSwap) + CPU monitoring + huge file downloads
#
# - Uploads: 1KB..500MB via API (above ~1GB the upload handler OOMs —
# that's a known issue in the multipart handler which buffers chunks).
# - Downloads: 1KB..10GB. For files >500MB we inject blobs directly into
# the blob store + DB so the streaming download path is tested without
# going through the buffering upload handler.
# ============================================================================
set -uo pipefail
BASE_URL="${BASE_URL:-http://127.0.0.1:8086}"
USERNAME="${BENCH_USER:-benchuser}"
PASSWORD="${BENCH_PASS:-BenchPass123!}"
FOLDER_ID="${BENCH_FOLDER:-0e72efc0-0d1c-45a1-b434-52336643b3f7}"
USER_ID="${BENCH_USER_ID:-c410b103-7b86-4ac2-9eb4-3804351547be}"
WORKDIR="/tmp/oxibench"
RESULTS="$WORKDIR/results_v3.csv"
STORAGE="./storage"
MONITOR_INTERVAL=0.2 # 200ms sample rate
# ── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GRN='\033[0;32m'; YEL='\033[1;33m'
CYA='\033[0;36m'; BLD='\033[1m'; DIM='\033[2m'; NC='\033[0m'
header() { printf "\n${BLD}${CYA}═══════════════════════════════════════════${NC}\n"; \
printf "${BLD}${CYA} %s${NC}\n" "$1"; \
printf "${BLD}${CYA}═══════════════════════════════════════════${NC}\n"; }
ok() { printf "${GRN}✓${NC} %s\n" "$1"; }
warn() { printf "${YEL}⚠${NC} %s\n" "$1"; }
fail() { printf "${RED}✗${NC} %s\n" "$1"; }
# ── Server PID ───────────────────────────────────────────────────────────────
SERVER_PID=""
find_server_pid() {
SERVER_PID=$(pgrep -f "target/release/oxicloud" 2>/dev/null | head -1 || \
pgrep -f "target/debug/oxicloud" 2>/dev/null | head -1 || echo "")
[[ -z "$SERVER_PID" ]] && { fail "Server PID not found"; exit 1; }
}
# ── Memory (KB) ─────────────────────────────────────────────────────────────
get_rss_kb() { awk '/^VmRSS:/ {print $2}' /proc/$SERVER_PID/status 2>/dev/null || echo 0; }
get_peak_kb() { awk '/^VmPeak:/ {print $2}' /proc/$SERVER_PID/status 2>/dev/null || echo 0; }
get_swap_kb() { awk '/^VmSwap:/ {print $2}' /proc/$SERVER_PID/status 2>/dev/null || echo 0; }
get_vsize_kb() { awk '/^VmSize:/ {print $2}' /proc/$SERVER_PID/status 2>/dev/null || echo 0; }
# ── CPU jiffies ──────────────────────────────────────────────────────────────
get_cpu_jiffies() { awk '{print $14+$15}' /proc/$SERVER_PID/stat 2>/dev/null || echo 0; }
# ── Background monitor ──────────────────────────────────────────────────────
MONITOR_PID=""
MONITOR_RESULT="$WORKDIR/_mon"
MON_PEAK_RSS=0; MON_PEAK_VM=0; MON_PEAK_SWAP=0; MON_CPU_PCT="0.0"
start_monitor() {
rm -f "$MONITOR_RESULT"
(
peak_rss=0; peak_vm=0; peak_swap=0
cpu0=$(get_cpu_jiffies); t0=$(date +%s%N)
while true; do
r=$(get_rss_kb); v=$(get_vsize_kb); s=$(get_swap_kb)
(( r > peak_rss )) && peak_rss=$r
(( v > peak_vm )) && peak_vm=$v
(( s > peak_swap )) && peak_swap=$s
sleep $MONITOR_INTERVAL 2>/dev/null || break
done
cpu1=$(get_cpu_jiffies); t1=$(date +%s%N)
dt=$(( cpu1 - cpu0 )); wall=$(( (t1 - t0) / 1000000 ))
cpup=0
(( wall > 0 )) && cpup=$(echo "scale=1; $dt * 10 * 100 / $wall" | bc 2>/dev/null || echo 0)
echo "$peak_rss $peak_vm $peak_swap $cpup" > "$MONITOR_RESULT"
) &
MONITOR_PID=$!
}
stop_monitor() {
[[ -n "$MONITOR_PID" ]] && { kill $MONITOR_PID 2>/dev/null; wait $MONITOR_PID 2>/dev/null || true; MONITOR_PID=""; }
if [[ -f "$MONITOR_RESULT" ]]; then
read -r MON_PEAK_RSS MON_PEAK_VM MON_PEAK_SWAP MON_CPU_PCT < "$MONITOR_RESULT"
else
MON_PEAK_RSS=0; MON_PEAK_VM=0; MON_PEAK_SWAP=0; MON_CPU_PCT="0.0"
fi
}
# ── Auth ─────────────────────────────────────────────────────────────────────
TOKEN=""
login() {
TOKEN=$(curl -sf -X POST "$BASE_URL/api/auth/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD\"}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])" 2>/dev/null || echo "")
[[ -z "$TOKEN" ]] && { fail "JWT login failed"; exit 1; }
}
# ── File gen ─────────────────────────────────────────────────────────────────
gen_file() {
local path="$1" bytes="$2"
local bs=1048576
local count=$(( bytes / bs )) rem=$(( bytes % bs ))
(( count > 0 )) && dd if=/dev/urandom of="$path" bs=$bs count=$count 2>/dev/null
(( rem > 0 )) && dd if=/dev/urandom bs=$rem count=1 >> "$path" 2>/dev/null
}
human() {
local b=$1
if (( b >= 1073741824 )); then printf "%.2f GB" "$(echo "$b/1073741824" | bc -l)"
elif (( b >= 1048576 )); then printf "%.1f MB" "$(echo "$b/1048576" | bc -l)"
elif (( b >= 1024 )); then printf "%.1f KB" "$(echo "$b/1024" | bc -l)"
else printf "%d B" "$b"; fi
}
throughput() {
local b=$1 ms=$2
(( ms <= 0 )) && { echo "---"; return; }
human $(echo "$b*1000/$ms" | bc 2>/dev/null || echo 0)
}
mb() { echo $(( $1 / 1024 )); } # KB -> MB
# ── Inject large blob (bypass upload handler) ──────────────────────────────
# Creates a random file as a blob, registers it in DB, returns file_id.
# Usage: inject_blob <label> <size_bytes>
inject_blob() {
local label="$1" size="$2"
local hash fpath blob_dir blob_path file_id
hash=$(printf "%s_%s" "$label" "$(date +%s%N)" | sha256sum | awk '{print $1}')
blob_dir="$STORAGE/.blobs/${hash:0:2}"
blob_path="$blob_dir/${hash}.blob"
mkdir -p "$blob_dir"
printf " Generating blob %s (%s) ..." "$label" "$(human $size)" >&2
gen_file "$blob_path" "$size"
printf " done\n" >&2
# Register blob in dedup index
docker exec -i $(docker ps -qf "name=postgres") psql -U postgres -d oxicloud -q <<SQL >/dev/null 2>&1
INSERT INTO storage.blobs (hash, size, content_type, ref_count)
VALUES ('$hash', $size, 'application/octet-stream', 1)
ON CONFLICT DO NOTHING;
SQL
# Register file metadata
file_id=$(docker exec -i $(docker ps -qf "name=postgres") psql -U postgres -d oxicloud -t -A <<SQL 2>/dev/null
INSERT INTO storage.files (name, folder_id, user_id, blob_hash, size, mime_type)
VALUES ('bench_${label}.bin', '$FOLDER_ID'::uuid, '$USER_ID', '$hash', $size, 'application/octet-stream')
RETURNING id::text;
SQL
)
file_id=$(echo "$file_id" | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1)
echo "$file_id"
}
# ── Setup ────────────────────────────────────────────────────────────────────
mkdir -p "$WORKDIR"
rm -f "$RESULTS"
echo "test,size_bytes,direction,latency_ms,rss_before_mb,rss_peak_mb,rss_after_mb,swap_peak_mb,cpu_pct,throughput_MBs,http_code" > "$RESULTS"
find_server_pid
header "OxiCloud Benchmark v3 — RAM + CPU + Huge Files"
printf " %-14s %s\n" "Server" "$BASE_URL"
printf " %-14s %s\n" "PID" "$SERVER_PID"
printf " %-14s %d MB\n" "RSS now" $(mb $(get_rss_kb))
printf " %-14s %d MB\n" "VmPeak" $(mb $(get_peak_kb))
printf " %-14s %d MB\n" "Swap" $(mb $(get_swap_kb))
printf " %-14s %s\n" "Cores" "$(nproc)"
printf " %-14s %s\n" "Disk free" "$(df -h $WORKDIR | tail -1 | awk '{print $4}')"
login; ok "Authenticated"
echo
# ── Upload sizes (safe for the buffering handler) ───────────────────────────
UPLOAD_SIZES=(
"1KB:1024"
"64KB:65536"
"256KB:262144"
"1MB:1048576"
"10MB:10485760"
"50MB:52428800"
"100MB:104857600"
"250MB:262144000"
"500MB:524288000"
)
# Additional sizes for download-only (injected directly as blobs)
DOWNLOAD_ONLY_SIZES=(
"1GB:1073741824"
"2GB:2147483648"
"5GB:5368709120"
"10GB:10737418240"
)
# ── Generate upload test files ──────────────────────────────────────────────
header "Generating Upload Test Files"
for e in "${UPLOAD_SIZES[@]}"; do
l="${e%%:*}"; b="${e##*:}"
f="$WORKDIR/test_${l}.bin"
if [[ -f "$f" ]] && [[ $(stat -c%s "$f" 2>/dev/null || echo 0) -eq "$b" ]]; then
printf " %s exists\n" "$l"
else
printf " %s (%s) ..." "$l" "$(human $b)"
gen_file "$f" "$b"
printf " done\n"
fi
done
# ============================================================================
# UPLOADS (via API multipart)
# ============================================================================
header "Upload Benchmarks (via API)"
printf "${BLD}%-8s │ %8s %10s │ %7s %7s %7s %6s │ %5s │ %4s${NC}\n" \
"Size" "Latency" "Thruput" "RSSbef" "RSSpk" "RSSaft" "Swap" "CPU%" "HTTP"
printf "%-8s │ %8s %10s │ %7s %7s %7s %6s │ %5s │ %4s\n" \
"------" "-------" "--------" "------" "-----" "------" "----" "----" "----"
declare -A FILE_IDS
for e in "${UPLOAD_SIZES[@]}"; do
l="${e%%:*}"; b="${e##*:}"; f="$WORKDIR/test_${l}.bin"
login >/dev/null 2>&1
rss0=$(get_rss_kb)
start_monitor
t0=$(date +%s%N)
resp=$(curl -s -w '\n%{http_code}' --max-time 3600 \
-X POST "$BASE_URL/api/files/upload" \
-H "Authorization: Bearer $TOKEN" \
-F "folder_id=$FOLDER_ID" \
-F "file=@$f" 2>&1)
t1=$(date +%s%N)
stop_monitor
code=$(echo "$resp" | tail -1)
body=$(echo "$resp" | head -n -1)
ms=$(( (t1 - t0) / 1000000 ))
rss1=$(get_rss_kb)
tp=$(throughput "$b" "$ms")
fid=$(echo "$body" | python3 -c "import sys,json;print(json.load(sys.stdin)['id'])" 2>/dev/null || echo "")
FILE_IDS[$l]="$fid"
printf "%-8s │ %6dms %8s/s │ %5dMB %5dMB %5dMB %4dMB │ %5s │ %4s\n" \
"$l" "$ms" "$tp" \
$(mb $rss0) $(mb $MON_PEAK_RSS) $(mb $rss1) $(mb $MON_PEAK_SWAP) \
"$MON_CPU_PCT" "$code"
tpm=$(echo "scale=2;$b*1000/($ms+1)/1048576" | bc 2>/dev/null || echo 0)
echo "upload_$l,$b,upload,$ms,$(mb $rss0),$(mb $MON_PEAK_RSS),$(mb $rss1),$(mb $MON_PEAK_SWAP),$MON_CPU_PCT,$tpm,$code" >> "$RESULTS"
sleep 1
done
# ============================================================================
# INJECT LARGE BLOBS (for download-only tests)
# ============================================================================
header "Injecting Large Blobs (bypass upload handler)"
for e in "${DOWNLOAD_ONLY_SIZES[@]}"; do
l="${e%%:*}"; b="${e##*:}"
fid=$(inject_blob "$l" "$b")
FILE_IDS[$l]="$fid"
ok "Registered $l → $fid"
done
# ============================================================================
# DOWNLOADS (all sizes including injected blobs)
# ============================================================================
ALL_SIZES=("${UPLOAD_SIZES[@]}" "${DOWNLOAD_ONLY_SIZES[@]}")
header "Download Benchmarks"
printf "${BLD}%-8s │ %8s %10s │ %7s %7s %7s %6s │ %5s │ %4s${NC}\n" \
"Size" "Latency" "Thruput" "RSSbef" "RSSpk" "RSSaft" "Swap" "CPU%" "HTTP"
printf "%-8s │ %8s %10s │ %7s %7s %7s %6s │ %5s │ %4s\n" \
"------" "-------" "--------" "------" "-----" "------" "----" "----" "----"
for e in "${ALL_SIZES[@]}"; do
l="${e%%:*}"; b="${e##*:}"
fid="${FILE_IDS[$l]:-}"
[[ -z "$fid" ]] && { warn "$l skipped (no ID)"; continue; }
login >/dev/null 2>&1
rss0=$(get_rss_kb)
start_monitor
t0=$(date +%s%N)
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 7200 \
"$BASE_URL/api/files/$fid" -H "Authorization: Bearer $TOKEN" 2>&1)
t1=$(date +%s%N)
stop_monitor
ms=$(( (t1 - t0) / 1000000 ))
rss1=$(get_rss_kb)
tp=$(throughput "$b" "$ms")
printf "%-8s │ %6dms %8s/s │ %5dMB %5dMB %5dMB %4dMB │ %5s │ %4s\n" \
"$l" "$ms" "$tp" \
$(mb $rss0) $(mb $MON_PEAK_RSS) $(mb $rss1) $(mb $MON_PEAK_SWAP) \
"$MON_CPU_PCT" "$code"
tpm=$(echo "scale=2;$b*1000/($ms+1)/1048576" | bc 2>/dev/null || echo 0)
echo "download_$l,$b,download,$ms,$(mb $rss0),$(mb $MON_PEAK_RSS),$(mb $rss1),$(mb $MON_PEAK_SWAP),$MON_CPU_PCT,$tpm,$code" >> "$RESULTS"
sleep 1
done
# ============================================================================
# RANGE REQUESTS (first 64 KB — constant memory regardless of file size)
# ============================================================================
header "Range Requests (64 KB slice from each file)"
printf "${BLD}%-8s │ %8s │ %7s %7s │ %4s${NC}\n" "Size" "Latency" "RSSpk" "RSSdelta" "HTTP"
printf "%-8s │ %8s │ %7s %7s │ %4s\n" "------" "-------" "-----" "-------" "----"
for e in "${ALL_SIZES[@]}"; do
l="${e%%:*}"; b="${e##*:}"
fid="${FILE_IDS[$l]:-}"
[[ -z "$fid" ]] && continue
login >/dev/null 2>&1
rss0=$(get_rss_kb)
start_monitor
t0=$(date +%s%N)
code=$(curl -s -o /dev/null -w '%{http_code}' \
"$BASE_URL/api/files/$fid" \
-H "Authorization: Bearer $TOKEN" \
-H "Range: bytes=0-65535" 2>&1)
t1=$(date +%s%N)
stop_monitor
ms=$(( (t1 - t0) / 1000000 ))
rss1=$(get_rss_kb)
d=$(( rss1 - rss0 ))
printf "%-8s │ %6dms │ %5dMB %+5dKB │ %4s\n" \
"$l" "$ms" $(mb $MON_PEAK_RSS) "$d" "$code"
echo "range_$l,$b,range,$ms,$(mb $rss0),$(mb $MON_PEAK_RSS),$(mb $rss1),$(mb $MON_PEAK_SWAP),$MON_CPU_PCT,0,$code" >> "$RESULTS"
done
# ============================================================================
# CONCURRENT DOWNLOADS
# ============================================================================
header "Concurrent Downloads"
run_concurrent() {
local lbl="$1" fid="$2" n="$3" each="$4"
local total=$(( each * n ))
login >/dev/null 2>&1
rss0=$(get_rss_kb)
start_monitor
t0=$(date +%s%N)
local pids=()
for i in $(seq 1 $n); do
curl -s -o /dev/null --max-time 7200 "$BASE_URL/api/files/$fid" \
-H "Authorization: Bearer $TOKEN" &
pids+=($!)
done
wait "${pids[@]}"
t1=$(date +%s%N)
stop_monitor
ms=$(( (t1 - t0) / 1000000 ))
rss1=$(get_rss_kb)
tp=$(throughput $total $ms)
printf " ${BLD}%dx %s${NC} %dms %s/s RSS:%dMB→peak %dMB→%dMB Swap:%dMB CPU:%s%%\n" \
"$n" "$lbl" "$ms" "$tp" \
$(mb $rss0) $(mb $MON_PEAK_RSS) $(mb $rss1) $(mb $MON_PEAK_SWAP) "$MON_CPU_PCT"
echo "concurrent_${n}x${lbl},$total,concurrent,$ms,$(mb $rss0),$(mb $MON_PEAK_RSS),$(mb $rss1),$(mb $MON_PEAK_SWAP),$MON_CPU_PCT,0,200" >> "$RESULTS"
}
fid="${FILE_IDS[10MB]:-}"; [[ -n "$fid" ]] && run_concurrent "10MB" "$fid" 5 10485760
fid="${FILE_IDS[100MB]:-}"; [[ -n "$fid" ]] && run_concurrent "100MB" "$fid" 3 104857600
fid="${FILE_IDS[1GB]:-}"; [[ -n "$fid" ]] && run_concurrent "1GB" "$fid" 2 1073741824
fid="${FILE_IDS[5GB]:-}"; [[ -n "$fid" ]] && run_concurrent "5GB" "$fid" 2 5368709120
# ============================================================================
# MEMORY TRACE during 5 GB download
# ============================================================================
header "Memory Trace: RSS during 5 GB download"
fid="${FILE_IDS[5GB]:-}"
if [[ -n "$fid" ]]; then
login >/dev/null 2>&1
TRACE="$WORKDIR/mem_trace_5gb.csv"
echo "elapsed_ms,rss_mb,vmsize_mb,swap_mb" > "$TRACE"
t0=$(date +%s%N)
(
while true; do
now=$(date +%s%N)
el=$(( (now - t0) / 1000000 ))
echo "$el,$(mb $(get_rss_kb)),$(mb $(get_vsize_kb)),$(mb $(get_swap_kb))" >> "$TRACE"
sleep 0.2
done
) &
SAMPLER=$!
curl -s -o /dev/null --max-time 7200 "$BASE_URL/api/files/$fid" -H "Authorization: Bearer $TOKEN"
kill $SAMPLER 2>/dev/null; wait $SAMPLER 2>/dev/null || true
mx=$(awk -F, 'NR>1{if($2>m)m=$2}END{print m+0}' "$TRACE")
mn=$(awk -F, 'NR>1{if(!m||$2<m)m=$2}END{print m+0}' "$TRACE")
samp=$(( $(wc -l < "$TRACE") - 1 ))
printf " Samples: %d (every 200ms)\n" "$samp"
printf " RSS min: %d MB\n" "$mn"
printf " RSS max: %d MB\n" "$mx"
printf " RSS delta: %d MB\n" $(( mx - mn ))
printf " Trace file: %s\n" "$TRACE"
else
warn "5GB file not available"
fi
# ============================================================================
# FINAL STATE
# ============================================================================
header "Final Server State"
printf " RSS: %5d MB\n" $(mb $(get_rss_kb))
printf " VmPeak: %5d MB\n" $(mb $(get_peak_kb))
printf " VmSize: %5d MB\n" $(mb $(get_vsize_kb))
printf " VmSwap: %5d MB\n" $(mb $(get_swap_kb))
echo
printf "${BLD}CSV Results:${NC}\n"
column -t -s',' "$RESULTS" 2>/dev/null || cat "$RESULTS"
echo
ok "Benchmark v3 complete — $RESULTS"