#!/bin/bash

# Measure client JVM memory + startup with and without AppCDS, for A/B comparison.
#
# For each mode it launches one or more clients (via client-terminal.sh by default),
# waits for the ClientDriver JVM(s) to appear, samples memory while they settle,
# then stops them and reports aggregate numbers:
#   - "appcds"    : default client.sh behavior (curated classpath + shared archive)
#   - "no-appcds" : client.sh --no-appcds (full classpath discovery, no archive)
#
# Options (before an optional "--"; everything after "--" is passed to BOTH runs):
#   --settle N    seconds to sample each run        (default 12)
#   --clients K   launch K concurrent clients       (default 1)
#   --launcher P  entry point to launch             (default ./client-terminal.sh)
#
# Examples:
#   ./appcds_mem.sh -- -i 1                 # single ChUI client
#   ./appcds_mem.sh --clients 5 -- -i 1     # 5 clients: see cross-process archive sharing
#   ./appcds_mem.sh --launcher ./client.sh -- --swing
#
# Why K>1 matters: a single client's CDS archive is a PRIVATE mapping (shows in
# PrivClean), so single-process RSS rarely drops. With several clients mapping the
# SAME .jsa, those pages are shared -> SharedClean rises and total Pss (which splits
# shared pages across mappers) drops. That sharing, plus faster startup (ToSettle),
# is AppCDS's actual win.
#
# NOTE: clients must actually start and reach idle for the numbers to mean anything
# (server running + any required connect args). Runs that don't come up are flagged.

srcpath="$(cd "$(dirname "$0")"; pwd)"

settle=12
clients=1
launcher="$srcpath/client-terminal.sh"
while [[ $# -gt 0 ]]; do
   case "$1" in
      --settle)   settle="$2"; shift 2 ;;
      --clients)  clients="$2"; shift 2 ;;
      --launcher) launcher="$2"; shift 2 ;;
      --)         shift; break ;;
      *)          break ;;
   esac
done
passthrough=("$@")

now()    { date +%s.%N; }
elapsed(){ awk -v a="$1" -v b="$2" 'BEGIN{ d=b-a; if(d<0)d=0; printf "%.1f", d }'; }
mb()     { awk -v k="${1:-0}" 'BEGIN{ printf "%.1f", k/1024 }'; }

vmrss_kb(){ awk '/^VmRSS:/{print $2}' "/proc/$1/status" 2>/dev/null; }

# Sum VmRSS (kB) across all pids in "$@".
agg_vmrss(){ local p s=0 r; for p in "$@"; do r=$(vmrss_kb "$p"); [ -n "$r" ] && s=$((s+r)); done; echo "$s"; }

# Sum smaps_rollup fields across pids. Echoes: "rss pss shclean privclean privdirty" (kB).
agg_smaps(){
   awk '
      /^Rss:/{r+=$2} /^Pss:/{p+=$2} /^Shared_Clean:/{s+=$2}
      /^Private_Clean:/{pc+=$2} /^Private_Dirty:/{d+=$2}
      END{ printf "%d %d %d %d %d", r, p, s, pc, d }' \
      $(for p in "$@"; do echo "/proc/$p/smaps_rollup"; done) 2>/dev/null
}

