1010 required : true
1111 default : " static-spawn"
1212 type : choice
13- options : ["static-spawn", "visibility-return"]
13+ options : ["static-spawn", "visibility-return", "visibility-itemdisplay-return", "visibility-textdisplay-return" ]
1414 runs :
1515 description : " Restart-isolated runs (4, 8, or formal 12)"
1616 required : true
@@ -53,17 +53,22 @@ jobs:
5353 if : >-
5454 github.event_name == 'workflow_dispatch' ||
5555 github.event.action != 'labeled' ||
56+ github.event.label.name == 'phase2-packet-visibility-smoke' ||
5657 github.event.label.name == 'phase2-packet-static-formal' ||
57- github.event.label.name == 'phase2-packet-visibility-formal'
58+ github.event.label.name == 'phase2-packet-visibility-formal' ||
59+ github.event.label.name == 'phase2-packet-visibility-itemdisplay-smoke' ||
60+ github.event.label.name == 'phase2-packet-visibility-itemdisplay-formal' ||
61+ github.event.label.name == 'phase2-packet-visibility-textdisplay-smoke' ||
62+ github.event.label.name == 'phase2-packet-visibility-textdisplay-formal'
5863 runs-on : ubuntu-latest
5964 timeout-minutes : 100
6065 env :
61- CAMPAIGN_SCENARIO : ${{ github.event_name == 'workflow_dispatch' && inputs.scenario || github.event.label.name == 'phase2-packet- visibility-formal' && 'visibility-return' || 'static-spawn' }}
62- CAMPAIGN_RUNS : ${{ github.event_name == 'workflow_dispatch' && inputs.runs || github.event.action == 'labeled' && '12' || '4' }}
63- CAMPAIGN_ITEMS : ${{ github.event_name == 'workflow_dispatch' && inputs.items || github.event.action == 'labeled' && '4096' || '1024' }}
64- CAMPAIGN_WARMUP_SECONDS : ${{ github.event_name == 'workflow_dispatch' && inputs.warmup_seconds || github.event.action == 'labeled' && '60' || '10' }}
65- CAMPAIGN_SETTLE_SECONDS : ${{ github.event_name == 'workflow_dispatch' && inputs.settle_seconds || github.event.action == 'labeled' && '10' || '5' }}
66- CAMPAIGN_MEASURE_SECONDS : ${{ github.event_name == 'workflow_dispatch' && inputs.measure_seconds || github.event.action == 'labeled' && '10' || '5' }}
66+ CAMPAIGN_SCENARIO : ${{ github.event_name == 'workflow_dispatch' && inputs.scenario || contains( github.event.label.name, 'visibility-itemdisplay') && 'visibility-itemdisplay-return' || contains(github.event.label.name, ' visibility-textdisplay') && 'visibility-textdisplay-return' || contains(github.event.label.name, 'visibility') && 'visibility-return' || 'static-spawn' }}
67+ CAMPAIGN_RUNS : ${{ github.event_name == 'workflow_dispatch' && inputs.runs || contains( github.event.label.name, '-formal') && '12' || '4' }}
68+ CAMPAIGN_ITEMS : ${{ github.event_name == 'workflow_dispatch' && inputs.items || contains( github.event.label.name, '-formal') && '4096' || '1024' }}
69+ CAMPAIGN_WARMUP_SECONDS : ${{ github.event_name == 'workflow_dispatch' && inputs.warmup_seconds || contains( github.event.label.name, '-formal') && '60' || '10' }}
70+ CAMPAIGN_SETTLE_SECONDS : ${{ github.event_name == 'workflow_dispatch' && inputs.settle_seconds || contains( github.event.label.name, '-formal') && '10' || '5' }}
71+ CAMPAIGN_MEASURE_SECONDS : ${{ github.event_name == 'workflow_dispatch' && inputs.measure_seconds || contains( github.event.label.name, '-formal') && '10' || contains(github.event.label.name, 'visibility') && '10' || '5' }}
6772 CAMPAIGN_SNAPLEN : ${{ github.event_name == 'workflow_dispatch' && inputs.snaplen || '128' }}
6873
6974 steps :
@@ -100,6 +105,8 @@ jobs:
100105 bash -n tools/perf/prepare-phase2-protocol-client.sh
101106 bash -n tools/perf/run-phase2-runtime-once.sh
102107 node --check tools/perf/phase2-protocol-client.js
108+ node --check tools/perf/analyze-phase2-protocol-trace.js
109+ node tools/perf/analyze-phase2-protocol-trace.js --self-test
103110 pwsh -NoProfile -File tools/perf/analyze-phase2-abba.ps1 -SelfTest
104111 pwsh -NoProfile -File tools/perf/analyze-phase2-pcap.ps1 -SelfTest
105112
@@ -124,6 +131,7 @@ jobs:
124131 run : bash tools/perf/prepare-phase2-protocol-client.sh phase2-dependencies/protocol-client
125132
126133 - name : Run restart-isolated packet ABBA campaign
134+ id : packet_campaign
127135 env :
128136 SCENARIO : ${{ env.CAMPAIGN_SCENARIO }}
129137 RUNS : ${{ env.CAMPAIGN_RUNS }}
@@ -134,29 +142,39 @@ jobs:
134142 SNAPLEN : ${{ env.CAMPAIGN_SNAPLEN }}
135143 run : |
136144 set -euo pipefail
137- [[ "$SCENARIO" == static-spawn || "$SCENARIO" == visibility-return ]]
145+ [[ "$SCENARIO" == static-spawn || "$SCENARIO" == visibility-return || \
146+ "$SCENARIO" == visibility-itemdisplay-return || "$SCENARIO" == visibility-textdisplay-return ]]
138147 [[ "$RUNS" =~ ^(4|8|12)$ ]]
139148 [[ "$ITEMS" =~ ^[0-9]+$ ]] && (( ITEMS >= 1 && ITEMS <= 8192 ))
140149 [[ "$WARMUP_SECONDS" =~ ^[0-9]+$ ]] && (( WARMUP_SECONDS >= 10 ))
141150 [[ "$SETTLE_SECONDS" =~ ^[0-9]+$ ]] && (( SETTLE_SECONDS >= 5 ))
142151 [[ "$MEASURE_SECONDS" =~ ^[0-9]+$ ]] && (( MEASURE_SECONDS >= 5 ))
143152 [[ "$SNAPLEN" == 0 || "$SNAPLEN" == 128 ]]
144- if [[ "$SCENARIO" == visibility-return ]]; then
153+ if [[ "$SCENARIO" == visibility-* ]]; then
145154 (( MEASURE_SECONDS >= 10 ))
146155 fi
147156
148157 PLUGIN_JAR=$(find build/libs -maxdepth 1 -type f -name 'InteractionVisualizer-*.jar' ! -name '*-sources.jar' ! -name '*-benchmark.jar' -print -quit)
149158 test -n "$PLUGIN_JAR"
150159 mkdir -p phase2-results/packet
151160 MANIFEST=phase2-results/packet/abba-manifest.csv
161+ PROTOCOL_MANIFEST=phase2-results/packet/protocol-trace-abba-manifest.csv
152162 printf 'Scenario,Block,Position,Variant,RunId,StackSha256,ArtifactSha256,CaptureMethod,SourcePath\n' > "$MANIFEST"
163+ printf 'Scenario,Block,Position,Variant,RunId,StackSha256,ArtifactSha256,CaptureMethod,SourcePath\n' > "$PROTOCOL_MANIFEST"
153164 ARTIFACT_SHA=$(sha256sum "$PLUGIN_JAR" | awk '{print $1}')
154165 STACK_SHA=$(
155166 {
156167 sha256sum phase2-dependencies/paper.jar
157168 sha256sum phase2-dependencies/protocol-client/client-build-manifest.json
169+ sha256sum tools/perf/phase2-protocol-client.js
170+ sha256sum tools/perf/analyze-phase2-protocol-trace.js
171+ sha256sum tools/perf/analyze-phase2-pcap.ps1
172+ sha256sum tools/perf/analyze-phase2-abba.ps1
173+ sha256sum tools/perf/run-phase2-runtime-once.sh
158174 java -version 2>&1
159- printf '%s\n' 'java=-Xms2G -Xmx2G -XX:+UseG1GC -XX:+AlwaysPreTouch' "scenario=$SCENARIO" "snaplen=$SNAPLEN"
175+ printf '%s\n' \
176+ 'java=-Xms2G -Xmx2G -XX:+UseG1GC -XX:+AlwaysPreTouch -Dinteractionvisualizer.performance.allowBlockScene=true' \
177+ "scenario=$SCENARIO" "snaplen=$SNAPLEN" 'protocolTrace=semantic-lifecycle-v1'
160178 } | sha256sum | awk '{print $1}'
161179 )
162180
@@ -180,11 +198,15 @@ jobs:
180198 PHASE2_MEASURE_SECONDS="$MEASURE_SECONDS" \
181199 PHASE2_CAPTURE_ENABLED=1 \
182200 PHASE2_CAPTURE_SNAPLEN="$SNAPLEN" \
201+ PHASE2_PROTOCOL_TRACE_ENABLED=1 \
183202 bash tools/perf/run-phase2-runtime-once.sh
184203
185204 printf '%s,%d,%d,%s,%s,%s,%s,tcpdump-lo-s%s,%s/%s.pcap-analysis.json\n' \
186205 "$SCENARIO" "$block" "$position" "$variant" "$run_id" "$STACK_SHA" "$ARTIFACT_SHA" \
187206 "$SNAPLEN" "$run_id" "$run_id" >> "$MANIFEST"
207+ printf '%s,%d,%d,%s,%s,%s,%s,protocol-client-memory,%s/%s.protocol-trace-analysis.json\n' \
208+ "$SCENARIO" "$block" "$position" "$variant" "$run_id" "$STACK_SHA" "$ARTIFACT_SHA" \
209+ "$run_id" "$run_id" >> "$PROTOCOL_MANIFEST"
188210 done
189211
190212 minimum_seconds=$(( MEASURE_SECONDS - 1 ))
@@ -201,11 +223,132 @@ jobs:
201223 -OutputJson "phase2-results/packet/$safe_metric.analysis.json" -Overwrite
202224 done
203225
226+ # These semantic lifecycle counts are generated from the same
227+ # explicit half-open window as the pcap evidence. They supplement,
228+ # and never replace, the transport-level byte and retransmit gates.
229+ for metric in traceCoverage.windowEventCount identity.spawn.observations spawnPeaks.epochAligned50ms.spawnedEntityIdCount spawnPeaks.epochAligned1s.spawnedEntityIdCount; do
230+ safe_metric=${metric//./_}
231+ pwsh -NoProfile -File tools/perf/analyze-phase2-abba.ps1 "$PROTOCOL_MANIFEST" \
232+ -Scenario "$SCENARIO" -Metric "$metric" -Direction LowerIsBetter \
233+ "${incomplete[@]}" \
234+ -OutputJson "phase2-results/packet/protocol_$safe_metric.analysis.json" -Overwrite
235+ done
236+
237+ - name : Summarize semantic protocol evidence
238+ if : always()
239+ env :
240+ SCENARIO : ${{ env.CAMPAIGN_SCENARIO }}
241+ EXPECTED_RUNS : ${{ env.CAMPAIGN_RUNS }}
242+ CAMPAIGN_OUTCOME : ${{ steps.packet_campaign.outcome }}
243+ run : |
244+ python3 - <<'PY'
245+ import json
246+ import os
247+ import re
248+ from pathlib import Path
249+
250+ results_root = Path("phase2-results/packet")
251+ results_root.mkdir(parents=True, exist_ok=True)
252+ suffix = ".protocol-trace-analysis.json"
253+ records = []
254+ errors = []
255+ for analysis_path in sorted(results_root.glob(f"*/*{suffix}")):
256+ run_id = analysis_path.name[:-len(suffix)]
257+ variant_match = re.search(r"_([AB])_[0-9]+$", run_id)
258+ try:
259+ analysis = json.loads(analysis_path.read_text(encoding="utf-8"))
260+ status = analysis.get("status", {})
261+ identity = analysis.get("identity", {})
262+ spawn = identity.get("spawn", {})
263+ destroy = identity.get("destroy", {})
264+ peaks = analysis.get("spawnPeaks", {})
265+ records.append({
266+ "runId": run_id,
267+ "variant": variant_match.group(1) if variant_match else None,
268+ "sourcePath": analysis_path.as_posix(),
269+ "formalEvidenceReady": status.get("formalEvidenceReady") is True,
270+ "windowCovered": status.get("windowCovered") is True,
271+ "windowDurationMs": analysis.get("window", {}).get("durationMs"),
272+ "windowEventCount": analysis.get("traceCoverage", {}).get("windowEventCount"),
273+ "spawnObservations": spawn.get("observations"),
274+ "uniqueSpawnIds": spawn.get("uniqueIdCount"),
275+ "destroyObservations": destroy.get("observations"),
276+ "peak50msSpawnIds": peaks.get("epochAligned50ms", {}).get("spawnedEntityIdCount"),
277+ "peak1sSpawnIds": peaks.get("epochAligned1s", {}).get("spawnedEntityIdCount"),
278+ "duplicateLiveSpawnObservations": identity.get("duplicateLiveSpawnObservations"),
279+ "destroyWithoutKnownLiveObservations": identity.get("destroyWithoutKnownLiveObservations"),
280+ })
281+ except Exception as error:
282+ errors.append({"sourcePath": analysis_path.as_posix(), "error": str(error)})
283+
284+ expected_runs = int(os.environ["EXPECTED_RUNS"])
285+ run_ids = [record["runId"] for record in records]
286+ unique_run_ids = len(run_ids) == len(set(run_ids))
287+ complete_run_set = len(records) == expected_runs and unique_run_ids and not errors
288+ formal_evidence_ready = complete_run_set and all(
289+ record["formalEvidenceReady"] for record in records
290+ )
291+ summary = {
292+ "schemaVersion": 1,
293+ "scenario": os.environ["SCENARIO"],
294+ "campaignOutcome": os.environ.get("CAMPAIGN_OUTCOME", "unknown"),
295+ "requestedRuns": expected_runs,
296+ "observedRuns": len(records),
297+ "formalEvidenceReady": formal_evidence_ready,
298+ "formalCampaignReady": (
299+ formal_evidence_ready
300+ and expected_runs == 12
301+ and os.environ.get("CAMPAIGN_OUTCOME") == "success"
302+ ),
303+ "errors": errors,
304+ "runs": records,
305+ }
306+ summary_path = results_root / "protocol-trace-summary.json"
307+ summary_path.write_text(
308+ json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
309+ encoding="utf-8",
310+ )
311+
312+ step_summary = os.environ.get("GITHUB_STEP_SUMMARY")
313+ if step_summary:
314+ lines = [
315+ "### Phase 2 semantic protocol trace",
316+ "",
317+ f"- Scenario: `{summary['scenario']}`",
318+ f"- Explicit-window runs: `{summary['observedRuns']}/{summary['requestedRuns']}`",
319+ f"- Per-run `formalEvidenceReady`: `{'ready' if formal_evidence_ready else 'not-ready'}`",
320+ f"- Formal 12-run ABBA campaign: `{'ready' if summary['formalCampaignReady'] else 'incomplete'}`",
321+ "",
322+ "| Run | Variant | Ready | Window events | Spawn IDs | Unique IDs | Peak 50 ms | Peak 1 s |",
323+ "|---|---:|---:|---:|---:|---:|---:|---:|",
324+ ]
325+ for record in records:
326+ lines.append(
327+ "| {runId} | {variant} | {ready} | {events} | {spawn} | {unique} | {peak50} | {peak1s} |".format(
328+ runId=record["runId"],
329+ variant=record["variant"] or "?",
330+ ready="yes" if record["formalEvidenceReady"] else "no",
331+ events=record["windowEventCount"],
332+ spawn=record["spawnObservations"],
333+ unique=record["uniqueSpawnIds"],
334+ peak50=record["peak50msSpawnIds"],
335+ peak1s=record["peak1sSpawnIds"],
336+ )
337+ )
338+ if errors:
339+ lines.extend(["", f"Trace summary errors: `{len(errors)}`"])
340+ with open(step_summary, "a", encoding="utf-8", newline="\n") as stream:
341+ stream.write("\n".join(lines) + "\n")
342+
343+ if summary["campaignOutcome"] == "success" and not formal_evidence_ready:
344+ raise SystemExit("successful campaign is missing complete formal-ready protocol trace evidence")
345+ PY
346+
204347 - name : Publish packet evidence
205348 if : always()
206349 uses : actions/upload-artifact@v4
207350 with :
208- name : phase2-packet-${{ inputs.scenario }}-${{ github.sha }}-${{ github.run_id }}
351+ name : phase2-packet-${{ env.CAMPAIGN_SCENARIO }}-${{ github.sha }}-${{ github.run_id }}
209352 path : |
210353 phase2-results/packet
211354 phase2-dependencies/protocol-client/client-build-manifest.json
0 commit comments