Skip to content

Commit 495253a

Browse files
committed
Capture direct-write Spark profiles
1 parent 74d17f4 commit 495253a

3 files changed

Lines changed: 228 additions & 5 deletions

File tree

.github/workflows/phase2-packet-capture.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,8 @@ jobs:
174174
java -version 2>&1
175175
printf '%s\n' \
176176
'java=-Xms2G -Xmx2G -XX:+UseG1GC -XX:+AlwaysPreTouch -Dinteractionvisualizer.performance.allowBlockScene=true -Xlog:gc*=info,safepoint=info:file=jvm-gc-safepoint.log:time,uptime,level,tags:filecount=0 -Dfile.encoding=UTF-8' \
177-
"scenario=$SCENARIO" "snaplen=$SNAPLEN" 'protocolTrace=semantic-lifecycle-v1'
177+
"scenario=$SCENARIO" "snaplen=$SNAPLEN" 'protocolTrace=semantic-lifecycle-v1' \
178+
'sparkProfile=none'
178179
} | sha256sum | awk '{print $1}'
179180
)
180181
@@ -199,6 +200,7 @@ jobs:
199200
PHASE2_CAPTURE_ENABLED=1 \
200201
PHASE2_CAPTURE_SNAPLEN="$SNAPLEN" \
201202
PHASE2_PROTOCOL_TRACE_ENABLED=1 \
203+
PHASE2_SPARK_PROFILE_MODE=none \
202204
bash tools/perf/run-phase2-runtime-once.sh
203205
204206
printf '%s,%d,%d,%s,%s,%s,%s,tcpdump-lo-s%s,%s/%s.pcap-analysis.json\n' \

.github/workflows/phase2-runtime-ab.yml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ on:
3737
required: true
3838
default: "180"
3939
type: string
40+
spark_profile_mode:
41+
description: "Optional 4-run Spark diagnostic for block-direct-write"
42+
required: true
43+
default: "none"
44+
type: choice
45+
options: ["none", "cpu", "alloc"]
4046

4147
permissions:
4248
contents: read
@@ -57,6 +63,7 @@ jobs:
5763
CAMPAIGN_WARMUP_SECONDS: ${{ github.event_name == 'workflow_dispatch' && inputs.warmup_seconds || github.event.action == 'labeled' && '120' || '10' }}
5864
CAMPAIGN_SETTLE_SECONDS: ${{ github.event_name == 'workflow_dispatch' && inputs.settle_seconds || github.event.action == 'labeled' && '20' || '5' }}
5965
CAMPAIGN_MEASURE_SECONDS: ${{ github.event_name == 'workflow_dispatch' && inputs.measure_seconds || github.event.action == 'labeled' && '180' || '10' }}
66+
CAMPAIGN_SPARK_PROFILE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.spark_profile_mode || 'none' }}
6067

