Skip to content

Commit 4d207a7

Browse files
committed
perf: benchmark table region key plans
1 parent f60e9bf commit 4d207a7

5 files changed

Lines changed: 237 additions & 0 deletions

File tree

.github/workflows/ensure-labels.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ jobs:
2828
{ name: "performance-region-fingerprint", color: "bfdadc", description: "Use the table region fingerprint performance profile" },
2929
{ name: "performance-display-spawn", color: "bfdadc", description: "Use the display entity spawn performance profile" },
3030
{ name: "performance-region-queue", color: "bfdadc", description: "Use the region update queue performance profile" },
31+
{ name: "performance-region-keys", color: "bfdadc", description: "Use the region key plan performance profile" },
3132
{ name: "performance-scheduler-reflection", color: "bfdadc", description: "Use the scheduler reflection performance profile" },
3233
{ name: "performance-ray-proxy", color: "bfdadc", description: "Use the client ray-proxy performance profile" }
3334
];

.github/workflows/performance-ab.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ on:
3636
- region-fingerprint
3737
- display-spawn
3838
- region-queue
39+
- region-keys
3940
- scheduler-reflection
4041
- ray-proxy
4142
- infra
@@ -164,6 +165,7 @@ jobs:
164165
"performance-region-fingerprint": "region-fingerprint",
165166
"performance-display-spawn": "display-spawn",
166167
"performance-region-queue": "region-queue",
168+
"performance-region-keys": "region-keys",
167169
"performance-scheduler-reflection": "scheduler-reflection",
168170
"performance-ray-proxy": "ray-proxy",
169171
}

docs/performance-testing.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ and exactly one profile label to a pure performance PR:
5555
- `performance-region-fingerprint` - a real started-table snapshot and layout, measuring the
5656
complete region map plus a batch of private/public hands, discards, melds and all 136 wall
5757
slots;
58+
- `performance-region-keys` - all 452 dynamic keys requested while planning one complete
59+
table refresh, with exact text, order, uniqueness and checksum sentinels outside timing;
5860
- `performance-scheduler-reflection` - representative global, region and entity scheduling
5961
bursts through both Folia-style reflective capabilities and standard Paper fallbacks;
6062
- `performance-ray-proxy` - 1, 4 and 32-viewer coordinator lifecycle and unchanged-geometry

