Skip to content

Restore visual parity and optimize packet-only displays #3

Restore visual parity and optimize packet-only displays

Restore visual parity and optimize packet-only displays #3

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