Skip to content

Commit 8d3431c

Browse files
committed
Harden dropped-item runtime validation
Close shutdown and evidence-provenance gaps so formal runtime results and Paper compatibility checks are merge-ready.
1 parent 5fdf817 commit 8d3431c

13 files changed

Lines changed: 1876 additions & 139 deletions

File tree

.github/workflows/build.yml

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ jobs:
8080
run: |
8181
bash -n tools/perf/prepare-phase2-protocol-client.sh
8282
bash -n tools/perf/run-phase2-runtime-once.sh
83+
bash -n tools/perf/run-paper-compatibility-smoke.sh
8384
bash tools/perf/run-phase2-runtime-once.sh --self-test
85+
bash tools/perf/run-paper-compatibility-smoke.sh --self-test
8486
node --check tools/perf/phase2-protocol-client.js
8587
8688
- name: Run checks and build production jar
@@ -94,3 +96,99 @@ jobs:
9496
path: build/libs/InteractionVisualizer-*.jar
9597
if-no-files-found: error
9698
retention-days: 14
99+
100+
paper-26-2-compatibility:
101+
name: Paper 26.2 compatibility smoke
102+
needs: build
103+
runs-on: ubuntu-latest
104+
timeout-minutes: 15
105+
env:
106+
PAPER_VERSION: "26.2"
107+
PAPER_BUILD_ID: "62"
108+
PAPER_CHANNEL: BETA
109+
PAPER_SHA256: 597cb54eef27b318dfb7daef924f9bbe2816e6b0547f0f861b15b42337b6ccd5
110+
PAPER_USER_AGENT: InteractionVisualizer-CI/1.0 (https://github.com/EllanServer/InteractionVisualizer)
111+
112+
steps:
113+
- name: Check out compatibility harness
114+
uses: actions/checkout@v4
115+
with:
116+
persist-credentials: false
117+
118+
- name: Set up Java 25
119+
uses: actions/setup-java@v4
120+
with:
121+
distribution: temurin
122+
java-version: "25"
123+
124+
- name: Download current production jar
125+
uses: actions/download-artifact@v4
126+
with:
127+
name: InteractionVisualizer-${{ github.sha }}
128+
path: compatibility-dependencies/plugin
129+
130+
- name: Select production jar
131+
shell: bash
132+
run: |
133+
set -euo pipefail
134+
shopt -s nullglob
135+
candidates=()
136+
for candidate in compatibility-dependencies/plugin/InteractionVisualizer-*.jar; do
137+
case "$candidate" in
138+
*-sources.jar|*-benchmark.jar|*-runtime-compare.jar) continue ;;
139+
esac
140+
candidates+=("$candidate")
141+
done
142+
if (( ${#candidates[@]} != 1 )); then
143+
printf 'Expected exactly one production jar, found %d\n' "${#candidates[@]}" >&2
144+
printf '%s\n' "${candidates[@]}" >&2
145+
exit 1
146+
fi
147+
echo "PAPER_COMPAT_PLUGIN_JAR=${candidates[0]}" >> "$GITHUB_ENV"
148+
149+
- name: Download pinned Paper 26.2 build 62
150+
shell: bash
151+
run: |
152+
set -euo pipefail
153+
mkdir -p compatibility-dependencies/paper
154+
curl --fail --silent --show-error \
155+
-H "User-Agent: $PAPER_USER_AGENT" \
156+
--output compatibility-dependencies/paper/builds.json \
157+
"https://fill.papermc.io/v3/projects/paper/versions/$PAPER_VERSION/builds"
158+
PAPER_RECORD=$(jq -ce --argjson build_id "$PAPER_BUILD_ID" \
159+
'first(.[] | select(.id == $build_id)) // error("pinned Paper build is missing")' \
160+
compatibility-dependencies/paper/builds.json)
161+
[[ "$(jq -r '.id' <<< "$PAPER_RECORD")" == "$PAPER_BUILD_ID" ]]
162+
[[ "$(jq -r '.channel' <<< "$PAPER_RECORD")" == "$PAPER_CHANNEL" ]]
163+
[[ "$(jq -r '.downloads."server:default".checksums.sha256' <<< "$PAPER_RECORD")" == "$PAPER_SHA256" ]]
164+
[[ "$(jq -r '.downloads."server:default".name' <<< "$PAPER_RECORD")" == "paper-$PAPER_VERSION-$PAPER_BUILD_ID.jar" ]]
165+
PAPER_URL=$(jq -er '.downloads."server:default".url' <<< "$PAPER_RECORD")
166+
PAPER_COMMIT_SHA=$(jq -er '.commits[0].sha' <<< "$PAPER_RECORD")
167+
[[ "$PAPER_COMMIT_SHA" =~ ^[0-9a-f]{40}$ ]]
168+
curl --fail --location --show-error \
169+
-H "User-Agent: $PAPER_USER_AGENT" \
170+
--output compatibility-dependencies/paper/paper.jar \
171+
"$PAPER_URL"
172+
echo "$PAPER_SHA256 compatibility-dependencies/paper/paper.jar" | sha256sum --check
173+
printf '%s\n' "$PAPER_RECORD" > compatibility-dependencies/paper/build-62.json
174+
echo "PAPER_COMPAT_PAPER_COMMIT_SHA=$PAPER_COMMIT_SHA" >> "$GITHUB_ENV"
175+
176+
- name: Run real Paper 26.2 compatibility smoke
177+
shell: bash
178+
env:
179+
PAPER_COMPAT_PAPER_JAR: compatibility-dependencies/paper/paper.jar
180+
PAPER_COMPAT_OUTPUT_ROOT: compatibility-results
181+
PAPER_COMPAT_SOURCE_SHA: ${{ github.sha }}
182+
run: bash tools/perf/run-paper-compatibility-smoke.sh
183+
184+
- name: Upload Paper 26.2 compatibility evidence
185+
if: always()
186+
uses: actions/upload-artifact@v4
187+
with:
188+
name: paper-26.2-build-62-compatibility-${{ github.sha }}-${{ github.run_id }}
189+
path: |
190+
compatibility-results/server.log
191+
compatibility-results/compatibility-manifest.json
192+
compatibility-dependencies/paper/build-62.json
193+
if-no-files-found: warn
194+
retention-days: 14

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

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,24 +55,27 @@ permissions:
5555

5656
jobs:
5757
clean-runtime-ab:
58-
name: Paper runtime ABBA
58+
name: >-
59+
${{ github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'phase2-runtime-dropped-item-formal' && 'Dropped-item formal runtime gate' || github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'phase2-runtime-formal' && 'Paper runtime formal ABBA' || github.event_name == 'workflow_dispatch' && inputs.spark_profile_mode == 'alloc' && 'Allocation diagnostic runtime ABBA' || github.event_name == 'workflow_dispatch' && inputs.spark_profile_mode != 'none' && 'Profile diagnostic runtime ABBA' || github.event_name == 'workflow_dispatch' && inputs.ab_factor == 'dropped-item-section-candidates' && 'Dropped-item formal runtime gate' || 'Paper runtime ABBA smoke' }}
5960
if: >-
6061
github.event_name == 'workflow_dispatch' ||
6162
github.event.action != 'labeled' ||
62-
github.event.label.name == 'phase2-runtime-formal'
63+
github.event.label.name == 'phase2-runtime-formal' ||
64+
github.event.label.name == 'phase2-runtime-dropped-item-formal'
6365
runs-on: ubuntu-latest
6466
timeout-minutes: 130
6567
env:
66-
CAMPAIGN_AB_FACTOR: ${{ github.event_name == 'workflow_dispatch' && inputs.ab_factor || 'scenario-config' }}
68+
CAMPAIGN_AB_FACTOR: ${{ github.event.action == 'labeled' && github.event.label.name == 'phase2-runtime-dropped-item-formal' && 'dropped-item-section-candidates' || github.event_name == 'workflow_dispatch' && inputs.ab_factor || 'scenario-config' }}
6769
CAMPAIGN_PAPER_VERSION: "26.1.2"
68-
CAMPAIGN_SCENARIO: ${{ github.event_name == 'workflow_dispatch' && inputs.scenario || 'static-steady' }}
69-
CAMPAIGN_RUNS: ${{ github.event_name == 'workflow_dispatch' && inputs.runs || github.event.action == 'labeled' && '12' || '4' }}
70-
CAMPAIGN_ITEMS: ${{ github.event_name == 'workflow_dispatch' && inputs.items || github.event.action == 'labeled' && '4096' || '1024' }}
70+
CAMPAIGN_PAPER_BUILD_ID: "74"
71+
CAMPAIGN_SCENARIO: ${{ (github.event.action == 'labeled' && github.event.label.name == 'phase2-runtime-dropped-item-formal' || github.event_name == 'workflow_dispatch' && inputs.ab_factor == 'dropped-item-section-candidates') && 'dropped-items' || github.event_name == 'workflow_dispatch' && inputs.scenario || 'static-steady' }}
72+
CAMPAIGN_RUNS: ${{ (github.event.action == 'labeled' && github.event.label.name == 'phase2-runtime-dropped-item-formal' || github.event_name == 'workflow_dispatch' && inputs.ab_factor == 'dropped-item-section-candidates' && inputs.spark_profile_mode == 'none') && '12' || github.event_name == 'workflow_dispatch' && inputs.ab_factor == 'dropped-item-section-candidates' && '4' || github.event_name == 'workflow_dispatch' && inputs.runs || github.event.action == 'labeled' && '12' || '4' }}
73+
CAMPAIGN_ITEMS: ${{ (github.event.action == 'labeled' && github.event.label.name == 'phase2-runtime-dropped-item-formal' || github.event_name == 'workflow_dispatch' && inputs.ab_factor == 'dropped-item-section-candidates') && '2048' || github.event_name == 'workflow_dispatch' && inputs.items || github.event.action == 'labeled' && '4096' || '1024' }}
7174
CAMPAIGN_DROPPED_NEARBY_ITEMS: "128"
72-
CAMPAIGN_WARMUP_SECONDS: ${{ github.event_name == 'workflow_dispatch' && inputs.warmup_seconds || github.event.action == 'labeled' && '120' || '10' }}
73-
CAMPAIGN_SETTLE_SECONDS: ${{ github.event_name == 'workflow_dispatch' && inputs.settle_seconds || github.event.action == 'labeled' && '20' || '5' }}
74-
CAMPAIGN_MEASURE_SECONDS: ${{ github.event_name == 'workflow_dispatch' && inputs.measure_seconds || github.event.action == 'labeled' && '180' || '10' }}
75-
CAMPAIGN_SPARK_PROFILE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.spark_profile_mode || 'none' }}
75+
CAMPAIGN_WARMUP_SECONDS: ${{ (github.event.action == 'labeled' && github.event.label.name == 'phase2-runtime-dropped-item-formal' || github.event_name == 'workflow_dispatch' && inputs.ab_factor == 'dropped-item-section-candidates' && inputs.spark_profile_mode == 'none') && '120' || github.event_name == 'workflow_dispatch' && inputs.warmup_seconds || github.event.action == 'labeled' && '120' || '10' }}
76+
CAMPAIGN_SETTLE_SECONDS: ${{ (github.event.action == 'labeled' && github.event.label.name == 'phase2-runtime-dropped-item-formal' || github.event_name == 'workflow_dispatch' && inputs.ab_factor == 'dropped-item-section-candidates' && inputs.spark_profile_mode == 'none') && '20' || github.event_name == 'workflow_dispatch' && inputs.settle_seconds || github.event.action == 'labeled' && '20' || '5' }}
77+
CAMPAIGN_MEASURE_SECONDS: ${{ (github.event.action == 'labeled' && github.event.label.name == 'phase2-runtime-dropped-item-formal' || github.event_name == 'workflow_dispatch' && inputs.ab_factor == 'dropped-item-section-candidates' && inputs.spark_profile_mode == 'none') && '180' || github.event_name == 'workflow_dispatch' && inputs.measure_seconds || github.event.action == 'labeled' && '180' || '10' }}
78+
CAMPAIGN_SPARK_PROFILE_MODE: ${{ github.event.action == 'labeled' && github.event.label.name == 'phase2-runtime-dropped-item-formal' && 'none' || github.event_name == 'workflow_dispatch' && inputs.spark_profile_mode || 'none' }}
7679
CAMPAIGN_EVIDENCE_KIND: ${{ github.event_name == 'workflow_dispatch' && inputs.spark_profile_mode != 'none' && 'diagnostic' || 'clean' }}
7780

7881
steps:
@@ -112,28 +115,30 @@ jobs:
112115
env:
113116
PAPER_USER_AGENT: InteractionVisualizer-Phase2/1.0 (https://github.com/EllanServer/InteractionVisualizer)
114117
PAPER_VERSION: ${{ env.CAMPAIGN_PAPER_VERSION }}
118+
PINNED_PAPER_BUILD_ID: ${{ env.CAMPAIGN_PAPER_BUILD_ID }}
115119
run: |
116120
mkdir -p phase2-dependencies
117121
case "$PAPER_VERSION" in
118122
26.1.2) PAPER_CHANNEL=STABLE ;;
119-
26.2) PAPER_CHANNEL=BETA ;;
120123
*) echo "Unsupported Paper version: $PAPER_VERSION" >&2; exit 64 ;;
121124
esac
122125
BUILDS=$(curl --fail --silent --show-error \
123126
-H "User-Agent: $PAPER_USER_AGENT" \
124127
"https://fill.papermc.io/v3/projects/paper/versions/$PAPER_VERSION/builds")
125-
PAPER_RECORD=$(echo "$BUILDS" | jq -c --arg channel "$PAPER_CHANNEL" \
126-
'first(.[] | select(.channel == $channel)) // empty')
128+
PAPER_RECORD=$(echo "$BUILDS" | jq -c \
129+
--arg channel "$PAPER_CHANNEL" --argjson build_id "$PINNED_PAPER_BUILD_ID" \
130+
'first(.[] | select(.channel == $channel and .id == $build_id)) // empty')
127131
test -n "$PAPER_RECORD"
128132
PAPER_URL=$(echo "$PAPER_RECORD" | jq -r '.downloads."server:default".url')
129133
PAPER_SHA256=$(echo "$PAPER_RECORD" | jq -r '.downloads."server:default".checksums.sha256')
130-
PAPER_BUILD_ID=$(echo "$PAPER_RECORD" | jq -r '.id')
134+
SELECTED_PAPER_BUILD_ID=$(echo "$PAPER_RECORD" | jq -r '.id')
135+
[[ "$SELECTED_PAPER_BUILD_ID" == "$PINNED_PAPER_BUILD_ID" ]]
131136
curl --fail --location --show-error \
132137
-H "User-Agent: $PAPER_USER_AGENT" \
133138
--output phase2-dependencies/paper.jar "$PAPER_URL"
134139
echo "$PAPER_SHA256 phase2-dependencies/paper.jar" | sha256sum --check
135140
echo "SELECTED_PAPER_CHANNEL=$PAPER_CHANNEL" >> "$GITHUB_ENV"
136-
echo "SELECTED_PAPER_BUILD_ID=$PAPER_BUILD_ID" >> "$GITHUB_ENV"
141+
echo "SELECTED_PAPER_BUILD_ID=$SELECTED_PAPER_BUILD_ID" >> "$GITHUB_ENV"
137142
138143
- name: Prepare immutable protocol client artifact
139144
run: bash tools/perf/prepare-phase2-protocol-client.sh phase2-dependencies/protocol-client
@@ -199,11 +204,17 @@ jobs:
199204
echo "dropped-item-section-candidates A/B is isolated to dropped-items" >&2
200205
exit 64
201206
fi
202-
if [[ "$SPARK_PROFILE_MODE" == none && "$RUNS" != 12 ]]; then
203-
echo "clean dropped-item-section-candidates evidence is formal-only and requires runs=12" >&2
204-
exit 64
205-
fi
206-
if [[ "$SPARK_PROFILE_MODE" != none && "$RUNS" != 4 ]]; then
207+
if [[ "$SPARK_PROFILE_MODE" == none ]]; then
208+
if [[ "$RUNS" != 12 ]]; then
209+
echo "clean dropped-item-section-candidates evidence is formal-only and requires runs=12" >&2
210+
exit 64
211+
fi
212+
if [[ "$WARMUP_SECONDS" != 120 || "$SETTLE_SECONDS" != 20 || \
213+
"$MEASURE_SECONDS" != 180 ]]; then
214+
echo "clean dropped-item-section-candidates evidence requires canonical 120/20/180 windows" >&2
215+
exit 64
216+
fi
217+
elif [[ "$RUNS" != 4 ]]; then
207218
echo "profiled dropped-item-section-candidates diagnostics require runs=4" >&2
208219
exit 64
209220
fi

