From 4d207a70fb794b37584c8943a21d6a72c65a8aab Mon Sep 17 00:00:00 2001 From: Arbousier1 Date: Thu, 16 Jul 2026 15:38:23 +0800 Subject: [PATCH] perf: benchmark table region key plans --- .github/workflows/ensure-labels.yml | 1 + .github/workflows/performance-ab.yml | 2 + docs/performance-testing.md | 2 + perf/ab/gate-config.json | 26 +++ .../mahjong/perf/RegionKeyPlanBenchmark.java | 206 ++++++++++++++++++ 5 files changed, 237 insertions(+) create mode 100644 src/perfTest/java/top/ellan/mahjong/perf/RegionKeyPlanBenchmark.java diff --git a/.github/workflows/ensure-labels.yml b/.github/workflows/ensure-labels.yml index 904f2c1..0fac515 100644 --- a/.github/workflows/ensure-labels.yml +++ b/.github/workflows/ensure-labels.yml @@ -28,6 +28,7 @@ jobs: { name: "performance-region-fingerprint", color: "bfdadc", description: "Use the table region fingerprint performance profile" }, { name: "performance-display-spawn", color: "bfdadc", description: "Use the display entity spawn performance profile" }, { name: "performance-region-queue", color: "bfdadc", description: "Use the region update queue performance profile" }, + { name: "performance-region-keys", color: "bfdadc", description: "Use the region key plan performance profile" }, { name: "performance-scheduler-reflection", color: "bfdadc", description: "Use the scheduler reflection performance profile" }, { name: "performance-ray-proxy", color: "bfdadc", description: "Use the client ray-proxy performance profile" } ]; diff --git a/.github/workflows/performance-ab.yml b/.github/workflows/performance-ab.yml index e3fced4..6b8a518 100644 --- a/.github/workflows/performance-ab.yml +++ b/.github/workflows/performance-ab.yml @@ -36,6 +36,7 @@ on: - region-fingerprint - display-spawn - region-queue + - region-keys - scheduler-reflection - ray-proxy - infra @@ -164,6 +165,7 @@ jobs: "performance-region-fingerprint": "region-fingerprint", "performance-display-spawn": "display-spawn", "performance-region-queue": "region-queue", + "performance-region-keys": "region-keys", "performance-scheduler-reflection": "scheduler-reflection", "performance-ray-proxy": "ray-proxy", } diff --git a/docs/performance-testing.md b/docs/performance-testing.md index 749784b..d5067ec 100644 --- a/docs/performance-testing.md +++ b/docs/performance-testing.md @@ -55,6 +55,8 @@ and exactly one profile label to a pure performance PR: - `performance-region-fingerprint` - a real started-table snapshot and layout, measuring the complete region map plus a batch of private/public hands, discards, melds and all 136 wall slots; +- `performance-region-keys` - all 452 dynamic keys requested while planning one complete + table refresh, with exact text, order, uniqueness and checksum sentinels outside timing; - `performance-scheduler-reflection` - representative global, region and entity scheduling bursts through both Folia-style reflective capabilities and standard Paper fallbacks; - `performance-ray-proxy` - 1, 4 and 32-viewer coordinator lifecycle and unchanged-geometry diff --git a/perf/ab/gate-config.json b/perf/ab/gate-config.json index 61328fb..0f70dd7 100644 --- a/perf/ab/gate-config.json +++ b/perf/ab/gate-config.json @@ -138,6 +138,20 @@ "top/ellan/mahjong/table/render/TableRegionDisplayCoordinator.class" ] }, + "region-keys": { + "include": "^top\\.ellan\\.mahjong\\.perf\\.RegionKeyPlanBenchmark\\.buildRepresentativeRegionKeyPlan$", + "benchmark_ids": [ + "region.keys.time", + "region.keys.alloc" + ], + "minimum_passes": 1, + "must_pass": [ + "region.keys.time" + ], + "required_classes": [ + "top/ellan/mahjong/table/render/TableRegionDisplayCoordinator.class" + ] + }, "scheduler-reflection": { "include": "^top\\.ellan\\.mahjong\\.perf\\.ServerSchedulerReflectionBenchmark\\.schedule(?:Folia|Paper)Burst$", "benchmark_ids": [ @@ -308,6 +322,18 @@ "metric": "gc.alloc.rate.norm", "direction": "lower" }, + { + "id": "region.keys.time", + "benchmark": "top.ellan.mahjong.perf.RegionKeyPlanBenchmark.buildRepresentativeRegionKeyPlan", + "metric": "primary", + "direction": "lower" + }, + { + "id": "region.keys.alloc", + "benchmark": "top.ellan.mahjong.perf.RegionKeyPlanBenchmark.buildRepresentativeRegionKeyPlan", + "metric": "gc.alloc.rate.norm", + "direction": "lower" + }, { "id": "scheduler.folia.time", "benchmark": "top.ellan.mahjong.perf.ServerSchedulerReflectionBenchmark.scheduleFoliaBurst", diff --git a/src/perfTest/java/top/ellan/mahjong/perf/RegionKeyPlanBenchmark.java b/src/perfTest/java/top/ellan/mahjong/perf/RegionKeyPlanBenchmark.java new file mode 100644 index 0000000..4df6cbb --- /dev/null +++ b/src/perfTest/java/top/ellan/mahjong/perf/RegionKeyPlanBenchmark.java @@ -0,0 +1,206 @@ +package top.ellan.mahjong.perf; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import top.ellan.mahjong.model.SeatWind; +import top.ellan.mahjong.table.render.TableRegionDisplayCoordinator; + +/** Measures every dynamic region key requested while planning one complete table refresh. */ +@State(Scope.Thread) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class RegionKeyPlanBenchmark { + private static final int WALL_TILE_REGIONS = 136; + private static final int HAND_TILE_REGIONS = 14; + private static final int DISCARD_TILE_REGIONS = 24; + private static final int MELD_TILE_REGIONS = 20; + private static final int EXPECTED_KEY_COUNT = 452; + private static final long CHECKSUM_SEED = 0x243F6A8885A308D3L; + private static final SeatWind[] WINDS = SeatWind.values(); + + private TableRegionDisplayCoordinator coordinator; + private MethodHandle seatRegionKey; + private MethodHandle handPrivateRegionKey; + private MethodHandle handPublicRegionKey; + private MethodHandle discardRegionKey; + private MethodHandle meldRegionKey; + private MethodHandle wallRegionKey; + private List expectedKeys; + private long expectedChecksum; + + @Setup(Level.Trial) + public void setUp() throws Throwable { + this.coordinator = new TableRegionDisplayCoordinator(null, null); + MethodHandles.Lookup lookup = MethodHandles.privateLookupIn( + TableRegionDisplayCoordinator.class, + MethodHandles.lookup() + ); + this.seatRegionKey = lookup.findVirtual( + TableRegionDisplayCoordinator.class, + "seatRegionKey", + MethodType.methodType(String.class, String.class, SeatWind.class) + ); + this.handPrivateRegionKey = indexedKeyHandle(lookup, "handPrivateRegionKey"); + this.handPublicRegionKey = indexedKeyHandle(lookup, "handPublicRegionKey"); + this.discardRegionKey = indexedKeyHandle(lookup, "discardRegionKey"); + this.meldRegionKey = indexedKeyHandle(lookup, "meldRegionKey"); + this.wallRegionKey = lookup.findVirtual( + TableRegionDisplayCoordinator.class, + "wallRegionKey", + MethodType.methodType(String.class, int.class) + ); + this.expectedKeys = referenceKeys(); + this.expectedChecksum = checksum(this.expectedKeys); + this.verifyRegionKeyContract(); + } + + @Setup(Level.Iteration) + public void verifyRegionKeyContract() throws Throwable { + List actual = this.actualKeys(); + if (!this.expectedKeys.equals(actual)) { + throw new IllegalStateException("Region key plan changed"); + } + Set unique = new HashSet<>(actual); + if (actual.size() != EXPECTED_KEY_COUNT || unique.size() != EXPECTED_KEY_COUNT) { + throw new IllegalStateException( + "Expected " + EXPECTED_KEY_COUNT + " distinct region keys, got " + actual.size() + + " entries and " + unique.size() + " distinct keys" + ); + } + long actualChecksum = this.buildRepresentativeRegionKeyPlan(); + if (actualChecksum != this.expectedChecksum) { + throw new IllegalStateException( + "Region key checksum changed: expected=" + this.expectedChecksum + ", actual=" + actualChecksum + ); + } + } + + @Benchmark + public long buildRepresentativeRegionKeyPlan() throws Throwable { + long checksum = CHECKSUM_SEED; + for (int index = 0; index < WALL_TILE_REGIONS; index++) { + checksum = mix(checksum, (String) this.wallRegionKey.invokeExact(this.coordinator, index)); + } + for (SeatWind wind : WINDS) { + checksum = mix(checksum, this.invokeSeatRegionKey("visual", wind)); + checksum = mix(checksum, this.invokeSeatRegionKey("labels", wind)); + checksum = mix(checksum, this.invokeSeatRegionKey("sticks", wind)); + checksum = mix(checksum, this.invokeSeatRegionKey("hand-public", wind)); + for (int index = 0; index < HAND_TILE_REGIONS; index++) { + checksum = mix(checksum, this.invokeIndexedKey(this.handPublicRegionKey, wind, index)); + } + checksum = mix(checksum, this.invokeSeatRegionKey("hand-private", wind)); + for (int index = 0; index < HAND_TILE_REGIONS; index++) { + checksum = mix(checksum, this.invokeIndexedKey(this.handPrivateRegionKey, wind, index)); + } + checksum = mix(checksum, this.invokeSeatRegionKey("discards", wind)); + for (int index = 0; index < DISCARD_TILE_REGIONS; index++) { + checksum = mix(checksum, this.invokeIndexedKey(this.discardRegionKey, wind, index)); + } + checksum = mix(checksum, this.invokeSeatRegionKey("melds", wind)); + for (int index = 0; index < MELD_TILE_REGIONS; index++) { + checksum = mix(checksum, this.invokeIndexedKey(this.meldRegionKey, wind, index)); + } + } + return checksum; + } + + private List actualKeys() throws Throwable { + List keys = new ArrayList<>(EXPECTED_KEY_COUNT); + for (int index = 0; index < WALL_TILE_REGIONS; index++) { + keys.add((String) this.wallRegionKey.invokeExact(this.coordinator, index)); + } + for (SeatWind wind : WINDS) { + keys.add(this.invokeSeatRegionKey("visual", wind)); + keys.add(this.invokeSeatRegionKey("labels", wind)); + keys.add(this.invokeSeatRegionKey("sticks", wind)); + keys.add(this.invokeSeatRegionKey("hand-public", wind)); + appendIndexedKeys(keys, this.handPublicRegionKey, wind, HAND_TILE_REGIONS); + keys.add(this.invokeSeatRegionKey("hand-private", wind)); + appendIndexedKeys(keys, this.handPrivateRegionKey, wind, HAND_TILE_REGIONS); + keys.add(this.invokeSeatRegionKey("discards", wind)); + appendIndexedKeys(keys, this.discardRegionKey, wind, DISCARD_TILE_REGIONS); + keys.add(this.invokeSeatRegionKey("melds", wind)); + appendIndexedKeys(keys, this.meldRegionKey, wind, MELD_TILE_REGIONS); + } + return keys; + } + + private String invokeSeatRegionKey(String region, SeatWind wind) throws Throwable { + return (String) this.seatRegionKey.invokeExact(this.coordinator, region, wind); + } + + private String invokeIndexedKey(MethodHandle handle, SeatWind wind, int index) throws Throwable { + return (String) handle.invokeExact(this.coordinator, wind, index); + } + + private void appendIndexedKeys(List keys, MethodHandle handle, SeatWind wind, int count) + throws Throwable { + for (int index = 0; index < count; index++) { + keys.add(this.invokeIndexedKey(handle, wind, index)); + } + } + + private static MethodHandle indexedKeyHandle(MethodHandles.Lookup lookup, String methodName) + throws NoSuchMethodException, IllegalAccessException { + return lookup.findVirtual( + TableRegionDisplayCoordinator.class, + methodName, + MethodType.methodType(String.class, SeatWind.class, int.class) + ); + } + + private static List referenceKeys() { + List keys = new ArrayList<>(EXPECTED_KEY_COUNT); + for (int index = 0; index < WALL_TILE_REGIONS; index++) { + keys.add("wall-" + index); + } + for (SeatWind wind : WINDS) { + String suffix = ":" + wind.name(); + keys.add("visual" + suffix); + keys.add("labels" + suffix); + keys.add("sticks" + suffix); + keys.add("hand-public" + suffix); + appendReferenceIndexedKeys(keys, "hand-public-", suffix, HAND_TILE_REGIONS); + keys.add("hand-private" + suffix); + appendReferenceIndexedKeys(keys, "hand-private-", suffix, HAND_TILE_REGIONS); + keys.add("discards" + suffix); + appendReferenceIndexedKeys(keys, "discards-", suffix, DISCARD_TILE_REGIONS); + keys.add("melds" + suffix); + appendReferenceIndexedKeys(keys, "melds-", suffix, MELD_TILE_REGIONS); + } + return List.copyOf(keys); + } + + private static void appendReferenceIndexedKeys(List keys, String prefix, String suffix, int count) { + for (int index = 0; index < count; index++) { + keys.add(prefix + index + suffix); + } + } + + private static long checksum(List keys) { + long checksum = CHECKSUM_SEED; + for (String key : keys) { + checksum = mix(checksum, key); + } + return checksum; + } + + private static long mix(long checksum, String key) { + return Long.rotateLeft(checksum, 7) ^ key.hashCode(); + } +}