6168
steps:
6269
- name: Check out immutable test source
@@ -114,6 +121,7 @@ jobs:
114121
WARMUP_SECONDS: ${{ env.CAMPAIGN_WARMUP_SECONDS }}
115122
SETTLE_SECONDS: ${{ env.CAMPAIGN_SETTLE_SECONDS }}
116123
MEASURE_SECONDS: ${{ env.CAMPAIGN_MEASURE_SECONDS }}
124+
SPARK_PROFILE_MODE: ${{ env.CAMPAIGN_SPARK_PROFILE_MODE }}
117125
run: |
118126
set -euo pipefail
119127
[[ "$SCENARIO" == static-steady || "$SCENARIO" == block-idle || \
@@ -131,6 +139,15 @@ jobs:
131139
[[ "$WARMUP_SECONDS" =~ ^[0-9]+$ ]] && (( WARMUP_SECONDS >= 10 ))
132140
[[ "$SETTLE_SECONDS" =~ ^[0-9]+$ ]] && (( SETTLE_SECONDS >= 5 ))
133141
[[ "$MEASURE_SECONDS" =~ ^[0-9]+$ ]] && (( MEASURE_SECONDS >= 10 ))
142+
[[ "$SPARK_PROFILE_MODE" =~ ^(none|cpu|alloc)$ ]]
143+
if [[ "$SPARK_PROFILE_MODE" != none && "$SCENARIO" != block-direct-write ]]; then
144+
echo "Spark profiling is currently isolated to block-direct-write" >&2
145+
exit 64
146+
fi
147+
if [[ "$SPARK_PROFILE_MODE" != none && "$RUNS" != 4 ]]; then
148+
echo "Spark profiling is diagnostic-only and requires runs=4" >&2
149+
exit 64
150+
fi
134151
if [[ "$SCENARIO" == block-direct-write ]]; then
135152
# Direct BlockState writes intentionally emit no Bukkit event.
136153
# Allow the 600-tick audit to begin plus enough ticks for every
@@ -152,7 +169,7 @@ jobs:
152169
java -version 2>&1
153170
printf '%s\n' \
154171
'java=-Xms2G -Xmx2G -XX:+UseG1GC -XX:+AlwaysPreTouch -Dinteractionvisualizer.performance.allowBlockScene=true -Xlog:gc*=info,safepoint=info:file=jvm-gc-safepoint.log:time,uptime,level,tags:filecount=0 -Dfile.encoding=UTF-8' \
155-
"scenario=$SCENARIO"
172+
"scenario=$SCENARIO" "sparkProfile=$SPARK_PROFILE_MODE"
156173
} | sha256sum | awk '{print $1}'
157174
)
158175
@@ -176,6 +193,7 @@ jobs:
176193
PHASE2_MEASURE_SECONDS="$MEASURE_SECONDS" \
177194
PHASE2_CAPTURE_ENABLED=0 \
178195
PHASE2_PROTOCOL_TRACE_ENABLED=0 \
196+
PHASE2_SPARK_PROFILE_MODE="$SPARK_PROFILE_MODE" \
179197
bash tools/perf/run-phase2-runtime-once.sh
180198
181199
printf '%s,%d,%d,%s,%s,%s,%s,none,%s/iv-perf.json\n' \

tools/perf/run-phase2-runtime-once.sh

Lines changed: 206 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ measure_seconds="${PHASE2_MEASURE_SECONDS:-180}"
3838
capture_enabled="${PHASE2_CAPTURE_ENABLED:-0}"
3939
capture_snaplen="${PHASE2_CAPTURE_SNAPLEN:-128}"
4040
protocol_trace_enabled="${PHASE2_PROTOCOL_TRACE_ENABLED:-$capture_enabled}"
41+
spark_profile_mode="${PHASE2_SPARK_PROFILE_MODE:-none}"
4142

4243
[[ "$run_id" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] \
4344
|| { echo "PHASE2_RUN_ID contains unsafe characters" >&2; exit 64; }
@@ -84,6 +85,14 @@ fi
8485
|| { echo "PHASE2_CAPTURE_ENABLED must be 0 or 1" >&2; exit 64; }
8586
[[ "$protocol_trace_enabled" == 0 || "$protocol_trace_enabled" == 1 ]] \
8687
|| { echo "PHASE2_PROTOCOL_TRACE_ENABLED must be 0 or 1" >&2; exit 64; }
88+
case "$spark_profile_mode" in
89+
none|cpu|alloc) ;;
90+
*) echo "PHASE2_SPARK_PROFILE_MODE must be none, cpu, or alloc" >&2; exit 64 ;;
91+
esac
92+
if [[ "$spark_profile_mode" != none && "$scenario" != block-direct-write ]]; then
93+
echo "Spark profiling is currently isolated to block-direct-write" >&2
94+
exit 64
95+
fi
8796
[[ -f "$plugin_jar" && -f "$paper_jar" ]] \
8897
|| { echo "Plugin or Paper JAR is missing" >&2; exit 66; }
8998
[[ -f "$client_root/client-build-manifest.json" ]] \
@@ -115,6 +124,8 @@ protocol_trace_analysis_path="$run_directory/$run_id.protocol-trace-analysis.jso
115124
jvm_gc_safepoint_log_name="jvm-gc-safepoint.log"
116125
jvm_gc_safepoint_log="$run_directory/$jvm_gc_safepoint_log_name"
117126
jvm_diagnostics_metadata="$run_directory/jvm-diagnostics.json"
127+
spark_profile_output="$run_directory/$run_id.spark-$spark_profile_mode.sparkprofile"
128+
spark_profile_metadata="$run_directory/spark-profile.json"
118129
jvm_gc_safepoint_xlog="-Xlog:gc*=info,safepoint=info:file=$jvm_gc_safepoint_log_name:time,uptime,level,tags:filecount=0"
119130
jvm_arguments_fingerprint="-Xms2G -Xmx2G -XX:+UseG1GC -XX:+AlwaysPreTouch -Dinteractionvisualizer.performance.allowBlockScene=true $jvm_gc_safepoint_xlog -Dfile.encoding=UTF-8"
120131

