Files
openairinterface5g/ci-scripts/yaml_files/5g_rfsimulator/exp.sh
T
2025-12-01 14:49:02 +09:00

651 lines
22 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
set -euo pipefail
# --- UE profile map (index 0..10). 0-index kept for convenience ---
# Initialize with "0" to denote no profile chosen.
declare -a UE_PROFILE_MAP
for i in {0..10}; do UE_PROFILE_MAP[$i]=0; done
# ---------------------------[ Random / diurnal Mode ]----------------------------
# UE scaler mode: 'random' (rapid random targets) or 'diurnal' (legacy cosine)
UE_SCALER_MODE="${UE_SCALER_MODE:-random}"
# Interval (sec) for scaler ticks
UE_SCALER_INTERVAL_SEC="${UE_SCALER_INTERVAL_SEC:-30}"
# -------------------------------[ Config ]------------------------------------
ARTIFACT_DIR="/media/nakaolab-go/2nd/artifacts"
mkdir -p "$ARTIFACT_DIR"
POWER_LOG_FILE="${POWER_LOG_FILE:-$ARTIFACT_DIR/power_usage.csv}"
CPU_PROXY_LOG_FILE="${CPU_PROXY_LOG_FILE:-$ARTIFACT_DIR/cpu_proxy.csv}"
RRC_SINCE_STATE="${RRC_SINCE_STATE:-$ARTIFACT_DIR/.rrc_since}"
# xApp (mac stats) container + path inside container
STATS_CONTAINER="${STATS_CONTAINER:-xapp-chatgpt}"
MAC_STATS_FILE_IN_CONTAINER="${MAC_STATS_FILE_IN_CONTAINER:-/tmp/mac_stats.log}"
PDCP_STATS_FILE_IN_CONTAINER="${PDCP_STATS_FILE_IN_CONTAINER:-/tmp/pdcp_stats.log}"
# Intervals (seconds)
RRC_LOG_INTERVAL_SEC="${RRC_LOG_INTERVAL_SEC:-10}"
POWER_LOG_INTERVAL_SEC="${POWER_LOG_INTERVAL_SEC:-5}"
CPU_PROXY_INTERVAL_SEC="${CPU_PROXY_INTERVAL_SEC:-5}"
# Docker compose helper
DC_BIN=(docker compose)
dc() { command "${DC_BIN[@]}" "$@"; }
dc down -v --remove-orphans
# Service names (override if your compose uses different ones)
AMF_SERVICE="${AMF_SERVICE:-oai-amf}"
UPF_SERVICE="${UPF_SERVICE:-oai-upf}"
GNB_SERVICE="${GNB_SERVICE:-oai-gnb}"
XAPP_SERVICE="${XAPP_SERVICE:-xapp-chatgpt}"
# UE service name pattern (for discovery)
UE_NAME_REGEX="${UE_NAME_REGEX:-^oai-nr-ue}"
# Test controls
TEST_MODE="${TEST_MODE:-0}" # 1=manual profiles allowed (TEST_* honored); 0=auto
TEST_UES="${TEST_UES:-}"
TEST_PROFILES="${TEST_PROFILES:-}" # profile name
TEST_DURATION_SEC="${TEST_DURATION_SEC:-0}" # 0 = run indefinitely
# Diurnal scaling controls (normal mode only; ignored if TEST_UE is set)
UE_MIN="${UE_MIN:-1}" # Minimum number of UEs to keep connected
UE_MAX="${UE_MAX:-10}"
UE_PEAK_HOUR="${UE_PEAK_HOUR:-17}" # Local hour with peak load (0..23)
UE_NOISE_FRAC="${UE_NOISE_FRAC:-0.15}" # ±fractional noise applied to target
# -------------------------[ Power Model (mW) ]--------------------------------
declare -A PWR_MW_PROFILE=(
[idle]=60 # screen on, modem camped, no transfers
[iot_chatty]=150 # tiny bursts, long sleeps
[browsing]=380 # short transfers with think-time
[voip]=520 # steady uplink/downlink small packets
[video]=850 # continuous DL; 720–1080p
[gaming]=950 # bursty DL + UL keepalives
[boring]=500 # for single PRB testing
[bulk_download]=1100 # sustained UL/DL at MCS ceiling
)
# -------------------------------[ Utils ]-------------------------------------
ts_now() { date +%s; }
ts_iso() { date -Is; }
ts_tag() { date +%Y%m%d_%H%M%S; }
log() { echo "[$(ts_iso)] $*"; }
exec_safe() {
local svc="$1"; shift
dc exec -T "$svc" bash -lc "$*"
}
# -----------------------------[ Discovery ]-----------------------------------
list_all_services() { dc config --services; }
get_all_ue_services() { list_all_services | grep -E "${UE_NAME_REGEX}" || true; }
get_core_services() {
# everything except UEs, gNB, xApp
list_all_services | grep -Ev "(${UE_NAME_REGEX}|^${GNB_SERVICE}$|^${XAPP_SERVICE}$)" || true
}
get_running_ues() {
dc ps --services --filter "status=running" \
| grep -E "${UE_NAME_REGEX}" || true
}
# -------------------------[ Readiness helpers ]-------------------------------
svc_cid() { dc ps -q "$1"; } # container id for a service
wait_healthy() {
local svc="$1" timeout="${2:-60}"
local cid; cid="$(svc_cid "$svc")"
if [[ -z "$cid" ]]; then
log "wait_healthy: service '$svc' not running."
return 1
fi
local health
health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$cid" 2>/dev/null || true)"
if [[ -z "$health" ]]; then
log "wait_healthy: no healthcheck for '$svc'; skipping."
return 0
fi
local start; start="$(ts_now)"
while :; do
health="$(docker inspect -f '{{.State.Health.Status}}' "$cid" 2>/dev/null || echo "")"
if [[ "$health" == "healthy" ]]; then
log "$svc is healthy."
return 0
fi
if (( $(ts_now) - start > timeout )); then
log "wait_healthy timeout for '$svc' (last='$health')."
return 1
fi
sleep 1
done
}
wait_log() {
local svc="$1" pat="$2" timeout="${3:-60}"
local cid; cid="$(svc_cid "$svc")"
if [[ -z "$cid" ]]; then
log "wait_log: service '$svc' not running."
return 1
fi
local start; start="$(ts_now)"
while :; do
if dc logs "$svc" --since=2m 2>&1 | grep -Eiq -- "$pat"; then
return 0
fi
if (( $(ts_now) - start > timeout )); then
log "wait_log timeout for '$svc' waiting '$pat'."
return 1
fi
sleep 1
done
}
get_container_ip() {
local cid; cid="$(svc_cid "$1")"
[[ -z "$cid" ]] && return 1
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$cid"
}
# --------------------------[ Staged bring-up ]--------------------------------
start_services_list() {
[[ $# -eq 0 ]] && return 0
dc up -d "$@" >/dev/null 2>&1
}
start_ue_list() {
[[ $# -eq 0 ]] && return 0
for svc in "$@"; do
log "Starting: $svc"
dc up -d "$svc" >/dev/null 2>&1
done
}
start_in_stages() {
# 1) CORE
mapfile -t core_svcs < <(get_core_services)
if [[ ${#core_svcs[@]} -gt 0 ]]; then
start_services_list "${core_svcs[@]}"
else
log "No CORE services to start."
exit 1
fi
# Ensure AMF is ready
wait_healthy "${AMF_SERVICE}" 15
wait_healthy "${UPF_SERVICE}" 15
# exec_safe "${UPF_SERVICE}" 'bash -lc "
# set -e
# sysctl -w net.ipv4.ip_forward=1
# sysctl -w net.ipv4.conf.all.rp_filter=2
# sysctl -w net.ipv4.conf.eth0.rp_filter=2
# sysctl -w net.ipv4.conf.eth1.rp_filter=2
# sysctl -w net.ipv4.conf.tun0.rp_filter=2
# ip rule add pref 100 iif tun0 to 192.168.71.0/24 lookup 201 || true
# ip route replace 192.168.71.0/24 dev eth0 table 201
# ip rule add pref 200 iif tun0 lookup 200 || true
# ip route replace 12.1.1.0/24 dev tun0 table 200
# ip route replace default via 192.168.72.129 dev eth1 table 200
# # UE -> DN (192.168.72.128/26) via eth1
# iptables -t nat -C POSTROUTING -s 12.1.1.0/24 -d 192.168.72.128/26 -o eth1 -j MASQUERADE 2>/dev/null || \
# iptables -t nat -A POSTROUTING -s 12.1.1.0/24 -d 192.168.72.128/26 -o eth1 -j MASQUERADE
# # UE -> 192.168.71.0/24 via eth0
# iptables -t nat -C POSTROUTING -s 12.1.1.0/24 -d 192.168.71.0/24 -o eth0 -j MASQUERADE 2>/dev/null || \
# iptables -t nat -A POSTROUTING -s 12.1.1.0/24 -d 192.168.71.0/24 -o eth0 -j MASQUERADE
# "'
wait_healthy oai-flexric 15
# 2) gNB
start_services_list "${GNB_SERVICE}"
wait_log "${GNB_SERVICE}" "Received NGSetupResponse from AMF" 20
wait_log "${GNB_SERVICE}" "Transaction ID E2 SETUP-REQUEST 0 E2 SETUP-RESPONSE 0" 15
# 3) xApp
start_services_list "${XAPP_SERVICE}"
wait_healthy "${XAPP_SERVICE}" 15
# 4) UE(s)
# if [[ -n "${TEST_UE:-}" ]]; then
# # Allow multiple UEs in TEST_UE (comma and/or space separated)
# IFS=', ' read -r -a _ue_list <<< "${TEST_UE}"
# start_services_list "${_ue_list[@]}"
# for ue in "${_ue_list[@]}"; do
# [[ -z "$ue" ]] && continue
# if wait_oai_tun "$ue" 20; then
# exec_safe "$ue" 'ip route replace default dev oaitun_ue1' || true
# run_traffic_profile "$ue" "random" "0" || true
# else
# exit 1
# fi
# done
# else
# # Initial diurnal target, then enforce UE count
# local target; if [[ "${UE_SCALER_MODE}" == "diurnal" ]]; then target="$(calc_target_ue_count)"; else target="$(random_target_ue_count)"; fi
# log "Initial diurnal target UE count: ${target}"
# ensure_ue_count "$target"
# fi
}
# -------------------------[ Diurnal UE scaling ]------------------------------
calc_target_ue_count() {
local total_ues="${UE_MAX:-}"
if [[ -z "$total_ues" ]]; then
total_ues="$(get_all_ue_services | wc -l | tr -d ' ')"
fi
local min="${UE_MIN:-1}"
if (( total_ues < min )); then total_ues="$min"; fi
local H M
H="$(date +%H)"; M="$(date +%M)"
# Compute target via cosine wave (peak at UE_PEAK_HOUR), then add noise and clamp
awk -v min="$min" -v max="$total_ues" -v peak="${UE_PEAK_HOUR:-14}" \
-v noise="${UE_NOISE_FRAC:-0.15}" -v H="$H" -v M="$M" 'BEGIN{
# time-of-day in days [0,1)
t = (H*60.0 + M)/1440.0
ph = peak/24.0
norm = (cos(2*3.141592653589793*(t - ph)) + 1)/2 # 0..1
base = min + norm*(max-min)
srand(); u = (rand()*2.0 - 1.0) * noise # [-noise, +noise]
val = base * (1.0 + u)
if (val < min) val = min
if (val > max) val = max
# round to nearest integer
printf("%.0f\n", val)
}'
}
ensure_ue_count() {
local target="$1"
# Build arrays
mapfile -t all_ues < <(get_all_ue_services)
mapfile -t running < <(get_running_ues)
# Current and desired counts
local cur="${#running[@]}"
if (( cur == target )); then
log "UE scaler: already at target (${target})."
return 0
fi
# Set helpers for membership tests
declare -A IS_RUNNING=()
for u in "${running[@]}"; do IS_RUNNING["$u"]=1; done
# Determine non-running UEs
declare -a not_running=()
for u in "${all_ues[@]}"; do
[[ -z "${IS_RUNNING[$u]:-}" ]] && not_running+=("$u")
done
# Start more UEs if needed
if (( cur < target )); then
local need=$(( target - cur ))
log "UE scaler: need +${need} UEs (cur=${cur}, target=${target})."
if (( ${#not_running[@]} == 0 )); then
log "UE scaler: no available UEs to start."
return 0
fi
local to_start=()
if command -v shuf >/dev/null 2>&1; then
mapfile -t to_start < <(printf '%s\n' "${not_running[@]}" | shuf -n "$need" || true)
else
to_start=("${not_running[@]:0:$need}")
fi
if (( ${#to_start[@]} > 0 )); then
start_ue_list "${to_start[@]}"
for ue in "${to_start[@]}"; do
if wait_log "$ue" "oaitun_ue1 successfully configured" 5 &&
exec_safe "$ue" 'ip link show oaitun_ue1 >/dev/null 2>&1'; then
exec_safe "$ue" 'ip route replace default dev oaitun_ue1'
# run_traffic_profile "$ue" "$(choose_random_profile)" "0"
run_traffic_profile "$ue" "boring"
else
dc down -v "$ue" >/dev/null 2>&1 &
fi
done
fi
return 0
fi
# Otherwise stop some UEs to shrink
local drop=$(( cur - target ))
log "UE scaler: need -${drop} UEs (cur=${cur}, target=${target})."
local to_stop=()
if command -v shuf >/dev/null 2>&1; then
mapfile -t to_stop < <(printf '%s\n' "${running[@]}" | shuf -n "$drop" || true)
else
to_stop=("${running[@]:0:$drop}")
fi
for ue in "${to_stop[@]}"; do
dc down -v "$ue" >/dev/null 2>&1 &
done
wait
}
# --------------------------[ UE traffic helpers ]-----------------------------
PROFILES=(boring browsing video bulk_download voip iot_chatty gaming)
choose_random_profile() {
if command -v shuf >/dev/null 2>&1; then
shuf -e "${PROFILES[@]}" -n1
else
idx=$(( RANDOM % ${#PROFILES[@]} ))
echo "${PROFILES[$idx]}"
fi
}
run_traffic_profile() {
local ue="$1" profile="$2"
# Record selected profile for this UE into UE_PROFILE_MAP
if [[ "$ue" =~ ([0-9]+)$ ]]; then
__idx="${BASH_REMATCH[1]}"
UE_PROFILE_MAP[$__idx]="$profile"
fi
dc cp /home/nakaolab-go/OAI/openairinterface5g/tools/scripts/traffic_runner.sh "$ue":/tmp/traffic_runner.sh >/dev/null 2>&1
exec_safe "$ue" "bash -lc '
prof=\$1;
/tmp/traffic_runner.sh --profile \$prof &
' -- $profile"
}
# --------------------------[ RRC / MAC scraping ]-----------------------------
rotate_stats_capture() {
local tag="${1:-}"
[[ -z "$tag" ]] && tag="$(ts_tag)"
if dc ps --services --filter "status=running" | grep -q "^${STATS_CONTAINER}$"; then
log "Rotating stats from ${STATS_CONTAINER}"
dc cp "${STATS_CONTAINER}:${MAC_STATS_FILE_IN_CONTAINER}" \
"${ARTIFACT_DIR}/mac_stats_${tag}.log" || true
dc cp "${STATS_CONTAINER}:${PDCP_STATS_FILE_IN_CONTAINER}" \
"${ARTIFACT_DIR}/pdcp_stats_${tag}.log" || true
# exec_safe "$STATS_CONTAINER" "> '${MAC_STATS_FILE_IN_CONTAINER}'"
# exec_safe "$STATS_CONTAINER" "> '${PDCP_STATS_FILE_IN_CONTAINER}'"
else
log "stats container ${STATS_CONTAINER} not running; skipping rotate"
fi
}
_since_arg_from_file() {
local f="$1"
local val
val="$(tr -d '[]\r\n' < "$f" 2>/dev/null || true)"
if [[ -n "$val" ]]; then
printf -- '--since=%s' "$val"
else
printf -- '--since=2m'
fi
}
dump_gnb_logs() {
local tag="$1"
dc logs "${GNB_SERVICE}" 2>&1 | grep -E "NR_RRC" > "${ARTIFACT_DIR}/gnb_rrc_${tag}.log"
dc logs "${GNB_SERVICE}" 2>&1 | grep -E "\[NR_MAC\] I Frame\.Slot|MAC:" > "${ARTIFACT_DIR}/gnb_${tag}.log"
}
# -----------------------------[ Power / CPU ]---------------------------------
init_power_logs() {
if [[ ! -s "$POWER_LOG_FILE" ]]; then
echo "epoch,ue,profile,power_mW,dt_s,energy_mJ" > "$POWER_LOG_FILE"
fi
if [[ ! -s "$CPU_PROXY_LOG_FILE" ]]; then
echo "epoch,container,cpu_percent,mem_usage" > "$CPU_PROXY_LOG_FILE"
fi
}
profile_total_mw() {
local profile="$1"
echo "${PWR_MW_PROFILE[$profile]:-0}"
}
sample_power_once() {
local epoch="$(ts_now)"
local dt_s="${POWER_LOG_INTERVAL_SEC}"
while IFS= read -r ue; do
[[ -z "$ue" ]] && continue
# Determine profile without container lock by consulting UE_PROFILE_MAP
local picked_profile picked_duration
if [[ "$ue" =~ ([0-9]+)$ ]]; then
__idx="${BASH_REMATCH[1]}"
picked_profile="${UE_PROFILE_MAP[$__idx]}"
fi
picked_duration=""
if [[ -z "$picked_profile" ]]; then
if [[ "${TEST_MODE}" -eq 1 ]]; then picked_profile="${TEST_PROFILE}"; else picked_profile="idle"; fi
fi
while [[ "$picked_profile" == -* ]]; do
picked_profile="${picked_profile#-}"
done
picked_profile="${picked_profile,,}"
local p_mw
p_mw="$(profile_total_mw "$picked_profile")"
: "${p_mw:=0}"
local energy_mJ=$(( p_mw * dt_s ))
echo "${epoch},${ue},${picked_profile},${p_mw},${dt_s},${energy_mJ}" >> "$POWER_LOG_FILE"
done < <(get_running_ues)
}
# --- CPU proxy: query docker stats by container ID and map back to service name ---
cpu_proxy_once() {
local epoch="$(ts_now)"
# Build list of UE service names (same as container names)
declare -a SVC_LIST=()
while IFS= read -r svc; do
[[ -z "$svc" ]] && continue
SVC_LIST+=("$svc")
done < <(get_running_ues)
# Query docker stats by NAME and key 'seen' by NAME
local fmt='{{.Name}},{{.CPUPerc}},{{.MemUsage}}'
declare -A SEEN=()
while IFS=',' read -r name cpu mem; do
[[ -z "$name" ]] && continue
name="${name#/}"
SEEN["$name"]=1
cpu="${cpu%%%}" # drop trailing '%'
echo "${epoch},${name},${cpu},${mem}" >> "$CPU_PROXY_LOG_FILE"
done < <(docker stats --no-stream --format "$fmt" "${SVC_LIST[@]}" 2>/dev/null || true)
# Any container absent from docker stats output → record zeros once
local svc
for svc in "${SVC_LIST[@]}"; do
if [[ -z "${SEEN[$svc]:-}" ]]; then
echo "${epoch},${svc},0,0" >> "$CPU_PROXY_LOG_FILE"
fi
done
}
rotate_csv_if_any() {
local src="$1" prefix="$2" tag="${3:-}"
[[ -z "$tag" ]] && tag="$(ts_tag)"
if [[ -s "$src" ]]; then
mv "$src" "${ARTIFACT_DIR}/${prefix}_${tag}.csv" || true
case "$prefix" in
power_usage) echo "epoch,ue,profile,power_mW,dt_s,energy_mJ" > "$src" ;;
cpu_proxy) echo "epoch,container,cpu_percent,mem_usage" > "$src" ;;
esac
fi
}
finalize_and_rotate_artifacts() {
local tag; tag="$(ts_tag)"
rotate_stats_capture "$tag" || true
dump_gnb_logs "$tag" || true
rotate_csv_if_any "$POWER_LOG_FILE" "power_usage" "$tag"
rotate_csv_if_any "$CPU_PROXY_LOG_FILE" "cpu_proxy" "$tag"
}
# -------------------------------[ Signals ]-----------------------------------
__SHUTTING_DOWN=0
on_interrupt() {
if [[ "${__SHUTTING_DOWN}" -eq 1 ]]; then return; fi
__SHUTTING_DOWN=1
log "Signal received (INT/TERM); taking final samples and rotating artifacts…"
# One last sample tick
sample_power_once || true
cpu_proxy_once || true
# Rotate artifacts and clean up
finalize_and_rotate_artifacts
cleanup
# 130 is the conventional exit code for SIGINT
exit 130
}
# --------------------------------[ Cleanup ]----------------------------------
cleanup() {
# Default: stop only UEs; keep infra up for inspection
log "Cleanup: stopping UE containers only"
while IFS= read -r ue; do
[[ -z "$ue" ]] && continue
dc down -v "$ue" >/dev/null 2>&1 &
done < <(dc ps --services | grep -E "${UE_NAME_REGEX}" || true)
}
# ---------------------------------[ Main ]------------------------------------
random_target_ue_count() {
local cur="$(get_running_ues | wc -l | tr -d ' ')"
bias=$((RANDOM % 100)) # 0–99
if (( cur <= UE_MIN )); then
if (( bias < 70 )); then
delta=$(( (RANDOM % 3) + 1 )) # +1..+3
else
delta=$(( RANDOM % 7 - 3 )) # -3..+3
fi
elif (( cur >= UE_MAX )); then
if (( bias < 70 )); then
delta=$(( -((RANDOM % 3) + 1) )) # -1..-3
else
delta=$(( RANDOM % 7 - 3 )) # -3..+3
fi
else # normal
delta=$(( RANDOM % 7 - 3 )) # -3..+3
fi
local target=$((cur + delta))
if (( target < UE_MIN )); then
target="${UE_MIN}"
elif (( target > UE_MAX )); then
target="${UE_MAX}"
fi
echo "$target"
}
main() {
trap on_interrupt INT TERM
init_power_logs
# Staged bring-up with readiness checks for both normal and TEST modes
start_in_stages
# Kick off traffic workload(s)
if [[ "${TEST_MODE}" -eq 1 ]]; then
log "TEST UE(S) ENABLED"
# Support multiple UEs (comma/space/newline separated)
read -r -a _ue_list <<< "$(tr ',\n' ' ' <<< "${TEST_UES}")"
read -r -a _ue_profiles <<< "$(tr ',\n' ' ' <<< "${TEST_PROFILES}")"
start_ue_list "${_ue_list[@]}"
for i in "${!_ue_list[@]}"; do
ue="${_ue_list[$i]}"
prof="${_ue_profiles[$i]}"
[[ -z "$ue" ]] && continue
if wait_log "$ue" "oaitun_ue1 successfully configured" 5 &&
exec_safe "$ue" 'ip link show oaitun_ue1 >/dev/null 2>&1'; then
exec_safe "$ue" "ip route replace default dev oaitun_ue1"
run_traffic_profile "$ue" "$prof"
else
exit 1
fi
done
fi
# Optional finite duration for TEST mode
if [[ "${TEST_MODE}" -eq 1 && -n "${TEST_UE:-}" && "${TEST_DURATION_SEC:-0}" -gt 0 ]]; then
RUN_STOP_TS=$(( $(ts_now) + TEST_DURATION_SEC ))
fi
# Main sampling & scaling loop
local last_rrc="$(( $(ts_now) - RRC_LOG_INTERVAL_SEC ))"
local last_power="$(( $(ts_now) - POWER_LOG_INTERVAL_SEC ))"
local last_cpu="$(( $(ts_now) - CPU_PROXY_INTERVAL_SEC ))"
local last_scale="$(( $(ts_now) - UE_SCALER_INTERVAL_SEC ))"
log "Entering main loop (sampling + diurnal scaler)…"
while :; do
local now; now="$(ts_now)"
if (( now - last_power >= POWER_LOG_INTERVAL_SEC )); then
sample_power_once || true
last_power="$now"
fi
if (( now - last_cpu >= CPU_PROXY_INTERVAL_SEC )); then
cpu_proxy_once || true
last_cpu="$now"
fi
# Re-evaluate diurnal target (normal mode only)
if [[ "${TEST_MODE}" -eq 0 ]] && (( now - last_scale >= UE_SCALER_INTERVAL_SEC )); then
local target;
if [[ "${UE_SCALER_MODE}" == "diurnal" ]]; then
target="$(calc_target_ue_count)";
else
target="$(random_target_ue_count)";
fi
log "Scaler tick: target = ${target}"
ensure_ue_count "$target"
last_scale="$now"
fi
# TEST duration stop gate
if [[ -n "${RUN_STOP_TS:-}" && "$now" -ge "${RUN_STOP_TS}" ]]; then
log "TEST run reached duration (${TEST_DURATION_SEC}s); finalizing artifacts…"
finalize_and_rotate_artifacts
cleanup
exit 0
fi
sleep 1
done
}
main "$@"