|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Shared measurement functions for warm pool experiments. |
| 3 | +# Source this file from experiment scripts: source "$(dirname "$0")/../lib/common.sh" |
| 4 | + |
| 5 | +set -euo pipefail |
| 6 | + |
| 7 | +EXPERIMENT_ID="${EXPERIMENT_ID:-unknown}" |
| 8 | +RESULTS_DIR="${RESULTS_DIR:-experiments/results}" |
| 9 | + |
| 10 | +CLEANUP_RESOURCES=() |
| 11 | +register_cleanup() { CLEANUP_RESOURCES+=("$1"); } |
| 12 | +deregister_cleanup() { CLEANUP_RESOURCES=("${CLEANUP_RESOURCES[@]/$1}"); } |
| 13 | +_cleanup_on_exit() { |
| 14 | + for res in "${CLEANUP_RESOURCES[@]}"; do |
| 15 | + [[ -z "$res" ]] && continue |
| 16 | + log "Cleaning up: $res" |
| 17 | + kubectl delete $res --ignore-not-found --wait=false 2>/dev/null || true |
| 18 | + done |
| 19 | +} |
| 20 | +trap _cleanup_on_exit EXIT |
| 21 | + |
| 22 | +log() { |
| 23 | + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" |
| 24 | +} |
| 25 | + |
| 26 | +ensure_results_dir() { |
| 27 | + mkdir -p "$RESULTS_DIR" |
| 28 | +} |
| 29 | + |
| 30 | +capture_timestamp() { |
| 31 | + if command -v gdate &>/dev/null; then |
| 32 | + gdate +%s%N |
| 33 | + elif date +%s%N | grep -qv N; then |
| 34 | + date +%s%N |
| 35 | + else |
| 36 | + # Fallback: seconds with 000000000 appended (no nanosecond support) |
| 37 | + echo "$(date +%s)000000000" |
| 38 | + fi |
| 39 | +} |
| 40 | + |
| 41 | +write_csv_header() { |
| 42 | + local file="$1" |
| 43 | + echo "run,experiment,config,create_ts,ready_ts,delta_ms,pod,status,scheduled_ms,pulled_ms,init_ms,supervisor_ms,ssh_ms" > "$file" |
| 44 | +} |
| 45 | + |
| 46 | +write_csv_row() { |
| 47 | + local file="$1" |
| 48 | + local run="$2" |
| 49 | + local config="$3" |
| 50 | + local create_ts="$4" |
| 51 | + local ready_ts="$5" |
| 52 | + local delta_ms="$6" |
| 53 | + local pod="$7" |
| 54 | + local row_status="$8" |
| 55 | + local scheduled_ms="${9:-}" |
| 56 | + local pulled_ms="${10:-}" |
| 57 | + local init_ms="${11:-}" |
| 58 | + local supervisor_ms="${12:-}" |
| 59 | + local ssh_ms="${13:-}" |
| 60 | + echo "${run},${EXPERIMENT_ID},${config},${create_ts},${ready_ts},${delta_ms},${pod},${row_status},${scheduled_ms},${pulled_ms},${init_ms},${supervisor_ms},${ssh_ms}" >> "$file" |
| 61 | +} |
| 62 | + |
| 63 | +_iso_to_epoch() { |
| 64 | + local ts="$1" |
| 65 | + if [[ -z "$ts" || "$ts" == "null" ]]; then |
| 66 | + echo "" |
| 67 | + return |
| 68 | + fi |
| 69 | + if command -v gdate &>/dev/null; then |
| 70 | + gdate -d "$ts" +%s 2>/dev/null || echo "" |
| 71 | + else |
| 72 | + date -d "$ts" +%s 2>/dev/null || echo "" |
| 73 | + fi |
| 74 | +} |
| 75 | + |
| 76 | +_event_ts() { |
| 77 | + local events_json="$1" |
| 78 | + local reason="$2" |
| 79 | + local pick="${3:-first}" |
| 80 | + local selector=".reason == \"$reason\"" |
| 81 | + if [[ "$reason" == *"|"* ]]; then |
| 82 | + local r1="${reason%%|*}" |
| 83 | + local r2="${reason#*|}" |
| 84 | + selector=".reason == \"$r1\" or .reason == \"$r2\"" |
| 85 | + fi |
| 86 | + if [[ "$pick" == "last" ]]; then |
| 87 | + echo "$events_json" | jq -r "[.items[] | select($selector)] | last | (.lastTimestamp // .eventTime // null)" 2>/dev/null |
| 88 | + else |
| 89 | + echo "$events_json" | jq -r "[.items[] | select($selector)] | first | (.lastTimestamp // .eventTime // null)" 2>/dev/null |
| 90 | + fi |
| 91 | +} |
| 92 | + |
| 93 | +extract_phase_deltas() { |
| 94 | + local events_json="$1" |
| 95 | + local create_ts="$2" |
| 96 | + local create_s=$((create_ts / 1000000000)) |
| 97 | + |
| 98 | + local sched_ts pull_ts init_ts start_ts |
| 99 | + sched_ts=$(_event_ts "$events_json" "Scheduled" "first") |
| 100 | + pull_ts=$(_event_ts "$events_json" "Pulled|Pulling" "last") |
| 101 | + init_ts=$(_event_ts "$events_json" "Created" "first") |
| 102 | + start_ts=$(_event_ts "$events_json" "Started" "last") |
| 103 | + |
| 104 | + local sched_ms="" pull_ms="" init_ms="" start_ms="" |
| 105 | + |
| 106 | + local epoch |
| 107 | + epoch=$(_iso_to_epoch "$sched_ts") |
| 108 | + [[ -n "$epoch" ]] && sched_ms=$(( (epoch - create_s) * 1000 )) |
| 109 | + |
| 110 | + epoch=$(_iso_to_epoch "$pull_ts") |
| 111 | + [[ -n "$epoch" ]] && pull_ms=$(( (epoch - create_s) * 1000 )) |
| 112 | + |
| 113 | + epoch=$(_iso_to_epoch "$init_ts") |
| 114 | + [[ -n "$epoch" ]] && init_ms=$(( (epoch - create_s) * 1000 )) |
| 115 | + |
| 116 | + epoch=$(_iso_to_epoch "$start_ts") |
| 117 | + [[ -n "$epoch" ]] && start_ms=$(( (epoch - create_s) * 1000 )) |
| 118 | + |
| 119 | + echo "${sched_ms},${pull_ms},${init_ms},${start_ms}," |
| 120 | +} |
| 121 | + |
| 122 | +collect_pod_events() { |
| 123 | + local pod="$1" |
| 124 | + local ns="${2:-default}" |
| 125 | + kubectl get events \ |
| 126 | + --namespace="$ns" \ |
| 127 | + --field-selector="involvedObject.name=$pod,involvedObject.kind=Pod" \ |
| 128 | + --sort-by='.lastTimestamp' \ |
| 129 | + -o json 2>/dev/null || echo '{"items":[]}' |
| 130 | +} |
| 131 | + |
| 132 | +detect_adoption() { |
| 133 | + local pod_name="$1" |
| 134 | + local ns="${2:-default}" |
| 135 | + if [[ -z "$pod_name" ]]; then |
| 136 | + echo "unknown" |
| 137 | + return |
| 138 | + fi |
| 139 | + |
| 140 | + local events_json |
| 141 | + events_json=$(collect_pod_events "$pod_name" "$ns") |
| 142 | + |
| 143 | + local scheduled_count |
| 144 | + scheduled_count=$(echo "$events_json" | \ |
| 145 | + jq '[.items[] | select(.reason == "Scheduled")] | length' 2>/dev/null || echo "0") |
| 146 | + |
| 147 | + local pulled_count |
| 148 | + pulled_count=$(echo "$events_json" | \ |
| 149 | + jq '[.items[] | select(.reason == "Pulling" or .reason == "Pulled")] | length' 2>/dev/null || echo "0") |
| 150 | + |
| 151 | + if (( scheduled_count == 0 && pulled_count == 0 )); then |
| 152 | + echo "warm-adopted" |
| 153 | + else |
| 154 | + echo "cold-fallback" |
| 155 | + fi |
| 156 | +} |
| 157 | + |
| 158 | +compute_stats() { |
| 159 | + local csv_file="$1" |
| 160 | + if [[ ! -f "$csv_file" ]]; then |
| 161 | + log "ERROR: CSV file not found: $csv_file" |
| 162 | + return 1 |
| 163 | + fi |
| 164 | + |
| 165 | + awk -F',' ' |
| 166 | + NR == 1 { next } |
| 167 | + $8 != "ok" { skipped++; next } |
| 168 | + $6 == "" { skipped++; next } |
| 169 | + { |
| 170 | + v = $6 + 0 |
| 171 | + n++ |
| 172 | + vals[n] = v |
| 173 | + sum += v |
| 174 | + if (n == 1 || v < min) min = v |
| 175 | + if (n == 1 || v > max) max = v |
| 176 | + } |
| 177 | + END { |
| 178 | + if (n == 0) { |
| 179 | + print "No data rows found." |
| 180 | + exit 1 |
| 181 | + } |
| 182 | +
|
| 183 | + # Sort values (insertion sort) |
| 184 | + for (i = 2; i <= n; i++) { |
| 185 | + key = vals[i] |
| 186 | + j = i - 1 |
| 187 | + while (j >= 1 && vals[j] > key) { |
| 188 | + vals[j+1] = vals[j] |
| 189 | + j-- |
| 190 | + } |
| 191 | + vals[j+1] = key |
| 192 | + } |
| 193 | +
|
| 194 | + p50_idx = int(n * 0.5 + 0.5) |
| 195 | + p90_idx = int(n * 0.9 + 0.5) |
| 196 | + if (p50_idx < 1) p50_idx = 1 |
| 197 | + if (p90_idx < 1) p90_idx = 1 |
| 198 | + if (p50_idx > n) p50_idx = n |
| 199 | + if (p90_idx > n) p90_idx = n |
| 200 | +
|
| 201 | + mean = sum / n |
| 202 | + printf "\n--- Statistics (%d samples) ---\n", n |
| 203 | + printf " min: %10.1f ms\n", min |
| 204 | + printf " max: %10.1f ms\n", max |
| 205 | + printf " mean: %10.1f ms\n", mean |
| 206 | + printf " p50: %10.1f ms\n", vals[p50_idx] |
| 207 | + printf " p90: %10.1f ms\n", vals[p90_idx] |
| 208 | + printf "-------------------------------\n" |
| 209 | + if (skipped > 0) printf " (%d non-ok rows excluded)\n", skipped |
| 210 | + } |
| 211 | + ' "$csv_file" |
| 212 | +} |
0 commit comments