Skip to content

Commit 817390d

Browse files
committed
perf: benchmark singleton display region spawns
1 parent 610535d commit 817390d

5 files changed

Lines changed: 234 additions & 0 deletions

File tree

.github/workflows/ensure-labels.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ jobs:
2626
{ name: "performance-snapshot", color: "bfdadc", description: "Use the viewer snapshot performance profile" },
2727
{ name: "performance-gb-bot", color: "bfdadc", description: "Use the GB bot decision performance profile" },
2828
{ name: "performance-region-fingerprint", color: "bfdadc", description: "Use the table region fingerprint performance profile" },
29+
{ name: "performance-display-spawn", color: "bfdadc", description: "Use the display entity spawn performance profile" },
2930
{ name: "performance-ray-proxy", color: "bfdadc", description: "Use the client ray-proxy performance profile" }
3031
];
3132

.github/workflows/performance-ab.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ on:
3434
- snapshot
3535
- gb-bot
3636
- region-fingerprint
37+
- display-spawn
3738
- ray-proxy
3839
- infra
3940
forks:
@@ -159,6 +160,7 @@ jobs:
159160
"performance-snapshot": "snapshot",
160161
"performance-gb-bot": "gb-bot",
161162
"performance-region-fingerprint": "region-fingerprint",
163+
"performance-display-spawn": "display-spawn",
162164
"performance-ray-proxy": "ray-proxy",
163165
}
164166
selected = [profile for label, profile in choices.items() if label in labels]

