Skip to content

Commit 0c604dd

Browse files
committed
Merge origin/master into Paper 26 hotspot optimization
2 parents 7050203 + ad3e59b commit 0c604dd

10 files changed

Lines changed: 3918 additions & 21 deletions

File tree

.github/workflows/upstream-runtime-comparison.yml

Lines changed: 1657 additions & 0 deletions
Large diffs are not rendered by default.

benchmark-runtime/src/main/java/com/loohp/interactionvisualizer/benchmark/runtime/RuntimeComparisonPlugin.java

Lines changed: 655 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: InteractionVisualizerRuntimeCompare
2+
version: '1.0'
3+
main: com.loohp.interactionvisualizer.benchmark.runtime.RuntimeComparisonPlugin
4+
api-version: '26.1'
5+
softdepend: [InteractionVisualizer]
6+
description: Disposable target-independent runtime comparison driver for GitHub Actions
7+
commands:
8+
ivcompare:
9+
description: Controls the disposable InteractionVisualizer runtime comparison
10+
permission: interactionvisualizer.runtimecompare
11+
permissions:
12+
interactionvisualizer.runtimecompare:
13+
default: op

build.gradle.kts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@ val benchmarkSourceSet = sourceSets.create("benchmark") {
7777
runtimeClasspath += output + compileClasspath
7878
}
7979

80+
val runtimeComparisonSourceSet = sourceSets.create("runtimeComparison") {
81+
java.setSrcDirs(listOf("benchmark-runtime/src/main/java"))
82+
resources.setSrcDirs(listOf("benchmark-runtime/src/main/resources"))
83+
compileClasspath += configurations.compileClasspath.get()
84+
runtimeClasspath += output + compileClasspath
85+
}
86+
87+
sourceSets.test {
88+
compileClasspath += runtimeComparisonSourceSet.output
89+
runtimeClasspath += runtimeComparisonSourceSet.output
90+
}
91+
8092
val benchmarkJar = tasks.register<Jar>("benchmarkJar") {
8193
description = "Builds the standalone Paper A/B benchmark plugin (never shipped in production)."
8294
group = LifecycleBasePlugin.VERIFICATION_GROUP
@@ -88,6 +100,14 @@ val benchmarkJar = tasks.register<Jar>("benchmarkJar") {
88100
}
89101
}
90102