common/src/main/java/com/loohp/interactionvisualizer/api/VisualizerRunnableDisplay.java

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ public abstract class VisualizerRunnableDisplay implements VisualizerDisplay {
3535
/**
3636
* DO NOT CHANGE THESE FIELD
3737
*/
38-
private Set<ScheduledTask> tasks;
38+
private Set<ScheduledTask> tasks = new HashSet<>();
39+
private boolean unregistered;
3940

4041
/**
4142
* This method is used for cleaning up, return the ScheduledTask, return null to disable.
@@ -56,6 +57,7 @@ public final void register() {
5657
}
5758
InteractionVisualizerAPI.getPreferenceManager().registerEntry(key());
5859
TaskManager.runnables.add(this);
60+
this.unregistered = false;
5961
this.tasks = new HashSet<>();
6062
ScheduledTask gc = gc();
6163
if (gc != null) {
@@ -70,6 +72,7 @@ public final void register() {
7072
@Deprecated
7173
public final EntryKey registerNative() {
7274
TaskManager.runnables.add(this);
75+
this.unregistered = false;
7376
this.tasks = new HashSet<>();
7477
ScheduledTask gc = gc();
7578
if (gc != null) {
@@ -82,14 +85,67 @@ public final EntryKey registerNative() {
8285
return key();
8386
}
8487

88+
/**
89+
* Called once while this display is being unregistered, after the tasks
90+
* returned by {@link #gc()} and {@link #run()} have been cancelled.
91+
*/
92+
protected void onUnregister() {
93+
}
94+
8595
/**
8696
* Unregister this custom display to InteractionVisualizer.
8797
* You don't have to use this normally.
8898
*/
8999
@Deprecated
90-
public final void unregister() {
91-
TaskManager.runnables.remove(this);
92-
this.tasks.forEach(each -> each.cancel());
100+
public final synchronized void unregister() {
101+
if (unregistered) {
102+
return;
103+
}
104+
unregistered = true;
105+
106+
Throwable failure = null;
107+
Set<ScheduledTask> registeredTasks = tasks;
108+
tasks = new HashSet<>();
109+
for (ScheduledTask task : registeredTasks) {
110+
try {
111+
task.cancel();
112+
} catch (Throwable throwable) {
113+
failure = appendFailure(failure, throwable);
114+
}
115+
}
116+
try {
117+
onUnregister();
118+
} catch (Throwable throwable) {
119+
failure = appendFailure(failure, throwable);
120+
}
121+
try {
122+
TaskManager.runnables.removeIf(each -> each == this);
123+
} catch (Throwable throwable) {
124+
failure = appendFailure(failure, throwable);
125+
}
126+
rethrow(failure);
127+
}
128+
129+
private static Throwable appendFailure(Throwable current, Throwable addition) {
130+
if (current == null) {
131+
return addition;
132+
}
133+
if (current != addition) {
134+
current.addSuppressed(addition);
135+
}
136+
return current;
137+
}
138+
139+
private static void rethrow(Throwable failure) {
140+
if (failure instanceof Error error) {
141+
throw error;
142+
}
143+
if (failure instanceof RuntimeException exception) {
144+
throw exception;
145+
}
146+
if (failure != null) {
147+
throw new RuntimeException(failure);
148+
}
93149
}
94150

95151
}

0 commit comments

Comments
 (0)