|
| 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