|
| 1 | +package top.ellan.mahjong.perf; |
| 2 | + |
| 3 | +import java.lang.reflect.InvocationHandler; |
| 4 | +import java.lang.reflect.Method; |
| 5 | +import java.lang.reflect.Proxy; |
| 6 | +import java.util.concurrent.TimeUnit; |
| 7 | +import java.util.function.Consumer; |
| 8 | +import org.bukkit.Location; |
| 9 | +import org.bukkit.Server; |
| 10 | +import org.bukkit.World; |
| 11 | +import org.bukkit.entity.Entity; |
| 12 | +import org.bukkit.plugin.Plugin; |
| 13 | +import org.bukkit.scheduler.BukkitScheduler; |
| 14 | +import org.bukkit.scheduler.BukkitTask; |
| 15 | +import org.openjdk.jmh.annotations.Benchmark; |
| 16 | +import org.openjdk.jmh.annotations.BenchmarkMode; |
| 17 | +import org.openjdk.jmh.annotations.Level; |
| 18 | +import org.openjdk.jmh.annotations.Mode; |
| 19 | +import org.openjdk.jmh.annotations.OperationsPerInvocation; |
| 20 | +import org.openjdk.jmh.annotations.OutputTimeUnit; |
| 21 | +import org.openjdk.jmh.annotations.Scope; |
| 22 | +import org.openjdk.jmh.annotations.Setup; |
| 23 | +import org.openjdk.jmh.annotations.State; |
| 24 | +import org.openjdk.jmh.infra.Blackhole; |
| 25 | +import top.ellan.mahjong.runtime.PluginTask; |
| 26 | +import top.ellan.mahjong.runtime.ServerScheduler; |
| 27 | + |
| 28 | +/** Measures the reflective scheduler-capability dispatch used across Paper and Folia. */ |
| 29 | +@State(Scope.Thread) |
| 30 | +@BenchmarkMode(Mode.AverageTime) |
| 31 | +@OutputTimeUnit(TimeUnit.NANOSECONDS) |
| 32 | +public class ServerSchedulerReflectionBenchmark { |
| 33 | + private static final int GLOBAL_TASKS = 4; |
| 34 | + private static final int REGION_TASKS = 16; |
| 35 | + private static final int ENTITY_TASKS = 64; |
| 36 | + private static final int REPRESENTATIVE_TASKS = GLOBAL_TASKS + REGION_TASKS + ENTITY_TASKS; |
| 37 | + private static final Runnable NO_OP = () -> { |
| 38 | + }; |
| 39 | + |
| 40 | + private Harness folia; |
| 41 | + private Harness paper; |
| 42 | + private Location location; |
| 43 | + |
| 44 | + @Setup(Level.Trial) |
| 45 | + public void setUp() { |
| 46 | + World world = proxy(World.class, ServerSchedulerReflectionBenchmark::unsupported); |
| 47 | + this.location = new Location(world, 8.0, 64.0, -8.0); |
| 48 | + this.folia = createFoliaHarness(); |
| 49 | + this.paper = createPaperHarness(); |
| 50 | + this.verifyContract(); |
| 51 | + } |
| 52 | + |
| 53 | + @Benchmark |
| 54 | + @OperationsPerInvocation(REPRESENTATIVE_TASKS) |
| 55 | + public void scheduleFoliaBurst(Blackhole blackhole) { |
| 56 | + this.scheduleBurst(this.folia, blackhole); |
| 57 | + } |
| 58 | + |
| 59 | + @Benchmark |
| 60 | + @OperationsPerInvocation(REPRESENTATIVE_TASKS) |
| 61 | + public void schedulePaperBurst(Blackhole blackhole) { |
| 62 | + this.scheduleBurst(this.paper, blackhole); |
| 63 | + } |
| 64 | + |
| 65 | + private void scheduleBurst(Harness harness, Blackhole blackhole) { |
| 66 | + for (int index = 0; index < GLOBAL_TASKS; index++) { |
| 67 | + blackhole.consume(harness.scheduler().runGlobal(NO_OP)); |
| 68 | + } |
| 69 | + for (int index = 0; index < REGION_TASKS; index++) { |
| 70 | + blackhole.consume(harness.scheduler().runRegion(this.location, NO_OP)); |
| 71 | + } |
| 72 | + for (int index = 0; index < ENTITY_TASKS; index++) { |
| 73 | + blackhole.consume(harness.scheduler().runEntity(harness.entity(), NO_OP)); |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + private void verifyContract() { |
| 78 | + this.verifyHarness(this.folia, true); |
| 79 | + this.verifyHarness(this.paper, false); |
| 80 | + } |
| 81 | + |
| 82 | + private void verifyHarness(Harness harness, boolean expectFolia) { |
| 83 | + int foliaBefore = harness.foliaScheduler().invocations(); |
| 84 | + int paperBefore = harness.paperInvocations().value; |
| 85 | + assertActive(harness.scheduler().runGlobal(NO_OP)); |
| 86 | + assertActive(harness.scheduler().runRegion(this.location, NO_OP)); |
| 87 | + assertActive(harness.scheduler().runEntity(harness.entity(), NO_OP)); |
| 88 | + int foliaDelta = harness.foliaScheduler().invocations() - foliaBefore; |
| 89 | + int paperDelta = harness.paperInvocations().value - paperBefore; |
| 90 | + if (expectFolia ? foliaDelta != 3 || paperDelta != 0 : foliaDelta != 0 || paperDelta != 3) { |
| 91 | + throw new IllegalStateException( |
| 92 | + "Scheduler route changed: folia=" + foliaDelta + ", paper=" + paperDelta |
| 93 | + ); |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + private static void assertActive(PluginTask task) { |
| 98 | + if (task == null || task.isCancelled()) { |
| 99 | + throw new IllegalStateException("Scheduler must return an active task handle"); |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + private static Harness createFoliaHarness() { |
| 104 | + FakeFoliaScheduler foliaScheduler = new FakeFoliaScheduler(); |
| 105 | + Counter paperInvocations = new Counter(); |
| 106 | + BukkitScheduler bukkitScheduler = bukkitScheduler(paperInvocations); |
| 107 | + Server server = proxy( |
| 108 | + Server.class, |
| 109 | + (instance, method, arguments) -> switch (method.getName()) { |
| 110 | + case "getGlobalRegionScheduler", "getRegionScheduler" -> foliaScheduler; |
| 111 | + case "getScheduler" -> bukkitScheduler; |
| 112 | + default -> unsupported(instance, method, arguments); |
| 113 | + }, |
| 114 | + FoliaServerAccess.class |
| 115 | + ); |
| 116 | + Plugin plugin = plugin(server); |
| 117 | + Entity entity = proxy( |
| 118 | + Entity.class, |
| 119 | + (instance, method, arguments) -> { |
| 120 | + if (method.getName().equals("getScheduler") && method.getParameterCount() == 0) { |
| 121 | + return foliaScheduler; |
| 122 | + } |
| 123 | + return unsupported(instance, method, arguments); |
| 124 | + }, |
| 125 | + FoliaEntityAccess.class |
| 126 | + ); |
| 127 | + return new Harness(new ServerScheduler(plugin), entity, foliaScheduler, paperInvocations); |
| 128 | + } |
| 129 | + |
| 130 | + private static Harness createPaperHarness() { |
| 131 | + FakeFoliaScheduler foliaScheduler = new FakeFoliaScheduler(); |
| 132 | + Counter paperInvocations = new Counter(); |
| 133 | + BukkitScheduler bukkitScheduler = bukkitScheduler(paperInvocations); |
| 134 | + Server server = proxy( |
| 135 | + Server.class, |
| 136 | + (instance, method, arguments) -> { |
| 137 | + if (method.getName().equals("getScheduler") && method.getParameterCount() == 0) { |
| 138 | + return bukkitScheduler; |
| 139 | + } |
| 140 | + return unsupported(instance, method, arguments); |
| 141 | + } |
| 142 | + ); |
| 143 | + Plugin plugin = plugin(server); |
| 144 | + Entity entity = proxy( |
| 145 | + Entity.class, |
| 146 | + (instance, method, arguments) -> { |
| 147 | + if (method.getName().equals("getScheduler") && method.getParameterCount() == 0) { |
| 148 | + return null; |
| 149 | + } |
| 150 | + return unsupported(instance, method, arguments); |
| 151 | + } |
| 152 | + ); |
| 153 | + return new Harness(new ServerScheduler(plugin), entity, foliaScheduler, paperInvocations); |
| 154 | + } |
| 155 | + |
| 156 | + private static Plugin plugin(Server server) { |
| 157 | + return proxy( |
| 158 | + Plugin.class, |
| 159 | + (instance, method, arguments) -> switch (method.getName()) { |
| 160 | + case "isEnabled" -> true; |
| 161 | + case "getServer" -> server; |
| 162 | + default -> unsupported(instance, method, arguments); |
| 163 | + } |
| 164 | + ); |
| 165 | + } |
| 166 | + |
| 167 | + private static BukkitScheduler bukkitScheduler(Counter invocations) { |
| 168 | + BukkitTask task = proxy( |
| 169 | + BukkitTask.class, |
| 170 | + (instance, method, arguments) -> { |
| 171 | + if (method.getName().equals("isCancelled") && method.getParameterCount() == 0) { |
| 172 | + return false; |
| 173 | + } |
| 174 | + if (method.getName().equals("cancel") && method.getParameterCount() == 0) { |
| 175 | + return null; |
| 176 | + } |
| 177 | + return unsupported(instance, method, arguments); |
| 178 | + } |
| 179 | + ); |
| 180 | + return proxy( |
| 181 | + BukkitScheduler.class, |
| 182 | + (instance, method, arguments) -> { |
| 183 | + if (method.getName().startsWith("runTask")) { |
| 184 | + invocations.value++; |
| 185 | + return task; |
| 186 | + } |
| 187 | + return unsupported(instance, method, arguments); |
| 188 | + } |
| 189 | + ); |
| 190 | + } |
| 191 | + |
| 192 | + private static Object unsupported(Object instance, Method method, Object[] arguments) { |
| 193 | + if (method.getDeclaringClass() == Object.class) { |
| 194 | + return switch (method.getName()) { |
| 195 | + case "equals" -> instance == arguments[0]; |
| 196 | + case "hashCode" -> System.identityHashCode(instance); |
| 197 | + case "toString" -> instance.getClass().getInterfaces()[0].getSimpleName() + "Proxy"; |
| 198 | + default -> throw new UnsupportedOperationException(method.toString()); |
| 199 | + }; |
| 200 | + } |
| 201 | + throw new UnsupportedOperationException(method.toString()); |
| 202 | + } |
| 203 | + |
| 204 | + @SuppressWarnings("unchecked") |
| 205 | + private static <T> T proxy(Class<T> primary, InvocationHandler handler, Class<?>... additionalInterfaces) { |
| 206 | + Class<?>[] interfaces = new Class<?>[additionalInterfaces.length + 1]; |
| 207 | + interfaces[0] = primary; |
| 208 | + System.arraycopy(additionalInterfaces, 0, interfaces, 1, additionalInterfaces.length); |
| 209 | + return (T) Proxy.newProxyInstance( |
| 210 | + ServerSchedulerReflectionBenchmark.class.getClassLoader(), |
| 211 | + interfaces, |
| 212 | + handler |
| 213 | + ); |
| 214 | + } |
| 215 | + |
| 216 | + public interface FoliaServerAccess { |
| 217 | + Object getGlobalRegionScheduler(); |
| 218 | + |
| 219 | + Object getRegionScheduler(); |
| 220 | + } |
| 221 | + |
| 222 | + public interface FoliaEntityAccess { |
| 223 | + Object getScheduler(); |
| 224 | + } |
| 225 | + |
| 226 | + public static final class FakeFoliaScheduler { |
| 227 | + private final FakeScheduledTask task = new FakeScheduledTask(); |
| 228 | + private int invocations; |
| 229 | + |
| 230 | + public FakeScheduledTask run(Plugin plugin, Consumer<Object> consumer) { |
| 231 | + return this.invoked(); |
| 232 | + } |
| 233 | + |
| 234 | + public FakeScheduledTask run(Plugin plugin, Location location, Consumer<Object> consumer) { |
| 235 | + return this.invoked(); |
| 236 | + } |
| 237 | + |
| 238 | + public FakeScheduledTask run(Plugin plugin, Consumer<Object> consumer, Runnable retired) { |
| 239 | + return this.invoked(); |
| 240 | + } |
| 241 | + |
| 242 | + public int invocations() { |
| 243 | + return this.invocations; |
| 244 | + } |
| 245 | + |
| 246 | + private FakeScheduledTask invoked() { |
| 247 | + this.invocations++; |
| 248 | + return this.task; |
| 249 | + } |
| 250 | + } |
| 251 | + |
| 252 | + public static final class FakeScheduledTask { |
| 253 | + public void cancel() { |
| 254 | + } |
| 255 | + |
| 256 | + public boolean isCancelled() { |
| 257 | + return false; |
| 258 | + } |
| 259 | + } |
| 260 | + |
| 261 | + private static final class Counter { |
| 262 | + private int value; |
| 263 | + } |
| 264 | + |
| 265 | + private record Harness( |
| 266 | + ServerScheduler scheduler, |
| 267 | + Entity entity, |
| 268 | + FakeFoliaScheduler foliaScheduler, |
| 269 | + Counter paperInvocations |
| 270 | + ) { |
| 271 | + } |
| 272 | +} |
0 commit comments