@@ -614,6 +625,37 @@ if [[ "$capture_enabled" == 1 ]]; then
614625
kill -0 "$capture_pid"
615626
fi
616627

628+
spark_profile_start_command=""
629+
spark_profile_interval=""
630+
spark_profile_interval_unit=""
631+
spark_profile_only_ticks_over_ms=""
632+
if [[ "$spark_profile_mode" != none ]]; then
633+
spark_start_pattern="Profiler is now running!"
634+
case "$spark_profile_mode" in
635+
cpu)
636+
spark_profile_start_command="spark profiler start --interval 1 --only-ticks-over 40"
637+
spark_profile_interval="1"
638+
spark_profile_interval_unit="milliseconds"
639+
spark_profile_only_ticks_over_ms="40"
640+
;;
641+
alloc)
642+
spark_profile_start_command="spark profiler start --alloc --interval 32768"
643+
spark_profile_interval="32768"
644+
spark_profile_interval_unit="bytes"
645+
spark_profile_only_ticks_over_ms=""
646+
spark_start_pattern="Allocation Profiler is now running!"
647+
;;
648+
esac
649+
spark_start_count="$(grep -Fc -- "$spark_start_pattern" "$server_log" 2>/dev/null || true)"
650+
send_console "$spark_profile_start_command"
651+
wait_for_log_count "$spark_start_pattern" "$((spark_start_count + 1))" 60
652+
spark_start_record="$(grep -F -- "$spark_start_pattern" "$server_log" | tail -n 1)"
653+
if [[ "$spark_start_record" != *"(async)"* ]]; then
654+
echo "Spark did not start the async-profiler engine: $spark_start_record" >&2
655+
exit 1
656+
fi
657+
fi
658+
617659
send_console "iv perf start $run_id"
618660
wait_for_log "Performance sampling started: $run_id" 30
619661
window_start="$(python3 -c 'import time; print(f"{time.time():.6f}")')"
@@ -649,6 +691,78 @@ window_end="$(python3 -c 'import time; print(f"{time.time():.6f}")')"
649691
send_console "iv perf stop"
650692
wait_for_log "IV_PERF {\"label\":\"$run_id\"" 60
651693