103+
val runtimeComparisonJar = tasks.register<Jar>("runtimeComparisonJar") {
104+
description = "Builds the target-independent Paper runtime comparison driver."
105+
group = LifecycleBasePlugin.VERIFICATION_GROUP
106+
dependsOn(runtimeComparisonSourceSet.classesTaskName)
107+
archiveClassifier = "runtime-compare"
108+
from(runtimeComparisonSourceSet.output)
109+
}
110+
91111
tasks.withType<JavaCompile>().configureEach {
92112
options.release = 25
93113
options.encoding = "UTF-8"
@@ -252,6 +272,7 @@ tasks.check {
252272
dependsOn(verifyPaperOnlyArchitecture)
253273
dependsOn(verifyCustomContentIsolation)
254274
dependsOn(testLegacyTextComponentCacheDisabled)
275+
dependsOn(runtimeComparisonJar)
255276
}
256277

257278
tasks.named<ShadowJar>("shadowJar") {
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* This file is part of InteractionVisualizer.
3+
*
4+
* Copyright (C) 2026. Contributors
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*/
11+
12+
package com.loohp.interactionvisualizer.benchmark.runtime;
13+
14+
import org.junit.jupiter.api.Test;
15+
16+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
17+
import static org.junit.jupiter.api.Assertions.assertEquals;
18+
import static org.junit.jupiter.api.Assertions.assertFalse;
19+
import static org.junit.jupiter.api.Assertions.assertNull;
20+
import static org.junit.jupiter.api.Assertions.assertThrows;
21+
import static org.junit.jupiter.api.Assertions.assertTrue;
22+
23+
class RuntimeComparisonPluginTest {
24+
25+
@Test
26+
void requestedFlagsComeFromTheControlledRuntimeProfile() {
27+
assertEquals(
28+
new RuntimeComparisonPlugin.RequestedFlags(false, false),
29+
RuntimeComparisonPlugin.requestedFlagsForProfile("legacy-parity"));
30+
assertEquals(
31+
new RuntimeComparisonPlugin.RequestedFlags(true, true),
32+
RuntimeComparisonPlugin.requestedFlagsForProfile("optimized-candidate"));
33+
assertThrows(IllegalArgumentException.class,
34+
() -> RuntimeComparisonPlugin.requestedFlagsForProfile("unknown"));
35+
}
36+
37+
@Test
38+
void legacyTargetIsRecordedAsUnsupported() {
39+
RuntimeComparisonPlugin.EffectiveFlags flags =
40+
RuntimeComparisonPlugin.inspectEffectiveFlags(LegacyTarget.class);
41+
42+
assertEquals("unsupported-legacy", flags.packetOnlyStatic().status());
43+
assertNull(flags.packetOnlyStatic().value());
44+
assertEquals("unsupported-legacy", flags.eventDrivenBlockUpdates().status());
45+
assertNull(flags.eventDrivenBlockUpdates().value());
46+
assertEquals(
47+
"{\"packetOnlyStatic\":{"
48+
+ "\"status\":\"unsupported-legacy\",\"value\":null,"
49+
+ "\"field\":\"packetOnlyStaticVirtualItems\"},"
50+
+ "\"eventDrivenBlockUpdates\":{"
51+
+ "\"status\":\"unsupported-legacy\",\"value\":null,"
52+
+ "\"field\":\"eventDrivenBlockUpdates\"}}",
53+
flags.json());
54+
assertDoesNotThrow(() -> RuntimeComparisonPlugin.assertOptimizationProfile(
55+
"A", new RuntimeComparisonPlugin.RequestedFlags(true, true), flags));
56+
}
57+
58+
@Test
59+
void optimizedCandidateMustExposeTwoEnabledRuntimeFields() {
60+
RuntimeComparisonPlugin.EffectiveFlags enabled =
61+
RuntimeComparisonPlugin.inspectEffectiveFlags(EnabledCandidate.class);
62+
RuntimeComparisonPlugin.EffectiveFlags disabled =
63+
RuntimeComparisonPlugin.inspectEffectiveFlags(DisabledCandidate.class);
64+
65+
assertTrue(enabled.packetOnlyStatic().value());
66+
assertTrue(enabled.eventDrivenBlockUpdates().value());
67+
assertFalse(disabled.packetOnlyStatic().value());
68+
assertTrue(disabled.eventDrivenBlockUpdates().value());
69+
RuntimeComparisonPlugin.RequestedFlags requested =
70+
new RuntimeComparisonPlugin.RequestedFlags(true, true);
71+
assertDoesNotThrow(() -> RuntimeComparisonPlugin.assertOptimizationProfile(
72+
"B", requested, enabled));
73+
assertThrows(IllegalStateException.class,
74+
() -> RuntimeComparisonPlugin.assertOptimizationProfile("B", requested, disabled));
75+
}
76+
77+
@Test
78+
void partialOptimizationRequestIsRejected() {
79+
RuntimeComparisonPlugin.EffectiveFlags enabled =
80+
RuntimeComparisonPlugin.inspectEffectiveFlags(EnabledCandidate.class);
81+
82+
assertThrows(IllegalStateException.class,
83+
() -> RuntimeComparisonPlugin.assertOptimizationProfile(
84+
"B", new RuntimeComparisonPlugin.RequestedFlags(true, false), enabled));
85+
}
86+
87+
public static final class LegacyTarget {
88+
}
89+
90+
public static final class EnabledCandidate {
91+
public static boolean packetOnlyStaticVirtualItems = true;
92+
public static boolean eventDrivenBlockUpdates = true;
93+
}
94+
95+
public static final class DisabledCandidate {
96+
public static boolean packetOnlyStaticVirtualItems = false;
97+
public static boolean eventDrivenBlockUpdates = true;
98+
}
99+
}

tools/perf/analyze-phase2-abba.ps1

Lines changed: 89 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ Validates and analyzes a same-stack, restart-per-run Phase 2 ABBA campaign.
44
55
.DESCRIPTION
66
Consumes a CSV manifest with one row per independent run. Every formal campaign
7-
contains three four-run blocks in ABBA, BAAB, ABBA order. Adjacent runs are
8-
paired, B/A log-ratios are summarized by their median, and a deterministic
9-
paired bootstrap produces a 95% confidence interval.
7+
contains three four-run blocks in ABBA, BAAB, ABBA order. By default adjacent
8+
runs are paired. Pass -IndependentSamples when every run executes concurrently
9+
on its own runner; A and B are then resampled independently and the estimator is
10+
median(B) / median(A).
1011
1112
Required manifest columns:
1213
Scenario,Block,Position,Variant,RunId,StackSha256,ArtifactSha256,CaptureMethod,SourcePath
@@ -49,6 +50,8 @@ param(
4950

5051
[switch]$AllowIncomplete,
5152

53+
[switch]$IndependentSamples,
54+
5255
[string]$OutputJson,
5356

5457
[switch]$Overwrite,
@@ -295,6 +298,42 @@ function Get-BootstrapSummary {
295298
}
296299
}
297300

301+
function Get-IndependentBootstrapSummary {
302+
param(
303+
[Parameter(Mandatory = $true)][double[]]$AValues,
304+
[Parameter(Mandatory = $true)][double[]]$BValues,
305+
[Parameter(Mandatory = $true)][int]$Iterations,
306+
[Parameter(Mandatory = $true)][int]$RandomSeed
307+
)
308+
309+
if ($AValues.Count -eq 0 -or $BValues.Count -eq 0) {
310+
throw "Independent bootstrap requires non-empty A and B samples."
311+
}
312+
if (@($AValues + $BValues | Where-Object { $_ -le 0.0 }).Count -ne 0) {
313+
throw "Independent ratio bootstrap requires strictly positive metric values."
314+
}
315+
$random = [Random]::new($RandomSeed)
316+
$bootstrap = New-Object 'double[]' $Iterations
317+
for ($iteration = 0; $iteration -lt $Iterations; $iteration++) {
318+
$aSample = New-Object 'double[]' $AValues.Count
319+
$bSample = New-Object 'double[]' $BValues.Count
320+
for ($index = 0; $index -lt $AValues.Count; $index++) {
321+
$aSample[$index] = $AValues[$random.Next($AValues.Count)]
322+
}
323+
for ($index = 0; $index -lt $BValues.Count; $index++) {
324+
$bSample[$index] = $BValues[$random.Next($BValues.Count)]
325+
}
326+
$bootstrap[$iteration] = [math]::Log(
327+
(Get-Median -Values $bSample) / (Get-Median -Values $aSample))
328+
}
329+
return [pscustomobject]@{
330+
Median = [math]::Log(
331+
(Get-Median -Values $BValues) / (Get-Median -Values $AValues))
332+
Lower = Get-NearestRankPercentile -Values $bootstrap -Percentile 0.025
333+
Upper = Get-NearestRankPercentile -Values $bootstrap -Percentile 0.975
334+
}
335+
}
336+
298337
function Round-Metric {
299338
param([Parameter(Mandatory = $true)][double]$Value)
300339
return [math]::Round($Value, 6, [MidpointRounding]::AwayFromZero)
@@ -462,6 +501,7 @@ function Get-ScenarioResult {
462501
[Parameter(Mandatory = $true)][string]$MetricPath,
463502
[Parameter(Mandatory = $true)][string]$MetricDirection,
464503
[Parameter(Mandatory = $true)][bool]$PermitIncomplete,
504+
[bool]$UseIndependentSamples = $false,
465505
[Parameter(Mandatory = $true)][int]$Iterations,
466506
[Parameter(Mandatory = $true)][int]$RandomSeed,
467507
[switch]$Prepared
@@ -674,8 +714,18 @@ function Get-ScenarioResult {
674714
}
675715
}
676716

677-
[double[]]$logRatios = @($pairs | ForEach-Object { [double]$_.LogRatio })
678-
$bootstrap = Get-BootstrapSummary -LogRatios $logRatios -Iterations $Iterations -RandomSeed $RandomSeed
717+
if ($UseIndependentSamples) {
718+
[double[]]$aValues = @($evidenceRuns.ToArray() | Where-Object variant -eq "A" |
719+
ForEach-Object { [double]$_.value })
720+
[double[]]$bValues = @($evidenceRuns.ToArray() | Where-Object variant -eq "B" |
721+
ForEach-Object { [double]$_.value })
722+
$bootstrap = Get-IndependentBootstrapSummary -AValues $aValues -BValues $bValues `
723+
-Iterations $Iterations -RandomSeed $RandomSeed
724+
} else {
725+
[double[]]$logRatios = @($pairs | ForEach-Object { [double]$_.LogRatio })
726+
$bootstrap = Get-BootstrapSummary -LogRatios $logRatios `
727+
-Iterations $Iterations -RandomSeed $RandomSeed
728+
}
679729
$medianRatio = [math]::Exp($bootstrap.Median)
680730
$lowerRatio = [math]::Exp($bootstrap.Lower)
681731
$upperRatio = [math]::Exp($bootstrap.Upper)
@@ -688,6 +738,12 @@ function Get-ScenarioResult {
688738
$improvementLower = ($lowerRatio - 1.0) * 100.0
689739
$improvementUpper = ($upperRatio - 1.0) * 100.0
690740
}
741+
$reportedPairs = New-Object 'System.Collections.Generic.List[object]'
742+
if (-not $UseIndependentSamples) {
743+
foreach ($pair in $pairs) {
744+
$reportedPairs.Add($pair)
745+
}
746+
}
691747

692748
return [ordered]@{
693749
scenario = $ScenarioName
@@ -707,12 +763,13 @@ function Get-ScenarioResult {
707763
}
708764
captureMethod = $captureMethods[0]
709765
runCount = $Runs.Count
710-
pairCount = $pairs.Count
766+
samplingMode = if ($UseIndependentSamples) { "independent-runners" } else { "paired-adjacent" }
767+
pairCount = if ($UseIndependentSamples) { 0 } else { $pairs.Count }
711768
medianBRatioToA = Round-Metric $medianRatio
712769
ratioBootstrap95Ci = @((Round-Metric $lowerRatio), (Round-Metric $upperRatio))
713770
improvementPercent = Round-Metric $improvement
714771
improvementBootstrap95CiPercent = @((Round-Metric $improvementLower), (Round-Metric $improvementUpper))
715-
pairs = @($pairs.ToArray() | ForEach-Object {
772+
pairs = @($reportedPairs.ToArray() | ForEach-Object {
716773
[ordered]@{
717774
block = $_.Block
718775
positions = $_.Positions
@@ -822,6 +879,15 @@ function Invoke-AnalyzerSelfTest {
822879
[math]::Abs($result.improvementPercent - 20.0) -gt 0.000001) {
823880
throw "Self-test paired log-ratio failed."
824881
}
882+
$independentResult = Get-ScenarioResult -Runs $runs -ScenarioName "SELFTEST" `
883+
-MetricPath "metric" -MetricDirection "LowerIsBetter" -PermitIncomplete $false `
884+
-UseIndependentSamples $true -Iterations 1000 -RandomSeed 7 -Prepared
885+
if ($independentResult.samplingMode -ne "independent-runners" -or
886+
$independentResult.pairCount -ne 0 -or
887+
$independentResult.pairs.Count -ne 0 -or
888+
[math]::Abs($independentResult.medianBRatioToA - 0.8) -gt 0.000001) {
889+
throw "Self-test independent A/B bootstrap failed."
890+
}
825891
if ($null -ne $result.abFactor -or $result.configSha256.Count -ne 0 -or
826892
$result.legacyTextComponentCache.provenancePresent) {
827893
throw "Self-test legacy manifest compatibility failed."
@@ -991,6 +1057,10 @@ function Invoke-AnalyzerSelfTest {
9911057
medianBRatioToA = $result.medianBRatioToA
9921058
ratioBootstrap95Ci = $result.ratioBootstrap95Ci
9931059
improvementPercent = $result.improvementPercent
1060+
independentSamplingModePassed = (
1061+
$independentResult.samplingMode -eq "independent-runners" -and
1062+
$independentResult.pairCount -eq 0
1063+
)
9941064
enforcesFormalOrder = $true
9951065
enforcesSameStackAndArtifacts = $true
9961066
rejectsJfrDataLoss = $rejectedJfrDataLoss
@@ -1035,6 +1105,7 @@ for ($index = 0; $index -lt $scenarioGroups.Count; $index++) {
10351105
$group = $scenarioGroups[$index]
10361106
$results.Add((Get-ScenarioResult -Runs @($group.Group) -ScenarioName $group.Name `
10371107
-MetricPath $Metric -MetricDirection $Direction -PermitIncomplete $AllowIncomplete.IsPresent `
1108+
-UseIndependentSamples $IndependentSamples.IsPresent `
10381109
-Iterations $BootstrapIterations -RandomSeed ($Seed + $index)))
10391110
}
10401111

@@ -1103,8 +1174,16 @@ Write-JsonResult ([ordered]@{
11031174
minimumSeconds = $MinimumSeconds
11041175
bootstrapIterations = $BootstrapIterations
11051176
seed = $Seed
1106-
pairing = "adjacent positions 1-2 and 3-4 within each restart-per-run block"
1107-
estimator = "exp(median(paired log(B/A)))"
1108-
confidenceInterval = "deterministic paired bootstrap percentile 95% CI"
1177+
pairing = if ($IndependentSamples) { $null } else {
1178+
"adjacent positions 1-2 and 3-4 within each restart-per-run block"
1179+
}
1180+
estimator = if ($IndependentSamples) { "median(B) / median(A)" } else {
1181+
"exp(median(paired log(B/A)))"
1182+
}
1183+
confidenceInterval = if ($IndependentSamples) {
1184+
"deterministic independent A/B bootstrap percentile 95% CI"
1185+
} else {
1186+
"deterministic paired bootstrap percentile 95% CI"
1187+
}
11091188
results = @($results.ToArray())
11101189
})

0 commit comments

Comments
 (0)