# Run one mode. $1=label, $2=extra client flag ("" or "--no-appcds").
# Echoes: "label nUp peakKB rssKB pssKB shKB pcKB pdKB archive toSettle status"
run_one()
{
   local label="$1" flag="$2"
   local before; before="$(pgrep -f 'com.goldencode.p2j.main.ClientDriver' 2>/dev/null | sort)"

   # Launch $clients instances, each under a pty (script) with stdin held open so a
   # native ChUI driver idles instead of seeing EOF; setsid gives each its own group.
   local sids=() c log hold=$(( settle + 30 ))
   local t0; t0="$(now)"
   for c in $(seq 1 "$clients"); do
      log="$(mktemp)"
      setsid bash -c "sleep $hold | script -qfc \"$launcher $flag ${passthrough[*]}\" /dev/null" >"$log" 2>&1 &
      sids+=("$!")
   done

   # Wait (up to ~30s) for all $clients new ClientDriver JVMs to appear.
   local pids=() i
   for i in $(seq 1 60); do
      local now_pids; now_pids="$(pgrep -f 'com.goldencode.p2j.main.ClientDriver' 2>/dev/null | sort)"
      mapfile -t pids < <(comm -13 <(printf '%s\n' "$before") <(printf '%s\n' "$now_pids"))
      [ "${#pids[@]}" -ge "$clients" ] && break
      sleep 0.5
   done

   local status="ok" peak=0 peak_t="$t0"
   local nUp="${#pids[@]}"
   [ "$nUp" -lt "$clients" ] && status="only ${nUp}/${clients} up"
   if [ "$nUp" -eq 0 ]; then
      status="no-jvm"
   else
      # Sample aggregate RSS; track peak and the time of the last peak increase
      # (a proxy for "warmup done" -- RSS ramps during class loading, then plateaus).
      local iters=$(( settle * 5 )) agg
      for i in $(seq 1 "$iters"); do
         agg="$(agg_vmrss "${pids[@]}")"
         if [ "${agg:-0}" -gt "$peak" ]; then peak="$agg"; peak_t="$(now)"; fi
         # stop early if everything died
         local alive=0 p; for p in "${pids[@]}"; do [ -d "/proc/$p" ] && alive=1; done
         [ "$alive" -eq 0 ] && { status="exited-early"; break; }
         sleep 0.2
      done
   fi
   local toSettle; toSettle="$(elapsed "$t0" "$peak_t")"

   # Aggregate smaps + archive-mapped check (need at least one live pid).
   local rss=0 pss=0 shclean=0 privclean=0 privdirty=0 archive="-"
   local live=()
   for p in "${pids[@]}"; do [ -d "/proc/$p" ] && live+=("$p"); done
   if [ "${#live[@]}" -gt 0 ]; then
      read -r rss pss shclean privclean privdirty < <(agg_smaps "${live[@]}")
      if grep -q '/appcds/client/.*\.jsa' "/proc/${live[0]}/maps" 2>/dev/null; then
         archive="mapped"
      else
         archive="absent"
      fi
   fi

   # Stop every client's process group.
   for c in "${sids[@]}"; do kill -TERM -"$c" 2>/dev/null; done
   sleep 1
   for c in "${sids[@]}"; do kill -KILL -"$c" 2>/dev/null; done
   wait 2>/dev/null

   printf '%s %s %s %s %s %s %s %s %s %s %s\n' \
      "$label" "$nUp" "$peak" "$rss" "$pss" "$shclean" "$privclean" "$privdirty" \
      "$archive" "$toSettle" "$status"
}

echo "Launcher: $launcher"
echo "Clients per run: $clients; settle: ${settle}s; client args: ${passthrough[*]:-(none)}"
echo

on_res="$(run_one appcds "")"
off_res="$(run_one no-appcds "--no-appcds")"

hdr='%-10s %4s %9s %9s %9s %10s %10s %10s  %-7s %8s  %s\n'
printf "$hdr" MODE "#Up" "PeakRSS" "Rss" "Pss" "SharedCln" "PrivClean" "PrivDirty" "ARCHIVE" "ToSettle" "STATUS"
printf "$hdr" ""   ""    "(MB)"    "(MB)" "(MB)" "(MB)"      "(MB)"      "(MB)"      ""        "(s)"     ""
for res in "$on_res" "$off_res"; do
   read -r label nUp peak rss pss shc pc prd archive toSettle status <<< "$res"
   printf "$hdr" "$label" "$nUp" "$(mb "$peak")" "$(mb "$rss")" "$(mb "$pss")" \
      "$(mb "$shc")" "$(mb "$pc")" "$(mb "$prd")" "$archive" "$toSettle" "$status"
done

echo
echo "Reading the table:"
echo " - ARCHIVE=mapped means the .jsa is really mapped in; =absent means -Xshare:auto"
echo "   silently skipped it (classpath mismatch) -- then 'appcds' isn't using CDS at all."
echo " - ToSettle is wall-time to the last RSS increase (warmup proxy). CDS should make"
echo "   the 'appcds' value smaller -- that startup speedup is its main single-client win."
echo " - Pss splits shared pages across mappers. With --clients >1 and ARCHIVE=mapped,"
echo "   compare total Pss (and SharedClean): appcds should win as K grows, because the"
echo "   one archive is shared across all K JVMs. At K=1 appcds often ties or loses on RSS."
echo " - 'no-appcds' also loads the full library set, so part of any delta is footprint."