694+
if [[ "$spark_profile_mode" != none ]]; then
695+
spark_stop_pattern="Profiler stopped & save complete!"
696+
spark_stop_count="$(grep -Fc -- "$spark_stop_pattern" "$server_log" 2>/dev/null || true)"
697+
spark_profile_stop_command="spark profiler stop --save-to-file --comment phase2-$run_id-$spark_profile_mode"
698+
send_console "$spark_profile_stop_command"
699+
wait_for_log_count "$spark_stop_pattern" "$((spark_stop_count + 1))" 120
700+
mapfile -t spark_profile_candidates < <(
701+
find "$run_directory/plugins" -type f -name '*.sparkprofile' -print
702+
)
703+
if [[ "${#spark_profile_candidates[@]}" != 1 ]]; then
704+
echo "Expected exactly one saved Spark profile, found ${#spark_profile_candidates[@]}" >&2
705+
printf '%s\n' "${spark_profile_candidates[@]}" >&2
706+
exit 1
707+
fi
708+
mv -- "${spark_profile_candidates[0]}" "$spark_profile_output"
709+
test -s "$spark_profile_output"
710+
spark_profile_sha="$(sha256sum "$spark_profile_output" | awk '{print $1}')"
711+
spark_profile_size="$(stat -c '%s' "$spark_profile_output")"
712+
python3 - \
713+
"$spark_profile_metadata" \
714+
"$spark_profile_mode" \
715+
"$(basename "$spark_profile_output")" \
716+
"$spark_profile_sha" \
717+
"$spark_profile_size" \
718+
"$spark_profile_start_command" \
719+
"$spark_profile_stop_command" \
720+
"$spark_profile_interval" \
721+
"$spark_profile_interval_unit" \
722+
"$spark_profile_only_ticks_over_ms" <<'PY'
723+
from pathlib import Path
724+
import json
725+
import sys
726+
727+
(
728+
output_path,
729+
mode,
730+
profile_name,
731+
profile_sha,
732+
profile_size,
733+
start_command,
734+
stop_command,
735+
interval,
736+
interval_unit,
737+
only_ticks_over_ms,
738+
) = sys.argv[1:]
739+
740+
metadata = {
741+
"schemaVersion": 1,
742+
"profileEvidenceReady": True,
743+
"performanceEvidenceReady": False,
744+
"mode": mode,
745+
"engine": "async-profiler",
746+
"startCommand": start_command,
747+
"stopCommand": stop_command,
748+
"sampling": {
749+
"interval": float(interval),
750+
"intervalUnit": interval_unit,
751+
"onlyTicksOverMs": int(only_ticks_over_ms) if only_ticks_over_ms else None,
752+
},
753+
"profile": {
754+
"path": profile_name,
755+
"sha256": profile_sha,
756+
"sizeBytes": int(profile_size),
757+
},
758+
}
759+
Path(output_path).write_text(
760+
json.dumps(metadata, ensure_ascii=False, indent=2) + "\n",
761+
encoding="utf-8",
762+
)
763+
PY
764+
fi
765+
652766
if [[ "$capture_enabled" == 1 ]]; then
653767
stop_capture
654768
test -s "$capture_path"
@@ -1163,10 +1277,61 @@ metadata["metadataSha256"] = metadata_sha
11631277
print(json.dumps(metadata, ensure_ascii=False, separators=(",", ":")))
11641278
PY
11651279
)"
1280+
spark_profile_manifest_json="$(python3 - \
1281+
"$spark_profile_mode" \
1282+
"$spark_profile_metadata" \
1283+
"$spark_profile_output" <<'PY'
1284+
from pathlib import Path
1285+
import hashlib
1286+
import json
1287+
import sys
1288+
1289+
mode = sys.argv[1]
1290+
metadata_path = Path(sys.argv[2])
1291+
profile_path = Path(sys.argv[3])
1292+
if mode == "none":
1293+
if metadata_path.exists() or profile_path.exists():
1294+
raise SystemExit("disabled Spark profiling unexpectedly produced evidence")
1295+
print(json.dumps({
1296+
"enabled": False,
1297+
"mode": "none",
1298+
"profileEvidenceReady": None,
1299+
"performanceEvidenceReady": True,
1300+
"metadataPath": None,
1301+
"metadataSha256": None,
1302+
}, separators=(",", ":")))
1303+
raise SystemExit(0)
1304+
1305+
if not metadata_path.is_file() or not profile_path.is_file():
1306+
raise SystemExit("enabled Spark profiling did not produce complete evidence")
1307+
with metadata_path.open(encoding="utf-8") as stream:
1308+
metadata = json.load(stream)
1309+
raw_profile = profile_path.read_bytes()
1310+
if not raw_profile:
1311+
raise SystemExit("saved Spark profile is empty")
1312+
profile = metadata.get("profile", {})
1313+
if metadata.get("profileEvidenceReady") is not True:
1314+
raise SystemExit("Spark profile metadata is not profile-evidence ready")
1315+
if metadata.get("performanceEvidenceReady") is not False:
1316+
raise SystemExit("instrumented Spark evidence must not claim clean performance readiness")
1317+
if metadata.get("mode") != mode or metadata.get("engine") != "async-profiler":
1318+
raise SystemExit("Spark profile metadata mode/engine mismatch")
1319+
if profile.get("path") != profile_path.name:
1320+
raise SystemExit("Spark profile metadata path mismatch")
1321+
if profile.get("sha256") != hashlib.sha256(raw_profile).hexdigest():
1322+
raise SystemExit("Spark profile metadata SHA mismatch")
1323+
if profile.get("sizeBytes") != len(raw_profile):
1324+
raise SystemExit("Spark profile metadata size mismatch")
1325+
metadata["enabled"] = True
1326+
metadata["metadataPath"] = metadata_path.name
1327+
metadata["metadataSha256"] = hashlib.sha256(metadata_path.read_bytes()).hexdigest()
1328+
print(json.dumps(metadata, ensure_ascii=False, separators=(",", ":")))
1329+
PY
1330+
)"
11661331