perf/ab/gate-config.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,20 @@
138138
"top/ellan/mahjong/table/render/TableRegionDisplayCoordinator.class"
139139
]
140140
},
141+
"region-keys": {
142+
"include": "^top\\.ellan\\.mahjong\\.perf\\.RegionKeyPlanBenchmark\\.buildRepresentativeRegionKeyPlan$",
143+
"benchmark_ids": [
144+
"region.keys.time",
145+
"region.keys.alloc"
146+
],
147+
"minimum_passes": 1,
148+
"must_pass": [
149+
"region.keys.time"
150+
],
151+
"required_classes": [
152+
"top/ellan/mahjong/table/render/TableRegionDisplayCoordinator.class"
153+
]
154+
},
141155
"scheduler-reflection": {
142156
"include": "^top\\.ellan\\.mahjong\\.perf\\.ServerSchedulerReflectionBenchmark\\.schedule(?:Folia|Paper)Burst$",
143157
"benchmark_ids": [
@@ -308,6 +322,18 @@
308322
"metric": "gc.alloc.rate.norm",
309323
"direction": "lower"
310324
},
325+
{
326+
"id": "region.keys.time",
327+
"benchmark": "top.ellan.mahjong.perf.RegionKeyPlanBenchmark.buildRepresentativeRegionKeyPlan",
328+
"metric": "primary",
329+
"direction": "lower"
330+
},
331+
{
332+
"id": "region.keys.alloc",
333+
"benchmark": "top.ellan.mahjong.perf.RegionKeyPlanBenchmark.buildRepresentativeRegionKeyPlan",
334+
"metric": "gc.alloc.rate.norm",
335+
"direction": "lower"
336+
},
311337
{
312338
"id": "scheduler.folia.time",
313339
"benchmark": "top.ellan.mahjong.perf.ServerSchedulerReflectionBenchmark.scheduleFoliaBurst",
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package top.ellan.mahjong.perf;
2+
3+
import java.lang.invoke.MethodHandle;
4+
import java.lang.invoke.MethodHandles;
5+
import java.lang.invoke.MethodType;
6+
import java.util.ArrayList;
7+
import java.util.HashSet;
8+
import java.util.List;
9+
import java.util.Set;
10+
import java.util.concurrent.TimeUnit;
11+
import org.openjdk.jmh.annotations.Benchmark;
12+
import org.openjdk.jmh.annotations.BenchmarkMode;
13+
import org.openjdk.jmh.annotations.Level;
14+
import org.openjdk.jmh.annotations.Mode;
15+
import org.openjdk.jmh.annotations.OutputTimeUnit;
16+
import org.openjdk.jmh.annotations.Scope;
17+
import org.openjdk.jmh.annotations.Setup;
18+
import org.openjdk.jmh.annotations.State;
19+
import top.ellan.mahjong.model.SeatWind;
20+
import top.ellan.mahjong.table.render.TableRegionDisplayCoordinator;
21+
22+
/** Measures every dynamic region key requested while planning one complete table refresh. */
23+
@State(Scope.Thread)
24+
@BenchmarkMode(Mode.AverageTime)
25+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
26+
public class RegionKeyPlanBenchmark {
27+
private static final int WALL_TILE_REGIONS = 136;
28+
private static final int HAND_TILE_REGIONS = 14;
29+
private static final int DISCARD_TILE_REGIONS = 24;
30+
private static final int MELD_TILE_REGIONS = 20;
31+
private static final int EXPECTED_KEY_COUNT = 452;
32+
private static final long CHECKSUM_SEED = 0x243F6A8885A308D3L;
33+
private static final SeatWind[] WINDS = SeatWind.values();
34+
35+
private TableRegionDisplayCoordinator coordinator;
36+
private MethodHandle seatRegionKey;
37+
private MethodHandle handPrivateRegionKey;
38+
private MethodHandle handPublicRegionKey;
39+
private MethodHandle discardRegionKey;
40+
private MethodHandle meldRegionKey;
41+
private MethodHandle wallRegionKey;
42+
private List<String> expectedKeys;
43+
private long expectedChecksum;
44+
45+
@Setup(Level.Trial)
46+
public void setUp() throws Throwable {
47+
this.coordinator = new TableRegionDisplayCoordinator(null, null);
48+
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(
49+
TableRegionDisplayCoordinator.class,
50+
MethodHandles.lookup()
51+
);
52+
this.seatRegionKey = lookup.findVirtual(
53+
TableRegionDisplayCoordinator.class,
54+
"seatRegionKey",
55+
MethodType.methodType(String.class, String.class, SeatWind.class)
56+
);
57+
this.handPrivateRegionKey = indexedKeyHandle(lookup, "handPrivateRegionKey");
58+
this.handPublicRegionKey = indexedKeyHandle(lookup, "handPublicRegionKey");
59+
this.discardRegionKey = indexedKeyHandle(lookup, "discardRegionKey");
60+
this.meldRegionKey = indexedKeyHandle(lookup, "meldRegionKey");
61+
this.wallRegionKey = lookup.findVirtual(
62+
TableRegionDisplayCoordinator.class,
63+
"wallRegionKey",
64+
MethodType.methodType(String.class, int.class)
65+
);
66+
this.expectedKeys = referenceKeys();
67+
this.expectedChecksum = checksum(this.expectedKeys);
68+
this.verifyRegionKeyContract();
69+
}
70+
71+
@Setup(Level.Iteration)
72+
public void verifyRegionKeyContract() throws Throwable {
73+
List<String> actual = this.actualKeys();
74+
if (!this.expectedKeys.equals(actual)) {
75+
throw new IllegalStateException("Region key plan changed");
76+
}
77+
Set<String> unique = new HashSet<>(actual);
78+
if (actual.size() != EXPECTED_KEY_COUNT || unique.size() != EXPECTED_KEY_COUNT) {
79+
throw new IllegalStateException(
80+
"Expected " + EXPECTED_KEY_COUNT + " distinct region keys, got " + actual.size()
81+
+ " entries and " + unique.size() + " distinct keys"
82+
);
83+
}
84+
long actualChecksum = this.buildRepresentativeRegionKeyPlan();
85+
if (actualChecksum != this.expectedChecksum) {
86+
throw new IllegalStateException(
87+
"Region key checksum changed: expected=" + this.expectedChecksum + ", actual=" + actualChecksum
88+
);
89+
}
90+
}
91+
92+
@Benchmark
93+
public long buildRepresentativeRegionKeyPlan() throws Throwable {
94+
long checksum = CHECKSUM_SEED;
95+
for (int index = 0; index < WALL_TILE_REGIONS; index++) {
96+
checksum = mix(checksum, (String) this.wallRegionKey.invokeExact(this.coordinator, index));
97+
}
98+
for (SeatWind wind : WINDS) {
99+
checksum = mix(checksum, this.invokeSeatRegionKey("visual", wind));
100+
checksum = mix(checksum, this.invokeSeatRegionKey("labels", wind));
101+
checksum = mix(checksum, this.invokeSeatRegionKey("sticks", wind));
102+
checksum = mix(checksum, this.invokeSeatRegionKey("hand-public", wind));
103+
for (int index = 0; index < HAND_TILE_REGIONS; index++) {
104+
checksum = mix(checksum, this.invokeIndexedKey(this.handPublicRegionKey, wind, index));
105+
}
106+
checksum = mix(checksum, this.invokeSeatRegionKey("hand-private", wind));
107+
for (int index = 0; index < HAND_TILE_REGIONS; index++) {
108+
checksum = mix(checksum, this.invokeIndexedKey(this.handPrivateRegionKey, wind, index));
109+
}
110+
checksum = mix(checksum, this.invokeSeatRegionKey("discards", wind));
111+
for (int index = 0; index < DISCARD_TILE_REGIONS; index++) {
112+
checksum = mix(checksum, this.invokeIndexedKey(this.discardRegionKey, wind, index));
113+
}
114+
checksum = mix(checksum, this.invokeSeatRegionKey("melds", wind));
115+
for (int index = 0; index < MELD_TILE_REGIONS; index++) {
116+
checksum = mix(checksum, this.invokeIndexedKey(this.meldRegionKey, wind, index));
117+
}
118+
}
119+
return checksum;
120+
}
121+
122+
private List<String> actualKeys() throws Throwable {
123+
List<String> keys = new ArrayList<>(EXPECTED_KEY_COUNT);
124+
for (int index = 0; index < WALL_TILE_REGIONS; index++) {
125+
keys.add((String) this.wallRegionKey.invokeExact(this.coordinator, index));
126+
}
127+
for (SeatWind wind : WINDS) {
128+
keys.add(this.invokeSeatRegionKey("visual", wind));
129+
keys.add(this.invokeSeatRegionKey("labels", wind));
130+
keys.add(this.invokeSeatRegionKey("sticks", wind));
131+
keys.add(this.invokeSeatRegionKey("hand-public", wind));
132+
appendIndexedKeys(keys, this.handPublicRegionKey, wind, HAND_TILE_REGIONS);
133+
keys.add(this.invokeSeatRegionKey("hand-private", wind));
134+
appendIndexedKeys(keys, this.handPrivateRegionKey, wind, HAND_TILE_REGIONS);
135+
keys.add(this.invokeSeatRegionKey("discards", wind));
136+
appendIndexedKeys(keys, this.discardRegionKey, wind, DISCARD_TILE_REGIONS);
137+
keys.add(this.invokeSeatRegionKey("melds", wind));
138+
appendIndexedKeys(keys, this.meldRegionKey, wind, MELD_TILE_REGIONS);
139+
}
140+
return keys;
141+
}
142+
143+
private String invokeSeatRegionKey(String region, SeatWind wind) throws Throwable {
144+
return (String) this.seatRegionKey.invokeExact(this.coordinator, region, wind);
145+
}
146+
147+
private String invokeIndexedKey(MethodHandle handle, SeatWind wind, int index) throws Throwable {
148+
return (String) handle.invokeExact(this.coordinator, wind, index);
149+
}
150+
151+
private void appendIndexedKeys(List<String> keys, MethodHandle handle, SeatWind wind, int count)
152+
throws Throwable {
153+
for (int index = 0; index < count; index++) {
154+
keys.add(this.invokeIndexedKey(handle, wind, index));
155+
}
156+
}
157+
158+
private static MethodHandle indexedKeyHandle(MethodHandles.Lookup lookup, String methodName)
159+
throws NoSuchMethodException, IllegalAccessException {
160+
return lookup.findVirtual(
161+
TableRegionDisplayCoordinator.class,
162+
methodName,
163+
MethodType.methodType(String.class, SeatWind.class, int.class)
164+
);
165+
}
166+
167+
private static List<String> referenceKeys() {
168+
List<String> keys = new ArrayList<>(EXPECTED_KEY_COUNT);
169+
for (int index = 0; index < WALL_TILE_REGIONS; index++) {
170+
keys.add("wall-" + index);
171+
}
172+
for (SeatWind wind : WINDS) {
173+
String suffix = ":" + wind.name();
174+
keys.add("visual" + suffix);
175+
keys.add("labels" + suffix);
176+
keys.add("sticks" + suffix);
177+
keys.add("hand-public" + suffix);
178+
appendReferenceIndexedKeys(keys, "hand-public-", suffix, HAND_TILE_REGIONS);
179+
keys.add("hand-private" + suffix);
180+
appendReferenceIndexedKeys(keys, "hand-private-", suffix, HAND_TILE_REGIONS);
181+
keys.add("discards" + suffix);
182+
appendReferenceIndexedKeys(keys, "discards-", suffix, DISCARD_TILE_REGIONS);
183+
keys.add("melds" + suffix);
184+
appendReferenceIndexedKeys(keys, "melds-", suffix, MELD_TILE_REGIONS);
185+
}
186+
return List.copyOf(keys);
187+
}
188+
189+
private static void appendReferenceIndexedKeys(List<String> keys, String prefix, String suffix, int count) {
190+
for (int index = 0; index < count; index++) {
191+
keys.add(prefix + index + suffix);
192+
}
193+
}
194+
195+
private static long checksum(List<String> keys) {
196+
long checksum = CHECKSUM_SEED;
197+
for (String key : keys) {
198+
checksum = mix(checksum, key);
199+
}
200+
return checksum;
201+
}
202+
203+
private static long mix(long checksum, String key) {
204+
return Long.rotateLeft(checksum, 7) ^ key.hashCode();
205+
}
206+
}

0 commit comments

Comments
 (0)