@@ -38,6 +38,7 @@ measure_seconds="${PHASE2_MEASURE_SECONDS:-180}"
3838capture_enabled=" ${PHASE2_CAPTURE_ENABLED:- 0} "
3939capture_snaplen=" ${PHASE2_CAPTURE_SNAPLEN:- 128} "
4040protocol_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; }
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
115124jvm_gc_safepoint_log_name=" jvm-gc-safepoint.log"
116125jvm_gc_safepoint_log=" $run_directory /$jvm_gc_safepoint_log_name "
117126jvm_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"
118129jvm_gc_safepoint_xlog=" -Xlog:gc*=info,safepoint=info:file=$jvm_gc_safepoint_log_name :time,uptime,level,tags:filecount=0"
119130jvm_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 "
615626fi
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+
617659send_console " iv perf start $run_id "
618660wait_for_log " Performance sampling started: $run_id " 30
619661window_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}")')"
649691send_console " iv perf stop"
650692wait_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+
652766if [[ " $capture_enabled " == 1 ]]; then
653767 stop_capture
654768 test -s " $capture_path "
@@ -1163,10 +1277,61 @@ metadata["metadataSha256"] = metadata_sha
11631277print(json.dumps(metadata, ensure_ascii=False, separators=(",", ":")))
11641278PY
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
11671332cat > " $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}
12061372EOF
12071373
12081374python3 - \
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 '
12131382from pathlib import Path
12141383import hashlib
12151384import json
@@ -1219,6 +1388,9 @@ manifest_path = Path(sys.argv[1])
12191388metadata_path = Path(sys.argv[2])
12201389expected_metadata_sha = sys.argv[3]
12211390expected_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])
12221394with manifest_path.open(encoding="utf-8") as stream:
12231395 manifest = json.load(stream)
12241396with 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")
12421414if 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")
12441447PY
12451448
12461449echo " Completed Phase 2 runtime sample $run_id "
0 commit comments