Add jfr-demo: JFR + Mission Control profiling of a live Spring Boot app
Custom OrderProcessed event, CPU/allocation/lock workloads, a diagnostic endpoint over the JVM's live FlightRecorder state, a JUnit test using the jdk.jfr.Recording API, and nine captured recordings (flagship, mid-flight snapshot, isolated vs concurrent threshold comparisons, and a real dump+stop duplication bug) with their docs/output/*.txt transcripts.
This commit is contained in:
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproduces the main flagship recording: start a profile-settings recording,
|
||||
# drive real load, take a mid-flight snapshot to a DIFFERENT file (never the
|
||||
# recording's own destination -- see docs/output/09-dump-duplication-bug.txt
|
||||
# for what goes wrong if you do), then stop with its own filename.
|
||||
#
|
||||
# Usage: scripts/capture-jfr.sh <pid>
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
PID="${1:?usage: capture-jfr.sh <pid>}"
|
||||
mkdir -p recordings
|
||||
|
||||
jcmd "$PID" JFR.start name=live settings=profile maxsize=256m
|
||||
|
||||
scripts/load.sh cpu
|
||||
scripts/load.sh alloc
|
||||
scripts/load.sh lock
|
||||
|
||||
jcmd "$PID" JFR.dump name=live filename="$(pwd)/recordings/mid-flight-snapshot.jfr"
|
||||
|
||||
scripts/load.sh orders
|
||||
|
||||
jcmd "$PID" JFR.stop name=live filename="$(pwd)/recordings/live-demo.jfr"
|
||||
|
||||
echo "wrote recordings/live-demo.jfr and recordings/mid-flight-snapshot.jfr"
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generates the same real load used to produce docs/output/*.txt against a
|
||||
# running instance of the app (localhost:8080 by default).
|
||||
#
|
||||
# Usage: scripts/load.sh {cpu|alloc|lock|orders|all}
|
||||
set -euo pipefail
|
||||
HOST="${HOST:-localhost:8080}"
|
||||
|
||||
cpu() {
|
||||
echo "6 concurrent fibonacci(38) calls..."
|
||||
for i in 1 2 3 4 5 6; do curl -s "$HOST/api/cpu/fibonacci?n=38" -o /dev/null & done
|
||||
wait
|
||||
}
|
||||
|
||||
alloc() {
|
||||
echo "3 concurrent allocation-churn calls..."
|
||||
for i in 1 2 3; do curl -s "$HOST/api/alloc/churn?iterations=400000" -o /dev/null & done
|
||||
wait
|
||||
}
|
||||
|
||||
lock() {
|
||||
echo "one 16-worker, 20ms-hold lock-contention burst..."
|
||||
curl -s "$HOST/api/lock/contend?workers=16&holdMillis=20"; echo
|
||||
}
|
||||
|
||||
orders() {
|
||||
echo "60 order-processing calls (custom JFR event)..."
|
||||
for i in $(seq 1 60); do curl -s "$HOST/api/orders/process?items=$((3 + i % 5))" -o /dev/null; done
|
||||
echo "done"
|
||||
}
|
||||
|
||||
case "${1:-all}" in
|
||||
cpu) cpu ;;
|
||||
alloc) alloc ;;
|
||||
lock) lock ;;
|
||||
orders) orders ;;
|
||||
all) cpu; alloc; lock; orders ;;
|
||||
*) echo "usage: $0 {cpu|alloc|lock|orders|all}" >&2; exit 1 ;;
|
||||
esac
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproduces the dump/stop duplication trap documented in
|
||||
# docs/output/09-dump-duplication-bug.txt. Do not copy this pattern.
|
||||
#
|
||||
# Usage: scripts/reproduce-dump-bug.sh <pid>
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
PID="${1:?usage: reproduce-dump-bug.sh <pid>}"
|
||||
mkdir -p recordings
|
||||
DEST="$(pwd)/recordings/dump-duplication-bug.jfr"
|
||||
|
||||
jcmd "$PID" JFR.start name=live settings=profile filename="$DEST" maxsize=256m
|
||||
scripts/load.sh all
|
||||
# BUG: dumping to the SAME path the recording is already configured to write to...
|
||||
jcmd "$PID" JFR.dump name=live filename="$DEST"
|
||||
# ...then stopping with no filename, which falls back to that same path.
|
||||
jcmd "$PID" JFR.stop name=live
|
||||
|
||||
echo "wrote $DEST -- every event in it is duplicated. See docs/output/09-dump-duplication-bug.txt"
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerates every file under docs/output/.
|
||||
#
|
||||
# 01 comes from the JUnit test suite (jdk.jfr.Recording API, in-process).
|
||||
# 02-10 come from starting the real app and driving it with jcmd + curl,
|
||||
# exactly as described in the post. 11 comes from a standalone driver class.
|
||||
#
|
||||
# This script assumes JAVA_HOME points at a JDK with jfr/jcmd on PATH, and
|
||||
# that no other jfr-demo.jar is already running on port 8080.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "== 01: in-process custom-event test =="
|
||||
mvn -q -B test
|
||||
|
||||
echo "== 11: broken-event-pattern standalone demo =="
|
||||
mvn -q -B -DskipTests package
|
||||
java -cp target/classes com.ankurm.jfr.events.BrokenEventPatternDemo \
|
||||
| tee /tmp/broken-event-output.txt
|
||||
# docs/output/11-broken-event-pattern.txt is written by hand from this run's
|
||||
# output plus commentary; regenerate it manually if the numbers change.
|
||||
|
||||
echo "== starting app for the live jcmd-driven captures =="
|
||||
scripts/run.sh -d
|
||||
PID=$(ps -eo pid,cmd | grep '[j]fr-demo.jar' | awk '{print $1}' | head -1)
|
||||
echo "app pid: $PID"
|
||||
|
||||
echo "== 02-10: live recording capture =="
|
||||
scripts/capture-jfr.sh "$PID"
|
||||
# docs/output/02 through 10 are written by hand from the jfr print/summary
|
||||
# output of recordings/live-demo.jfr and the comparison recordings below;
|
||||
# regenerate them manually if the workload or JDK version changes.
|
||||
|
||||
echo "== threshold-comparison recordings (docs/output/07, 08) =="
|
||||
jcmd "$PID" JFR.start name=soloDefault settings=default
|
||||
for i in $(seq 1 40); do curl -s "localhost:8080/api/lock/contend?workers=2&holdMillis=12" -o /dev/null; done
|
||||
jcmd "$PID" JFR.stop name=soloDefault filename="$(pwd)/recordings/solo-default.jfr"
|
||||
|
||||
jcmd "$PID" JFR.start name=soloProfile settings=profile
|
||||
for i in $(seq 1 40); do curl -s "localhost:8080/api/lock/contend?workers=2&holdMillis=12" -o /dev/null; done
|
||||
jcmd "$PID" JFR.stop name=soloProfile filename="$(pwd)/recordings/solo-profile.jfr"
|
||||
|
||||
jcmd "$PID" JFR.start name=soloDefault2 settings=default
|
||||
for i in $(seq 1 10); do curl -s "localhost:8080/api/lock/contend?workers=2&holdMillis=35" -o /dev/null; done
|
||||
jcmd "$PID" JFR.stop name=soloDefault2 filename="$(pwd)/recordings/solo-default-longhold.jfr"
|
||||
|
||||
jcmd "$PID" JFR.start name=cmpA settings=default
|
||||
jcmd "$PID" JFR.start name=cmpB settings=profile
|
||||
for i in $(seq 1 40); do curl -s "localhost:8080/api/lock/contend?workers=2&holdMillis=12" -o /dev/null; done
|
||||
jcmd "$PID" JFR.stop name=cmpA filename="$(pwd)/recordings/compare-default.jfr"
|
||||
jcmd "$PID" JFR.stop name=cmpB filename="$(pwd)/recordings/compare-profile.jfr"
|
||||
|
||||
for p in $(ps -eo pid,cmd | grep '[j]fr-demo.jar' | awk '{print $1}'); do kill -9 "$p"; done
|
||||
echo "all recordings regenerated under recordings/"
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build and start the demo app in the foreground (or detached with -d).
|
||||
# Usage: scripts/run.sh [-d]
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
for p in $(ps -eo pid,cmd | grep '[j]fr-demo.jar' | awk '{print $1}'); do
|
||||
echo "killing previous instance: $p"
|
||||
kill -9 "$p"
|
||||
done
|
||||
|
||||
mvn -q -B -DskipTests package
|
||||
|
||||
if [[ "${1:-}" == "-d" ]]; then
|
||||
setsid nohup java -jar target/jfr-demo.jar > /tmp/jfr-demo.log 2>&1 < /dev/null &
|
||||
echo "started detached, pid $!"
|
||||
for i in $(seq 1 30); do
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' localhost:8080/api/jfr/recordings || true)
|
||||
[[ "$code" == "200" ]] && { echo "up after ${i}s"; exit 0; }
|
||||
sleep 1
|
||||
done
|
||||
echo "app did not come up in time, see /tmp/jfr-demo.log" >&2
|
||||
exit 1
|
||||
else
|
||||
exec java -jar target/jfr-demo.jar
|
||||
fi
|
||||
Reference in New Issue
Block a user