11671332
cat > "$run_directory/run-manifest.json" <<EOF
11681333
{
1169-
"schemaVersion": 3,
1334+
"schemaVersion": 4,
11701335
"runId": "$run_id",
11711336
"scenario": "$scenario",
11721337
"variant": "$variant",
@@ -1201,15 +1366,19 @@ cat > "$run_directory/run-manifest.json" <<EOF
12011366
"recordsPath": $block_scene_manifest_records_path,
12021367
"recordsSha256": $block_scene_manifest_records_sha
12031368
},
1204-
"jvmDiagnostics": $jvm_diagnostics_manifest_json
1369+
"jvmDiagnostics": $jvm_diagnostics_manifest_json,
1370+
"sparkProfile": $spark_profile_manifest_json
12051371
}
12061372
EOF
12071373

12081374
python3 - \
12091375
"$run_directory/run-manifest.json" \
12101376
"$jvm_diagnostics_metadata" \
12111377
"$jvm_diagnostics_metadata_sha" \
1212-
"$jvm_gc_safepoint_sha" <<'PY'
1378+
"$jvm_gc_safepoint_sha" \
1379+
"$spark_profile_mode" \
1380+
"$spark_profile_metadata" \
1381+
"$spark_profile_output" <<'PY'
12131382
from pathlib import Path
12141383
import hashlib
12151384
import json
@@ -1219,6 +1388,9 @@ manifest_path = Path(sys.argv[1])
12191388
metadata_path = Path(sys.argv[2])
12201389
expected_metadata_sha = sys.argv[3]
12211390
expected_log_sha = sys.argv[4]
1391+
spark_mode = sys.argv[5]
1392+
spark_metadata_path = Path(sys.argv[6])
1393+
spark_profile_path = Path(sys.argv[7])
12221394
with manifest_path.open(encoding="utf-8") as stream:
12231395
manifest = json.load(stream)
12241396
with metadata_path.open(encoding="utf-8") as stream:
@@ -1241,6 +1413,37 @@ if embedded_metadata != metadata:
12411413
raise SystemExit("run manifest JVM diagnostics do not match jvm-diagnostics.json")
12421414
if embedded.get("gcSafepointLog", {}).get("sha256") != expected_log_sha:
12431415
raise SystemExit("run manifest has the wrong finalized JVM diagnostic log SHA")
1416+
1417+
embedded_spark = manifest.get("sparkProfile")
1418+
if not isinstance(embedded_spark, dict):
1419+
raise SystemExit("run manifest has no Spark profile state")
1420+
if spark_mode == "none":
1421+
if (embedded_spark.get("enabled") is not False
1422+
or embedded_spark.get("mode") != "none"
1423+
or embedded_spark.get("performanceEvidenceReady") is not True):
1424+
raise SystemExit("run manifest incorrectly enables Spark profiling")
1425+
else:
1426+
if embedded_spark.get("enabled") is not True or embedded_spark.get("mode") != spark_mode:
1427+
raise SystemExit("run manifest Spark profile mode mismatch")
1428+
if (embedded_spark.get("profileEvidenceReady") is not True
1429+
or embedded_spark.get("performanceEvidenceReady") is not False):
1430+
raise SystemExit("run manifest Spark evidence-readiness mismatch")
1431+
with spark_metadata_path.open(encoding="utf-8") as stream:
1432+
spark_metadata = json.load(stream)
1433+
spark_metadata_sha = hashlib.sha256(spark_metadata_path.read_bytes()).hexdigest()
1434+
if embedded_spark.get("metadataPath") != spark_metadata_path.name:
1435+
raise SystemExit("run manifest Spark metadata path mismatch")
1436+
if embedded_spark.get("metadataSha256") != spark_metadata_sha:
1437+
raise SystemExit("run manifest Spark metadata SHA mismatch")
1438+
embedded_spark_metadata = dict(embedded_spark)
1439+
embedded_spark_metadata.pop("enabled")
1440+
embedded_spark_metadata.pop("metadataPath")
1441+
embedded_spark_metadata.pop("metadataSha256")
1442+
if embedded_spark_metadata != spark_metadata:
1443+
raise SystemExit("run manifest Spark metadata does not match spark-profile.json")
1444+
raw_spark_profile = spark_profile_path.read_bytes()
1445+
if embedded_spark.get("profile", {}).get("sha256") != hashlib.sha256(raw_spark_profile).hexdigest():
1446+
raise SystemExit("run manifest Spark profile SHA mismatch")
12441447
PY
12451448

12461449
echo "Completed Phase 2 runtime sample $run_id"

0 commit comments

Comments
 (0)