perf/ab/gate-config.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,20 @@
110110
"top/ellan/mahjong/table/render/TableRegionFingerprintService.class"
111111
]
112112
},
113+
"display-spawn": {
114+
"include": "^top\\.ellan\\.mahjong\\.perf\\.DisplayEntitiesSpawnAllBenchmark\\.spawnRepresentativeTableRegions$",
115+
"benchmark_ids": [
116+
"display.spawn.time",
117+
"display.spawn.alloc"
118+
],
119+
"minimum_passes": 2,
120+
"must_pass": [
121+
"display.spawn.time"
122+
],
123+
"required_classes": [
124+
"top/ellan/mahjong/render/display/DisplayEntities.class"
125+
]
126+
},
113127
"ray-proxy": {
114128
"include": "^top\\.ellan\\.mahjong\\.perf\\.RayProxyCoordinatorBenchmark\\.proxyPlan(?:1Viewer|4Viewers|32Viewers)$",
115129
"benchmark_ids": [
@@ -239,6 +253,18 @@
239253
"metric": "gc.alloc.rate.norm",
240254
"direction": "lower"
241255
},
256+
{
257+
"id": "display.spawn.time",
258+
"benchmark": "top.ellan.mahjong.perf.DisplayEntitiesSpawnAllBenchmark.spawnRepresentativeTableRegions",
259+
"metric": "primary",
260+
"direction": "lower"
261+
},
262+
{
263+
"id": "display.spawn.alloc",
264+
"benchmark": "top.ellan.mahjong.perf.DisplayEntitiesSpawnAllBenchmark.spawnRepresentativeTableRegions",
265+
"metric": "gc.alloc.rate.norm",
266+
"direction": "lower"
267+
},
242268
{
243269
"id": "ray.viewers1.time",
244270
"benchmark": "top.ellan.mahjong.perf.RayProxyCoordinatorBenchmark.proxyPlan1Viewer",
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package top.ellan.mahjong.perf;
2+
3+
import java.lang.reflect.Proxy;
4+
import java.util.Collection;
5+
import java.util.List;
6+
import java.util.concurrent.TimeUnit;
7+
import org.bukkit.entity.Entity;
8+
import org.bukkit.entity.Player;
9+
import org.bukkit.plugin.Plugin;
10+
import org.openjdk.jmh.annotations.Benchmark;
11+
import org.openjdk.jmh.annotations.BenchmarkMode;
12+
import org.openjdk.jmh.annotations.Level;
13+
import org.openjdk.jmh.annotations.Mode;
14+
import org.openjdk.jmh.annotations.OutputTimeUnit;
15+
import org.openjdk.jmh.annotations.Scope;
16+
import org.openjdk.jmh.annotations.Setup;
17+
import org.openjdk.jmh.annotations.State;
18+
import top.ellan.mahjong.render.display.DisplayEntities;
19+
import top.ellan.mahjong.render.display.DisplayEntityRuntime;
20+
21+
/** Measures the one-entity region shape used by hand, discard, meld and wall tiles. */
22+
@State(Scope.Thread)
23+
@BenchmarkMode(Mode.AverageTime)
24+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
25+
public class DisplayEntitiesSpawnAllBenchmark {
26+
private static final int REPRESENTATIVE_TABLE_REGION_COUNT = 312;
27+
28+
private Entity entity;
29+
private DisplayEntityRuntime runtime;
30+
private List<DisplayEntities.EntitySpec> singletonSpec;
31+
32+
@Setup(Level.Trial)
33+
public void setUp() {
34+
Plugin plugin = proxy(Plugin.class);
35+
this.entity = proxy(Entity.class);
36+
this.runtime = new EmptyViewerRuntime(plugin);
37+
this.singletonSpec = List.of(new ConstantEntitySpec(this.entity));
38+
this.verifyContract();
39+
}
40+
41+
@Benchmark
42+
public int spawnRepresentativeTableRegions() {
43+
int spawned = 0;
44+
for (int region = 0; region < REPRESENTATIVE_TABLE_REGION_COUNT; region++) {
45+
spawned += DisplayEntities.spawnAll(this.runtime, this.singletonSpec).size();
46+
}
47+
return spawned;
48+
}
49+
50+
private void verifyContract() {
51+
List<Entity> spawned = DisplayEntities.spawnAll(this.runtime, this.singletonSpec);
52+
if (spawned.size() != 1 || spawned.get(0) != this.entity) {
53+
throw new IllegalStateException("Singleton spawn contract changed");
54+
}
55+
try {
56+
spawned.add(this.entity);
57+
throw new IllegalStateException("spawnAll must return an immutable list");
58+
} catch (UnsupportedOperationException expected) {
59+
// Expected contract.
60+
}
61+
}
62+
63+
@SuppressWarnings("unchecked")
64+
private static <T> T proxy(Class<T> type) {
65+
return (T) Proxy.newProxyInstance(
66+
type.getClassLoader(),
67+
new Class<?>[] {type},
68+
(instance, method, arguments) -> {
69+
throw new UnsupportedOperationException(method.toString());
70+
}
71+
);
72+
}
73+
74+
private record EmptyViewerRuntime(Plugin bukkitPlugin) implements DisplayEntityRuntime {
75+
@Override
76+
public Collection<? extends Player> onlinePlayers() {
77+
return List.of();
78+
}
79+
}
80+
81+
private record ConstantEntitySpec(Entity entity) implements DisplayEntities.EntitySpec {
82+
@Override
83+
public Entity spawn(DisplayEntityRuntime runtime) {
84+
return this.entity;
85+
}
86+
87+
@Override
88+
public boolean canReuse(DisplayEntityRuntime runtime, Entity candidate) {
89+
return candidate == this.entity;
90+
}
91+
92+
@Override
93+
public void apply(DisplayEntityRuntime runtime, Entity candidate) {
94+
throw new UnsupportedOperationException("Benchmark does not reconcile entities");
95+
}
96+
}
97+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package top.ellan.mahjong.render.display;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertSame;
5+
import static org.junit.jupiter.api.Assertions.assertThrows;
6+
7+
import java.lang.reflect.Proxy;
8+
import java.util.Collection;
9+
import java.util.List;
10+
import org.bukkit.entity.Entity;
11+
import org.bukkit.entity.Player;
12+
import org.bukkit.plugin.Plugin;
13+
import org.junit.jupiter.api.Test;
14+
15+
class DisplayEntitiesSpawnAllContractTest {
16+
@Test
17+
void singletonSpawnReturnsTheSameEntityInAnImmutableList() {
18+
Entity entity = proxy(Entity.class);
19+
CountingRuntime runtime = new CountingRuntime(proxy(Plugin.class));
20+
21+
List<Entity> spawned = DisplayEntities.spawnAll(runtime, List.of(new ConstantEntitySpec(entity)));
22+
23+
assertEquals(1, spawned.size());
24+
assertSame(entity, spawned.get(0));
25+
assertEquals(1, runtime.onlinePlayerReads());
26+
assertThrows(UnsupportedOperationException.class, () -> spawned.add(entity));
27+
}
28+
29+
@Test
30+
void singletonNullSpawnReturnsAnImmutableEmptyList() {
31+
CountingRuntime runtime = new CountingRuntime(proxy(Plugin.class));
32+
33+
List<Entity> spawned = DisplayEntities.spawnAll(runtime, List.of(new ConstantEntitySpec(null)));
34+
35+
assertEquals(0, spawned.size());
36+
assertEquals(1, runtime.onlinePlayerReads());
37+
assertThrows(UnsupportedOperationException.class, () -> spawned.add(proxy(Entity.class)));
38+
}
39+
40+
@Test
41+
void multipleSpawnsPreserveOrderAndSkipNulls() {
42+
Entity first = proxy(Entity.class);
43+
Entity second = proxy(Entity.class);
44+
CountingRuntime runtime = new CountingRuntime(proxy(Plugin.class));
45+
46+
List<Entity> spawned = DisplayEntities.spawnAll(
47+
runtime,
48+
List.of(new ConstantEntitySpec(first), new ConstantEntitySpec(null), new ConstantEntitySpec(second))
49+
);
50+
51+
assertEquals(2, spawned.size());
52+
assertSame(first, spawned.get(0));
53+
assertSame(second, spawned.get(1));
54+
assertEquals(1, runtime.onlinePlayerReads());
55+
}
56+
57+
@SuppressWarnings("unchecked")
58+
private static <T> T proxy(Class<T> type) {
59+
return (T) Proxy.newProxyInstance(
60+
type.getClassLoader(),
61+
new Class<?>[] {type},
62+
(instance, method, arguments) -> {
63+
throw new UnsupportedOperationException(method.toString());
64+
}
65+
);
66+
}
67+
68+
private static final class CountingRuntime implements DisplayEntityRuntime {
69+
private final Plugin plugin;
70+
private int onlinePlayerReads;
71+
72+
private CountingRuntime(Plugin plugin) {
73+
this.plugin = plugin;
74+
}
75+
76+
@Override
77+
public Plugin bukkitPlugin() {
78+
return this.plugin;
79+
}
80+
81+
@Override
82+
public Collection<? extends Player> onlinePlayers() {
83+
this.onlinePlayerReads++;
84+
return List.of();
85+
}
86+
87+
private int onlinePlayerReads() {
88+
return this.onlinePlayerReads;
89+
}
90+
}
91+
92+
private record ConstantEntitySpec(Entity entity) implements DisplayEntities.EntitySpec {
93+
@Override
94+
public Entity spawn(DisplayEntityRuntime runtime) {
95+
return this.entity;
96+
}
97+
98+
@Override
99+
public boolean canReuse(DisplayEntityRuntime runtime, Entity candidate) {
100+
return candidate == this.entity;
101+
}
102+
103+
@Override
104+
public void apply(DisplayEntityRuntime runtime, Entity candidate) {
105+
throw new UnsupportedOperationException("Contract test does not reconcile entities");
106+
}
107+
}
108+
}

0 commit comments

Comments
 (0)