Restore visual parity and optimize packet-only displays #3
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Phase 2 Packet Capture A-B | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, labeled] | |
| workflow_dispatch: | |
| inputs: | |
| scenario: | |
| description: "Independent network scenario" | |
| required: true | |
| default: "static-spawn" | |
| type: choice | |
| options: ["static-spawn", "visibility-return"] | |
| runs: | |
| description: "Restart-isolated runs (4, 8, or formal 12)" | |
| required: true | |
| default: "12" | |
| type: choice | |
| options: ["4", "8", "12"] | |
| items: | |
| description: "Logical items" | |
| required: true | |
| default: "4096" | |
| type: string | |
| warmup_seconds: | |
| description: "Warmup after the real TCP client is ready" | |
| required: true | |
| default: "60" | |
| type: string | |
| settle_seconds: | |
| description: "Pre-window settling time" | |
| required: true | |
| default: "10" | |
| type: string | |
| measure_seconds: | |
| description: "Packet capture window" | |
| required: true | |
| default: "10" | |
| type: string | |
| snaplen: | |
| description: "tcpdump snapshot length; 128 for transport metrics, 0 for full compatibility capture" | |
| required: true | |
| default: "128" | |
| type: choice | |
| options: ["128", "0"] | |
| permissions: | |
| contents: read | |
| jobs: | |
| packet-capture-ab: | |
| name: Paper 26.1.2 packet ABBA | |
| if: >- | |
| github.event_name == 'workflow_dispatch' || | |
| github.event.action != 'labeled' || | |
| github.event.label.name == 'phase2-packet-static-formal' || | |
| github.event.label.name == 'phase2-packet-visibility-formal' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 100 | |
| env: | |
| CAMPAIGN_SCENARIO: ${{ github.event_name == 'workflow_dispatch' && inputs.scenario || github.event.label.name == 'phase2-packet-visibility-formal' && 'visibility-return' || 'static-spawn' }} | |
| CAMPAIGN_RUNS: ${{ github.event_name == 'workflow_dispatch' && inputs.runs || github.event.action == 'labeled' && '12' || '4' }} | |
| CAMPAIGN_ITEMS: ${{ github.event_name == 'workflow_dispatch' && inputs.items || github.event.action == 'labeled' && '4096' || '1024' }} | |
| CAMPAIGN_WARMUP_SECONDS: ${{ github.event_name == 'workflow_dispatch' && inputs.warmup_seconds || github.event.action == 'labeled' && '60' || '10' }} | |
| CAMPAIGN_SETTLE_SECONDS: ${{ github.event_name == 'workflow_dispatch' && inputs.settle_seconds || github.event.action == 'labeled' && '10' || '5' }} | |
| CAMPAIGN_MEASURE_SECONDS: ${{ github.event_name == 'workflow_dispatch' && inputs.measure_seconds || github.event.action == 'labeled' && '10' || '5' }} | |
| CAMPAIGN_SNAPLEN: ${{ github.event_name == 'workflow_dispatch' && inputs.snaplen || '128' }} | |
| steps: | |
| - name: Check out immutable test source | |
| uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Java 25 | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: "25" | |
| - name: Set up Node 24 for the isolated protocol peer | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "24" | |
| - name: Set up Gradle | |
| uses: gradle/actions/setup-gradle@v4 | |
| - name: Install capture readers | |
| env: | |
| DEBIAN_FRONTEND: noninteractive | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install --yes --no-install-recommends tcpdump tshark | |
| tcpdump --version | |
| tshark --version | |
| - name: Validate harness source | |
| run: | | |
| bash -n tools/perf/prepare-phase2-protocol-client.sh | |
| bash -n tools/perf/run-phase2-runtime-once.sh | |
| node --check tools/perf/phase2-protocol-client.js | |
| pwsh -NoProfile -File tools/perf/analyze-phase2-abba.ps1 -SelfTest | |
| pwsh -NoProfile -File tools/perf/analyze-phase2-pcap.ps1 -SelfTest | |
| - name: Build and test the production plugin | |
| run: ./gradlew clean check shadowJar --no-daemon --no-build-cache --rerun-tasks | |
| - name: Download stable Paper 26.1.2 | |
| env: | |
| PAPER_USER_AGENT: InteractionVisualizer-Phase2/1.0 (https://github.com/EllanServer/InteractionVisualizer) | |
| run: | | |
| mkdir -p phase2-dependencies | |
| BUILDS=$(curl --fail --silent --show-error \ | |
| -H "User-Agent: $PAPER_USER_AGENT" \ | |
| https://fill.papermc.io/v3/projects/paper/versions/26.1.2/builds) | |
| PAPER_URL=$(echo "$BUILDS" | jq -r 'first(.[] | select(.channel == "STABLE") | .downloads."server:default".url) // empty') | |
| test -n "$PAPER_URL" | |
| curl --fail --location --show-error \ | |
| -H "User-Agent: $PAPER_USER_AGENT" \ | |
| --output phase2-dependencies/paper.jar "$PAPER_URL" | |
| - name: Prepare immutable protocol client artifact | |
| run: bash tools/perf/prepare-phase2-protocol-client.sh phase2-dependencies/protocol-client | |
| - name: Run restart-isolated packet ABBA campaign | |
| env: | |
| SCENARIO: ${{ env.CAMPAIGN_SCENARIO }} | |
| RUNS: ${{ env.CAMPAIGN_RUNS }} | |
| ITEMS: ${{ env.CAMPAIGN_ITEMS }} | |
| WARMUP_SECONDS: ${{ env.CAMPAIGN_WARMUP_SECONDS }} | |
| SETTLE_SECONDS: ${{ env.CAMPAIGN_SETTLE_SECONDS }} | |
| MEASURE_SECONDS: ${{ env.CAMPAIGN_MEASURE_SECONDS }} | |
| SNAPLEN: ${{ env.CAMPAIGN_SNAPLEN }} | |
| run: | | |
| set -euo pipefail | |
| [[ "$SCENARIO" == static-spawn || "$SCENARIO" == visibility-return ]] | |
| [[ "$RUNS" =~ ^(4|8|12)$ ]] | |
| [[ "$ITEMS" =~ ^[0-9]+$ ]] && (( ITEMS >= 1 && ITEMS <= 8192 )) | |
| [[ "$WARMUP_SECONDS" =~ ^[0-9]+$ ]] && (( WARMUP_SECONDS >= 10 )) | |
| [[ "$SETTLE_SECONDS" =~ ^[0-9]+$ ]] && (( SETTLE_SECONDS >= 5 )) | |
| [[ "$MEASURE_SECONDS" =~ ^[0-9]+$ ]] && (( MEASURE_SECONDS >= 5 )) | |
| [[ "$SNAPLEN" == 0 || "$SNAPLEN" == 128 ]] | |
| if [[ "$SCENARIO" == visibility-return ]]; then | |
| (( MEASURE_SECONDS >= 10 )) | |
| fi | |
| PLUGIN_JAR=$(find build/libs -maxdepth 1 -type f -name 'InteractionVisualizer-*.jar' ! -name '*-sources.jar' ! -name '*-benchmark.jar' -print -quit) | |
| test -n "$PLUGIN_JAR" | |
| mkdir -p phase2-results/packet | |
| MANIFEST=phase2-results/packet/abba-manifest.csv | |
| printf 'Scenario,Block,Position,Variant,RunId,StackSha256,ArtifactSha256,CaptureMethod,SourcePath\n' > "$MANIFEST" | |
| ARTIFACT_SHA=$(sha256sum "$PLUGIN_JAR" | awk '{print $1}') | |
| STACK_SHA=$( | |
| { | |
| sha256sum phase2-dependencies/paper.jar | |
| sha256sum phase2-dependencies/protocol-client/client-build-manifest.json | |
| java -version 2>&1 | |
| printf '%s\n' 'java=-Xms2G -Xmx2G -XX:+UseG1GC -XX:+AlwaysPreTouch' "scenario=$SCENARIO" "snaplen=$SNAPLEN" | |
| } | sha256sum | awk '{print $1}' | |
| ) | |
| for run_number in $(seq 1 "$RUNS"); do | |
| block=$(( (run_number - 1) / 4 + 1 )) | |
| position=$(( (run_number - 1) % 4 + 1 )) | |
| if (( block % 2 == 1 )); then pattern=ABBA; else pattern=BAAB; fi | |
| variant=${pattern:$((position - 1)):1} | |
| run_id=$(printf '%s_%s_%02d' "${SCENARIO//-/_}" "$variant" "$run_number") | |
| PHASE2_PLUGIN_JAR="$PLUGIN_JAR" \ | |
| PHASE2_PAPER_JAR=phase2-dependencies/paper.jar \ | |
| PHASE2_CLIENT_ROOT=phase2-dependencies/protocol-client \ | |
| PHASE2_OUTPUT_ROOT=phase2-results/packet \ | |
| PHASE2_RUN_ID="$run_id" \ | |
| PHASE2_SCENARIO="$SCENARIO" \ | |
| PHASE2_VARIANT="$variant" \ | |
| PHASE2_ITEM_COUNT="$ITEMS" \ | |
| PHASE2_WARMUP_SECONDS="$WARMUP_SECONDS" \ | |
| PHASE2_SETTLE_SECONDS="$SETTLE_SECONDS" \ | |
| PHASE2_MEASURE_SECONDS="$MEASURE_SECONDS" \ | |
| PHASE2_CAPTURE_ENABLED=1 \ | |
| PHASE2_CAPTURE_SNAPLEN="$SNAPLEN" \ | |
| bash tools/perf/run-phase2-runtime-once.sh | |
| printf '%s,%d,%d,%s,%s,%s,%s,tcpdump-lo-s%s,%s/%s.pcap-analysis.json\n' \ | |
| "$SCENARIO" "$block" "$position" "$variant" "$run_id" "$STACK_SHA" "$ARTIFACT_SHA" \ | |
| "$SNAPLEN" "$run_id" "$run_id" >> "$MANIFEST" | |
| done | |
| minimum_seconds=$(( MEASURE_SECONDS - 1 )) | |
| incomplete=() | |
| if [[ "$RUNS" != 12 ]]; then incomplete=(-AllowIncomplete); fi | |
| # Loopback capture preserves TCP payload volume/timing for paired | |
| # regression checks, but its synthetic link headers and offload | |
| # behavior are not evidence of physical on-wire frame bytes. | |
| for metric in downstream.tcpPayloadBytes downstream.peak50ms.tcpPayloadBytes downstream.peak1s.tcpPayloadBytes; do | |
| safe_metric=${metric//./_} | |
| pwsh -NoProfile -File tools/perf/analyze-phase2-abba.ps1 "$MANIFEST" \ | |
| -Scenario "$SCENARIO" -Metric "$metric" -Direction LowerIsBetter \ | |
| -MinimumSeconds "$minimum_seconds" "${incomplete[@]}" \ | |
| -OutputJson "phase2-results/packet/$safe_metric.analysis.json" -Overwrite | |
| done | |
| - name: Publish packet evidence | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: phase2-packet-${{ inputs.scenario }}-${{ github.sha }}-${{ github.run_id }} | |
| path: | | |
| phase2-results/packet | |
| phase2-dependencies/protocol-client/client-build-manifest.json | |
| phase2-dependencies/protocol-client/client-files.sha256 | |
| phase2-dependencies/protocol-client/node-minecraft-protocol/package-lock.json | |
| phase2-dependencies/protocol-client/node-minecraft-protocol/production-lock-inventory.json | |
| if-no-files-found: warn | |
| retention-days: 30 |