From c437d6b34558cb5aa18b4ed50d74da703232846c Mon Sep 17 00:00:00 2001 From: Arbousier1 Date: Sun, 19 Jul 2026 21:08:23 +0800 Subject: [PATCH] perf(scheduler): cache immutable dispatch plans Avoid repeated reflection lookups and flatten scheduler fallbacks while preserving identity-safe concurrent routing. --- .../mahjong/runtime/ServerScheduler.java | 580 ++++++++++++++---- .../ServerSchedulerDispatchPlanTest.java | 300 +++++++++ .../ServerSchedulerReflectionCacheTest.java | 16 + 3 files changed, 781 insertions(+), 115 deletions(-) create mode 100644 src/test/java/top/ellan/mahjong/runtime/ServerSchedulerDispatchPlanTest.java diff --git a/src/main/java/top/ellan/mahjong/runtime/ServerScheduler.java b/src/main/java/top/ellan/mahjong/runtime/ServerScheduler.java index f73747b..6046992 100644 --- a/src/main/java/top/ellan/mahjong/runtime/ServerScheduler.java +++ b/src/main/java/top/ellan/mahjong/runtime/ServerScheduler.java @@ -8,11 +8,18 @@ import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; -import net.momirealms.sparrow.reflection.method.SMethod; +import net.momirealms.sparrow.reflection.method.SMethod0; +import net.momirealms.sparrow.reflection.method.SMethod1; +import net.momirealms.sparrow.reflection.method.SMethod2; +import net.momirealms.sparrow.reflection.method.SMethod3; +import net.momirealms.sparrow.reflection.method.SMethod4; +import net.momirealms.sparrow.reflection.method.SMethod5; import net.momirealms.sparrow.reflection.method.SparrowMethod; import org.bukkit.Location; +import org.bukkit.Server; import org.bukkit.entity.Entity; import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitScheduler; import org.bukkit.scheduler.BukkitTask; public final class ServerScheduler { @@ -77,6 +84,38 @@ protected Cache computeValue(Class type) { return Caffeine.newBuilder().build(); } }; + private static final ClassValue GLOBAL_SCHEDULER_DISPATCH_PLAN_CACHE = + new ClassValue<>() { + @Override + protected GlobalSchedulerDispatchPlan computeValue(Class type) { + return new GlobalSchedulerDispatchPlan( + resolveMethod(type, RUN_GLOBAL), + resolveMethod(type, RUN_GLOBAL_DELAYED), + resolveMethod(type, RUN_GLOBAL_TIMER) + ); + } + }; + private static final ClassValue REGION_SCHEDULER_DISPATCH_PLAN_CACHE = + new ClassValue<>() { + @Override + protected RegionSchedulerDispatchPlan computeValue(Class type) { + return new RegionSchedulerDispatchPlan( + resolveMethod(type, RUN_REGION), + resolveMethod(type, RUN_REGION_DELAYED), + resolveMethod(type, RUN_REGION_TIMER) + ); + } + }; + private static final ClassValue ENTITY_SCHEDULER_DISPATCH_PLAN_CACHE = + new ClassValue<>() { + @Override + protected EntitySchedulerDispatchPlan computeValue(Class type) { + return new EntitySchedulerDispatchPlan( + resolveMethod(type, RUN_ENTITY), + resolveMethod(type, RUN_ENTITY_DELAYED) + ); + } + }; private static final PluginTask NO_OP_TASK = new PluginTask() { @Override @@ -93,13 +132,14 @@ public boolean isCancelled() { /** * Each capability is published as one immutable entry so readers can never - * observe a target from one resolution paired with another target's scheduler. - * The null scheduler value is intentional: it negatively caches unavailable - * Folia capabilities on standard Paper runtimes. + * observe a target from one resolution paired with another target's scheduler + * or invocation plan. Null scheduler values intentionally negative-cache + * unavailable Folia capabilities on standard Paper runtimes. */ - private volatile SchedulerCapability globalSchedulerCapability; - private volatile SchedulerCapability regionSchedulerCapability; - private volatile SchedulerCapability entitySchedulerCapability; + private volatile GlobalSchedulerCapability globalSchedulerCapability; + private volatile RegionSchedulerCapability regionSchedulerCapability; + private volatile EntitySchedulerCapability entitySchedulerCapability; + private volatile BukkitSchedulerCapability bukkitSchedulerCapability; public ServerScheduler(Plugin plugin) { this.plugin = Objects.requireNonNull(plugin, "plugin"); @@ -109,71 +149,32 @@ public PluginTask runGlobal(Runnable runnable) { if (runnable == null || !this.isPluginEnabled()) { return NO_OP_TASK; } - Object scheduler = this.globalRegionScheduler(); - if (scheduler != null) { - PluginTask task = this.invokeSchedulerTask( - scheduler, - RUN_GLOBAL, - this.plugin, - taskConsumer(runnable) - ); - if (task != null) { - return task; - } - } - return wrap(this.plugin.getServer().getScheduler().runTask(this.plugin, runnable)); + return this.runGlobalValidated(this.plugin.getServer(), runnable); } public PluginTask runGlobalDelayed(Runnable runnable, long delayTicks) { if (runnable == null || !this.isPluginEnabled()) { return NO_OP_TASK; } - Object scheduler = this.globalRegionScheduler(); - if (scheduler != null) { - PluginTask task = this.invokeSchedulerTask( - scheduler, - RUN_GLOBAL_DELAYED, - this.plugin, - taskConsumer(runnable), - delayTicks - ); - if (task != null) { - return task; - } - } - return wrap(this.plugin.getServer().getScheduler().runTaskLater(this.plugin, runnable, delayTicks)); + return this.runGlobalDelayedValidated(this.plugin.getServer(), runnable, delayTicks); } public PluginTask runGlobalTimer(Runnable runnable, long delayTicks, long periodTicks) { if (runnable == null || !this.isPluginEnabled()) { return NO_OP_TASK; } - Object scheduler = this.globalRegionScheduler(); - if (scheduler != null) { - PluginTask task = this.invokeSchedulerTask( - scheduler, - RUN_GLOBAL_TIMER, - this.plugin, - taskConsumer(runnable), - delayTicks, - periodTicks - ); - if (task != null) { - return task; - } - } - return wrap(this.plugin.getServer().getScheduler().runTaskTimer(this.plugin, runnable, delayTicks, periodTicks)); + return this.runGlobalTimerValidated(this.plugin.getServer(), runnable, delayTicks, periodTicks); } public PluginTask runRegion(Location location, Runnable runnable) { if (location == null || location.getWorld() == null || runnable == null || !this.isPluginEnabled()) { return NO_OP_TASK; } - Object scheduler = this.regionScheduler(); - if (scheduler != null) { - PluginTask task = this.invokeSchedulerTask( - scheduler, - RUN_REGION, + Server server = this.plugin.getServer(); + RegionSchedulerCapability capability = this.regionSchedulerCapability(server); + if (capability.scheduler() != null) { + PluginTask task = capability.dispatchPlan().invokeRun( + capability.scheduler(), this.plugin, location, taskConsumer(runnable) @@ -182,18 +183,18 @@ public PluginTask runRegion(Location location, Runnable runnable) { return task; } } - return this.runGlobal(runnable); + return this.runGlobalValidated(server, runnable); } public PluginTask runRegionDelayed(Location location, Runnable runnable, long delayTicks) { if (location == null || location.getWorld() == null || runnable == null || !this.isPluginEnabled()) { return NO_OP_TASK; } - Object scheduler = this.regionScheduler(); - if (scheduler != null) { - PluginTask task = this.invokeSchedulerTask( - scheduler, - RUN_REGION_DELAYED, + Server server = this.plugin.getServer(); + RegionSchedulerCapability capability = this.regionSchedulerCapability(server); + if (capability.scheduler() != null) { + PluginTask task = capability.dispatchPlan().invokeDelayed( + capability.scheduler(), this.plugin, location, taskConsumer(runnable), @@ -203,18 +204,18 @@ public PluginTask runRegionDelayed(Location location, Runnable runnable, long de return task; } } - return this.runGlobalDelayed(runnable, delayTicks); + return this.runGlobalDelayedValidated(server, runnable, delayTicks); } public PluginTask runRegionTimer(Location location, Runnable runnable, long delayTicks, long periodTicks) { if (location == null || location.getWorld() == null || runnable == null || !this.isPluginEnabled()) { return NO_OP_TASK; } - Object scheduler = this.regionScheduler(); - if (scheduler != null) { - PluginTask task = this.invokeSchedulerTask( - scheduler, - RUN_REGION_TIMER, + Server server = this.plugin.getServer(); + RegionSchedulerCapability capability = this.regionSchedulerCapability(server); + if (capability.scheduler() != null) { + PluginTask task = capability.dispatchPlan().invokeTimer( + capability.scheduler(), this.plugin, location, taskConsumer(runnable), @@ -225,18 +226,17 @@ public PluginTask runRegionTimer(Location location, Runnable runnable, long dela return task; } } - return this.runGlobalTimer(runnable, delayTicks, periodTicks); + return this.runGlobalTimerValidated(server, runnable, delayTicks, periodTicks); } public PluginTask runEntity(Entity entity, Runnable runnable) { if (entity == null || runnable == null || !this.isPluginEnabled()) { return NO_OP_TASK; } - Object scheduler = this.entityScheduler(entity); - if (scheduler != null) { - PluginTask task = this.invokeSchedulerTask( - scheduler, - RUN_ENTITY, + EntitySchedulerCapability capability = this.entitySchedulerCapability(entity); + if (capability.scheduler() != null) { + PluginTask task = capability.dispatchPlan().invokeRun( + capability.scheduler(), this.plugin, taskConsumer(runnable), NO_OP_RUNNABLE @@ -245,18 +245,17 @@ public PluginTask runEntity(Entity entity, Runnable runnable) { return task; } } - return this.runGlobal(runnable); + return this.runGlobalValidated(this.plugin.getServer(), runnable); } public PluginTask runEntityDelayed(Entity entity, Runnable runnable, long delayTicks) { if (entity == null || runnable == null || !this.isPluginEnabled()) { return NO_OP_TASK; } - Object scheduler = this.entityScheduler(entity); - if (scheduler != null) { - PluginTask task = this.invokeSchedulerTask( - scheduler, - RUN_ENTITY_DELAYED, + EntitySchedulerCapability capability = this.entitySchedulerCapability(entity); + if (capability.scheduler() != null) { + PluginTask task = capability.dispatchPlan().invokeDelayed( + capability.scheduler(), this.plugin, taskConsumer(runnable), NO_OP_RUNNABLE, @@ -266,7 +265,7 @@ public PluginTask runEntityDelayed(Entity entity, Runnable runnable, long delayT return task; } } - return this.runGlobalDelayed(runnable, delayTicks); + return this.runGlobalDelayedValidated(this.plugin.getServer(), runnable, delayTicks); } @SuppressWarnings("unchecked") @@ -277,7 +276,7 @@ public CompletableFuture teleport(Entity entity, Location location) { MethodResolution teleportAsync = resolveMethod(entity.getClass(), TELEPORT_ASYNC); if (teleportAsync.isAvailable()) { try { - Object result = teleportAsync.invoke(entity, location); + Object result = teleportAsync.invoke1(entity, location); if (result instanceof CompletableFuture future) { return (CompletableFuture) future; } @@ -322,35 +321,103 @@ public PluginTask removeEntity(Entity entity, long delayTicks) { return this.runEntityDelayed(entity, removeTask, delayTicks); } - private Object globalRegionScheduler() { - Object server = this.plugin.getServer(); - SchedulerCapability capability = this.globalSchedulerCapability; + private PluginTask runGlobalValidated(Server server, Runnable runnable) { + GlobalSchedulerCapability capability = this.globalSchedulerCapability(server); + if (capability.scheduler() != null) { + PluginTask task = capability.dispatchPlan().invokeRun( + capability.scheduler(), + this.plugin, + taskConsumer(runnable) + ); + if (task != null) { + return task; + } + } + return wrap(this.bukkitScheduler(server).runTask(this.plugin, runnable)); + } + + private PluginTask runGlobalDelayedValidated(Server server, Runnable runnable, long delayTicks) { + GlobalSchedulerCapability capability = this.globalSchedulerCapability(server); + if (capability.scheduler() != null) { + PluginTask task = capability.dispatchPlan().invokeDelayed( + capability.scheduler(), + this.plugin, + taskConsumer(runnable), + delayTicks + ); + if (task != null) { + return task; + } + } + return wrap(this.bukkitScheduler(server).runTaskLater(this.plugin, runnable, delayTicks)); + } + + private PluginTask runGlobalTimerValidated(Server server, Runnable runnable, long delayTicks, long periodTicks) { + GlobalSchedulerCapability capability = this.globalSchedulerCapability(server); + if (capability.scheduler() != null) { + PluginTask task = capability.dispatchPlan().invokeTimer( + capability.scheduler(), + this.plugin, + taskConsumer(runnable), + delayTicks, + periodTicks + ); + if (task != null) { + return task; + } + } + return wrap(this.bukkitScheduler(server).runTaskTimer(this.plugin, runnable, delayTicks, periodTicks)); + } + + private GlobalSchedulerCapability globalSchedulerCapability(Server server) { + GlobalSchedulerCapability capability = this.globalSchedulerCapability; if (capability != null && capability.target() == server) { - return capability.scheduler(); + return capability; } - Object scheduler = this.invokeNoArgs(server, GET_GLOBAL_REGION_SCHEDULER); - this.globalSchedulerCapability = new SchedulerCapability(server, scheduler); - return scheduler; + Object scheduler = invokeNoArgs(server, GET_GLOBAL_REGION_SCHEDULER); + GlobalSchedulerDispatchPlan dispatchPlan = scheduler == null + ? null + : GLOBAL_SCHEDULER_DISPATCH_PLAN_CACHE.get(scheduler.getClass()); + capability = new GlobalSchedulerCapability(server, scheduler, dispatchPlan); + this.globalSchedulerCapability = capability; + return capability; } - private Object regionScheduler() { - Object server = this.plugin.getServer(); - SchedulerCapability capability = this.regionSchedulerCapability; + private RegionSchedulerCapability regionSchedulerCapability(Server server) { + RegionSchedulerCapability capability = this.regionSchedulerCapability; if (capability != null && capability.target() == server) { - return capability.scheduler(); + return capability; } - Object scheduler = this.invokeNoArgs(server, GET_REGION_SCHEDULER); - this.regionSchedulerCapability = new SchedulerCapability(server, scheduler); - return scheduler; + Object scheduler = invokeNoArgs(server, GET_REGION_SCHEDULER); + RegionSchedulerDispatchPlan dispatchPlan = scheduler == null + ? null + : REGION_SCHEDULER_DISPATCH_PLAN_CACHE.get(scheduler.getClass()); + capability = new RegionSchedulerCapability(server, scheduler, dispatchPlan); + this.regionSchedulerCapability = capability; + return capability; } - private Object entityScheduler(Entity entity) { - SchedulerCapability capability = this.entitySchedulerCapability; + private EntitySchedulerCapability entitySchedulerCapability(Entity entity) { + EntitySchedulerCapability capability = this.entitySchedulerCapability; if (capability != null && capability.target() == entity) { + return capability; + } + Object scheduler = invokeNoArgs(entity, GET_ENTITY_SCHEDULER); + EntitySchedulerDispatchPlan dispatchPlan = scheduler == null + ? null + : ENTITY_SCHEDULER_DISPATCH_PLAN_CACHE.get(scheduler.getClass()); + capability = new EntitySchedulerCapability(entity, scheduler, dispatchPlan); + this.entitySchedulerCapability = capability; + return capability; + } + + private BukkitScheduler bukkitScheduler(Server server) { + BukkitSchedulerCapability capability = this.bukkitSchedulerCapability; + if (capability != null && capability.target() == server) { return capability.scheduler(); } - Object scheduler = this.invokeNoArgs(entity, GET_ENTITY_SCHEDULER); - this.entitySchedulerCapability = new SchedulerCapability(entity, scheduler); + BukkitScheduler scheduler = server.getScheduler(); + this.bukkitSchedulerCapability = new BukkitSchedulerCapability(server, scheduler); return scheduler; } @@ -358,7 +425,7 @@ private boolean isPluginEnabled() { return this.plugin.isEnabled(); } - private Object invokeNoArgs(Object target, MethodSignature signature) { + private static Object invokeNoArgs(Object target, MethodSignature signature) { if (target == null) { return null; } @@ -367,19 +434,77 @@ private Object invokeNoArgs(Object target, MethodSignature signature) { return null; } try { - return resolution.invoke(target); + return resolution.invoke0(target); } catch (IllegalAccessException exception) { return null; } } - private PluginTask invokeSchedulerTask(Object scheduler, MethodSignature signature, Object... args) { - MethodResolution resolution = resolveMethod(scheduler.getClass(), signature); + private static PluginTask invokeSchedulerTask( + Object scheduler, + MethodResolution resolution, + Object argument0, + Object argument1 + ) { if (!resolution.isAvailable()) { return null; } try { - return wrap(resolution.invoke(scheduler, args)); + return wrap(resolution.invoke2(scheduler, argument0, argument1)); + } catch (IllegalAccessException exception) { + return null; + } + } + + private static PluginTask invokeSchedulerTask( + Object scheduler, + MethodResolution resolution, + Object argument0, + Object argument1, + Object argument2 + ) { + if (!resolution.isAvailable()) { + return null; + } + try { + return wrap(resolution.invoke3(scheduler, argument0, argument1, argument2)); + } catch (IllegalAccessException exception) { + return null; + } + } + + private static PluginTask invokeSchedulerTask( + Object scheduler, + MethodResolution resolution, + Object argument0, + Object argument1, + Object argument2, + Object argument3 + ) { + if (!resolution.isAvailable()) { + return null; + } + try { + return wrap(resolution.invoke4(scheduler, argument0, argument1, argument2, argument3)); + } catch (IllegalAccessException exception) { + return null; + } + } + + private static PluginTask invokeSchedulerTask( + Object scheduler, + MethodResolution resolution, + Object argument0, + Object argument1, + Object argument2, + Object argument3, + Object argument4 + ) { + if (!resolution.isAvailable()) { + return null; + } + try { + return wrap(resolution.invoke5(scheduler, argument0, argument1, argument2, argument3, argument4)); } catch (IllegalAccessException exception) { return null; } @@ -397,7 +522,117 @@ private static PluginTask wrap(Object task) { return task == null ? NO_OP_TASK : new ScheduledTaskHandle(task); } - private record SchedulerCapability(Object target, Object scheduler) { + private record GlobalSchedulerDispatchPlan( + MethodResolution runMethod, + MethodResolution delayedMethod, + MethodResolution timerMethod + ) { + private PluginTask invokeRun(Object scheduler, Plugin plugin, Consumer consumer) { + return invokeSchedulerTask(scheduler, this.runMethod, plugin, consumer); + } + + private PluginTask invokeDelayed(Object scheduler, Plugin plugin, Consumer consumer, long delayTicks) { + return invokeSchedulerTask(scheduler, this.delayedMethod, plugin, consumer, delayTicks); + } + + private PluginTask invokeTimer( + Object scheduler, + Plugin plugin, + Consumer consumer, + long delayTicks, + long periodTicks + ) { + return invokeSchedulerTask(scheduler, this.timerMethod, plugin, consumer, delayTicks, periodTicks); + } + } + + private record RegionSchedulerDispatchPlan( + MethodResolution runMethod, + MethodResolution delayedMethod, + MethodResolution timerMethod + ) { + private PluginTask invokeRun( + Object scheduler, + Plugin plugin, + Location location, + Consumer consumer + ) { + return invokeSchedulerTask(scheduler, this.runMethod, plugin, location, consumer); + } + + private PluginTask invokeDelayed( + Object scheduler, + Plugin plugin, + Location location, + Consumer consumer, + long delayTicks + ) { + return invokeSchedulerTask(scheduler, this.delayedMethod, plugin, location, consumer, delayTicks); + } + + private PluginTask invokeTimer( + Object scheduler, + Plugin plugin, + Location location, + Consumer consumer, + long delayTicks, + long periodTicks + ) { + return invokeSchedulerTask( + scheduler, + this.timerMethod, + plugin, + location, + consumer, + delayTicks, + periodTicks + ); + } + } + + private record EntitySchedulerDispatchPlan(MethodResolution runMethod, MethodResolution delayedMethod) { + private PluginTask invokeRun( + Object scheduler, + Plugin plugin, + Consumer consumer, + Runnable retired + ) { + return invokeSchedulerTask(scheduler, this.runMethod, plugin, consumer, retired); + } + + private PluginTask invokeDelayed( + Object scheduler, + Plugin plugin, + Consumer consumer, + Runnable retired, + long delayTicks + ) { + return invokeSchedulerTask(scheduler, this.delayedMethod, plugin, consumer, retired, delayTicks); + } + } + + private record GlobalSchedulerCapability( + Server target, + Object scheduler, + GlobalSchedulerDispatchPlan dispatchPlan + ) { + } + + private record RegionSchedulerCapability( + Server target, + Object scheduler, + RegionSchedulerDispatchPlan dispatchPlan + ) { + } + + private record EntitySchedulerCapability( + Entity target, + Object scheduler, + EntitySchedulerDispatchPlan dispatchPlan + ) { + } + + private record BukkitSchedulerCapability(Server target, BukkitScheduler scheduler) { } private record BukkitTaskHandle(BukkitTask task) implements PluginTask { @@ -434,7 +669,7 @@ private static Object invokeTaskMethod(Object task, MethodSignature signature) { return null; } try { - return resolution.invoke(task); + return resolution.invoke0(task); } catch (IllegalAccessException exception) { return null; } @@ -481,32 +716,147 @@ private Class[] parameterArray() { } } - private record MethodResolution(Method method, SMethod invoker) { - private static final MethodResolution UNAVAILABLE = new MethodResolution(null, null); + private static final class MethodResolution { + private static final MethodResolution UNAVAILABLE = new MethodResolution( + null, + null, + null, + null, + null, + null, + null + ); + + private final Method method; + private final SMethod0 invoker0; + private final SMethod1 invoker1; + private final SMethod2 invoker2; + private final SMethod3 invoker3; + private final SMethod4 invoker4; + private final SMethod5 invoker5; + + private MethodResolution( + Method method, + SMethod0 invoker0, + SMethod1 invoker1, + SMethod2 invoker2, + SMethod3 invoker3, + SMethod4 invoker4, + SMethod5 invoker5 + ) { + this.method = method; + this.invoker0 = invoker0; + this.invoker1 = invoker1; + this.invoker2 = invoker2; + this.invoker3 = invoker3; + this.invoker4 = invoker4; + this.invoker5 = invoker5; + } private static MethodResolution available(Method method) { - SMethod invoker = null; + SMethod0 invoker0 = null; + SMethod1 invoker1 = null; + SMethod2 invoker2 = null; + SMethod3 invoker3 = null; + SMethod4 invoker4 = null; + SMethod5 invoker5 = null; try { - invoker = SparrowMethod.of(method).asm(); + SparrowMethod sparrowMethod = SparrowMethod.of(method); + switch (method.getParameterCount()) { + case 0 -> invoker0 = sparrowMethod.asm$0(); + case 1 -> invoker1 = sparrowMethod.asm$1(); + case 2 -> invoker2 = sparrowMethod.asm$2(); + case 3 -> invoker3 = sparrowMethod.asm$3(); + case 4 -> invoker4 = sparrowMethod.asm$4(); + case 5 -> invoker5 = sparrowMethod.asm$5(); + default -> { + } + } } catch (RuntimeException | LinkageError exception) { // Some generated or strongly encapsulated runtime classes reject hidden invoker generation. } - return new MethodResolution(method, invoker); + return new MethodResolution(method, invoker0, invoker1, invoker2, invoker3, invoker4, invoker5); + } + + private Method method() { + return this.method; } private boolean isAvailable() { return this.method != null; } - private Object invoke(Object target, Object... args) throws IllegalAccessException { + private Object invoke0(Object target) throws IllegalAccessException { + this.requireAvailable(); + if (this.invoker0 != null) { + return this.invoker0.invoke(target); + } + return this.invokeReflectively(target); + } + + private Object invoke1(Object target, Object argument0) throws IllegalAccessException { + this.requireAvailable(); + if (this.invoker1 != null) { + return this.invoker1.invoke(target, argument0); + } + return this.invokeReflectively(target, argument0); + } + + private Object invoke2(Object target, Object argument0, Object argument1) throws IllegalAccessException { + this.requireAvailable(); + if (this.invoker2 != null) { + return this.invoker2.invoke(target, argument0, argument1); + } + return this.invokeReflectively(target, argument0, argument1); + } + + private Object invoke3(Object target, Object argument0, Object argument1, Object argument2) + throws IllegalAccessException { + this.requireAvailable(); + if (this.invoker3 != null) { + return this.invoker3.invoke(target, argument0, argument1, argument2); + } + return this.invokeReflectively(target, argument0, argument1, argument2); + } + + private Object invoke4( + Object target, + Object argument0, + Object argument1, + Object argument2, + Object argument3 + ) throws IllegalAccessException { + this.requireAvailable(); + if (this.invoker4 != null) { + return this.invoker4.invoke(target, argument0, argument1, argument2, argument3); + } + return this.invokeReflectively(target, argument0, argument1, argument2, argument3); + } + + private Object invoke5( + Object target, + Object argument0, + Object argument1, + Object argument2, + Object argument3, + Object argument4 + ) throws IllegalAccessException { + this.requireAvailable(); + if (this.invoker5 != null) { + return this.invoker5.invoke(target, argument0, argument1, argument2, argument3, argument4); + } + return this.invokeReflectively(target, argument0, argument1, argument2, argument3, argument4); + } + + private void requireAvailable() { if (this.method == null) { throw new IllegalStateException("Cannot invoke an unavailable method"); } - if (this.invoker != null) { - return this.invoker.invoke(target, args); - } + } + + private Object invokeReflectively(Object target, Object... arguments) throws IllegalAccessException { try { - return this.method.invoke(target, args); + return this.method.invoke(target, arguments); } catch (InvocationTargetException exception) { Throwable cause = exception.getCause(); if (cause instanceof RuntimeException runtimeException) { diff --git a/src/test/java/top/ellan/mahjong/runtime/ServerSchedulerDispatchPlanTest.java b/src/test/java/top/ellan/mahjong/runtime/ServerSchedulerDispatchPlanTest.java new file mode 100644 index 0000000..32e06dd --- /dev/null +++ b/src/test/java/top/ellan/mahjong/runtime/ServerSchedulerDispatchPlanTest.java @@ -0,0 +1,300 @@ +package top.ellan.mahjong.runtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.papermc.paper.threadedregions.scheduler.EntityScheduler; +import io.papermc.paper.threadedregions.scheduler.GlobalRegionScheduler; +import io.papermc.paper.threadedregions.scheduler.RegionScheduler; +import io.papermc.paper.threadedregions.scheduler.ScheduledTask; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import org.bukkit.Location; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.scheduler.BukkitTask; +import org.junit.jupiter.api.Test; + +final class ServerSchedulerDispatchPlanTest { + private static final Runnable NO_OP = () -> { + }; + + @Test + void dispatchesEveryFoliaMethodShapeThroughCachedPlans() { + Plugin plugin = mock(Plugin.class); + Server server = mock(Server.class); + World world = mock(World.class); + Location location = new Location(world, 8.0, 64.0, -8.0); + Entity entity = mock(Entity.class); + GlobalRegionScheduler global = mock(GlobalRegionScheduler.class); + RegionScheduler region = mock(RegionScheduler.class); + EntityScheduler entityScheduler = mock(EntityScheduler.class); + ScheduledTask task = mock(ScheduledTask.class); + + when(plugin.isEnabled()).thenReturn(true); + when(plugin.getServer()).thenReturn(server); + when(server.getGlobalRegionScheduler()).thenReturn(global); + when(server.getRegionScheduler()).thenReturn(region); + when(entity.getScheduler()).thenReturn(entityScheduler); + when(global.run(eq(plugin), any())).thenReturn(task); + when(global.runDelayed(eq(plugin), any(), anyLong())).thenReturn(task); + when(global.runAtFixedRate(eq(plugin), any(), anyLong(), anyLong())).thenReturn(task); + when(region.run(eq(plugin), eq(location), any())).thenReturn(task); + when(region.runDelayed(eq(plugin), eq(location), any(), anyLong())).thenReturn(task); + when(region.runAtFixedRate(eq(plugin), eq(location), any(), anyLong(), anyLong())).thenReturn(task); + when(entityScheduler.run(eq(plugin), any(), any(Runnable.class))).thenReturn(task); + when(entityScheduler.runDelayed(eq(plugin), any(), any(Runnable.class), anyLong())).thenReturn(task); + + ServerScheduler scheduler = new ServerScheduler(plugin); + + assertFalse(scheduler.runGlobal(NO_OP).isCancelled()); + assertFalse(scheduler.runGlobalDelayed(NO_OP, 11L).isCancelled()); + assertFalse(scheduler.runGlobalTimer(NO_OP, 13L, 17L).isCancelled()); + assertFalse(scheduler.runRegion(location, NO_OP).isCancelled()); + assertFalse(scheduler.runRegionDelayed(location, NO_OP, 19L).isCancelled()); + assertFalse(scheduler.runRegionTimer(location, NO_OP, 23L, 29L).isCancelled()); + assertFalse(scheduler.runEntity(entity, NO_OP).isCancelled()); + assertFalse(scheduler.runEntityDelayed(entity, NO_OP, 31L).isCancelled()); + + verify(server, times(1)).getGlobalRegionScheduler(); + verify(server, times(1)).getRegionScheduler(); + verify(entity, times(1)).getScheduler(); + verify(global).run(eq(plugin), any()); + verify(global).runDelayed(eq(plugin), any(), eq(11L)); + verify(global).runAtFixedRate(eq(plugin), any(), eq(13L), eq(17L)); + verify(region).run(eq(plugin), eq(location), any()); + verify(region).runDelayed(eq(plugin), eq(location), any(), eq(19L)); + verify(region).runAtFixedRate(eq(plugin), eq(location), any(), eq(23L), eq(29L)); + verify(entityScheduler).run(eq(plugin), any(), any(Runnable.class)); + verify(entityScheduler).runDelayed(eq(plugin), any(), any(Runnable.class), eq(31L)); + } + + @Test + void missingRegionAndEntityCapabilitiesFallBackToGlobalFoliaScheduler() { + Plugin plugin = mock(Plugin.class); + Server server = mock(Server.class); + World world = mock(World.class); + Location location = new Location(world, 0.0, 64.0, 0.0); + Entity entity = mock(Entity.class); + GlobalRegionScheduler global = mock(GlobalRegionScheduler.class); + ScheduledTask task = mock(ScheduledTask.class); + AtomicInteger executions = new AtomicInteger(); + + when(plugin.isEnabled()).thenReturn(true); + when(plugin.getServer()).thenReturn(server); + when(server.getGlobalRegionScheduler()).thenReturn(global); + when(global.run(eq(plugin), any())).thenAnswer(invocation -> { + @SuppressWarnings("unchecked") + Consumer consumer = invocation.getArgument(1, Consumer.class); + consumer.accept(task); + return task; + }); + + ServerScheduler scheduler = new ServerScheduler(plugin); + scheduler.runRegion(location, executions::incrementAndGet); + scheduler.runEntity(entity, executions::incrementAndGet); + + assertEquals(2, executions.get()); + verify(server, times(1)).getRegionScheduler(); + verify(entity, times(1)).getScheduler(); + verify(server, times(1)).getGlobalRegionScheduler(); + verify(global, times(2)).run(eq(plugin), any()); + verify(server, never()).getScheduler(); + } + + @Test + void cachesBukkitFallbackByServerIdentityAndInvalidatesOnServerChange() { + Plugin plugin = mock(Plugin.class); + Server firstServer = mock(Server.class); + Server secondServer = mock(Server.class); + BukkitScheduler firstScheduler = mock(BukkitScheduler.class); + BukkitScheduler secondScheduler = mock(BukkitScheduler.class); + BukkitTask firstTask = mock(BukkitTask.class); + BukkitTask secondTask = mock(BukkitTask.class); + World world = mock(World.class); + Location location = new Location(world, 0.0, 64.0, 0.0); + Entity entity = mock(Entity.class); + AtomicReference currentServer = new AtomicReference<>(firstServer); + + when(plugin.isEnabled()).thenReturn(true); + when(plugin.getServer()).thenAnswer(invocation -> currentServer.get()); + when(firstServer.getScheduler()).thenReturn(firstScheduler); + when(secondServer.getScheduler()).thenReturn(secondScheduler); + when(firstScheduler.runTask(eq(plugin), any(Runnable.class))).thenReturn(firstTask); + when(secondScheduler.runTask(eq(plugin), any(Runnable.class))).thenReturn(secondTask); + + ServerScheduler scheduler = new ServerScheduler(plugin); + scheduler.runGlobal(NO_OP); + scheduler.runRegion(location, NO_OP); + scheduler.runEntity(entity, NO_OP); + currentServer.set(secondServer); + scheduler.runGlobal(NO_OP); + scheduler.runRegion(location, NO_OP); + scheduler.runEntity(entity, NO_OP); + + verify(plugin, times(6)).getServer(); + verify(firstServer, times(1)).getScheduler(); + verify(secondServer, times(1)).getScheduler(); + verify(firstScheduler, times(3)).runTask(eq(plugin), any(Runnable.class)); + verify(secondScheduler, times(3)).runTask(eq(plugin), any(Runnable.class)); + } + + @Test + void nullFoliaTaskDoesNotScheduleAgainThroughBukkit() { + Plugin plugin = mock(Plugin.class); + Server server = mock(Server.class); + GlobalRegionScheduler global = mock(GlobalRegionScheduler.class); + + when(plugin.isEnabled()).thenReturn(true); + when(plugin.getServer()).thenReturn(server); + when(server.getGlobalRegionScheduler()).thenReturn(global); + when(global.run(eq(plugin), any())).thenReturn(null); + + PluginTask task = new ServerScheduler(plugin).runGlobal(NO_OP); + + assertTrue(task.isCancelled()); + verify(global, times(1)).run(eq(plugin), any()); + verify(server, never()).getScheduler(); + } + + @Test + void regionSchedulerFailureNeverFallsBackOrSchedulesTwice() { + Plugin plugin = mock(Plugin.class); + Server server = mock(Server.class); + World world = mock(World.class); + Location location = new Location(world, 0.0, 64.0, 0.0); + RegionScheduler region = mock(RegionScheduler.class); + + when(plugin.isEnabled()).thenReturn(true); + when(plugin.getServer()).thenReturn(server); + when(server.getRegionScheduler()).thenReturn(region); + when(region.run(eq(plugin), eq(location), any())).thenThrow(new IllegalStateException("region failed")); + + assertThrows(IllegalStateException.class, () -> new ServerScheduler(plugin).runRegion(location, NO_OP)); + + verify(region, times(1)).run(eq(plugin), eq(location), any()); + verify(server, never()).getGlobalRegionScheduler(); + verify(server, never()).getScheduler(); + } + + @Test + void entitySchedulerFailureNeverFallsBackOrSchedulesTwice() { + Plugin plugin = mock(Plugin.class); + Entity entity = mock(Entity.class); + EntityScheduler entityScheduler = mock(EntityScheduler.class); + + when(plugin.isEnabled()).thenReturn(true); + when(entity.getScheduler()).thenReturn(entityScheduler); + when(entityScheduler.run(eq(plugin), any(), any(Runnable.class))) + .thenThrow(new IllegalStateException("entity failed")); + + assertThrows(IllegalStateException.class, () -> new ServerScheduler(plugin).runEntity(entity, NO_OP)); + + verify(entityScheduler, times(1)).run(eq(plugin), any(), any(Runnable.class)); + verify(plugin, never()).getServer(); + } + + @Test + void neverRoutesConcurrentGlobalCallsThroughAnotherServersScheduler() throws InterruptedException { + Plugin plugin = mock(Plugin.class); + Server firstServer = mock(Server.class); + Server secondServer = mock(Server.class); + GlobalRegionScheduler firstScheduler = mock(GlobalRegionScheduler.class); + GlobalRegionScheduler secondScheduler = mock(GlobalRegionScheduler.class); + ScheduledTask task = mock(ScheduledTask.class); + ThreadLocal threadServer = new ThreadLocal<>(); + AtomicInteger wrongRoutes = new AtomicInteger(); + + when(plugin.isEnabled()).thenReturn(true); + when(plugin.getServer()).thenAnswer(invocation -> threadServer.get()); + when(firstServer.getGlobalRegionScheduler()).thenReturn(firstScheduler); + when(secondServer.getGlobalRegionScheduler()).thenReturn(secondScheduler); + when(firstScheduler.run(eq(plugin), any())).thenAnswer(invocation -> { + if (!Thread.currentThread().getName().equals("scheduler-server-first")) { + wrongRoutes.incrementAndGet(); + } + return task; + }); + when(secondScheduler.run(eq(plugin), any())).thenAnswer(invocation -> { + if (!Thread.currentThread().getName().equals("scheduler-server-second")) { + wrongRoutes.incrementAndGet(); + } + return task; + }); + + ServerScheduler scheduler = new ServerScheduler(plugin); + CountDownLatch ready = new CountDownLatch(2); + CountDownLatch start = new CountDownLatch(1); + CountDownLatch done = new CountDownLatch(2); + AtomicReference failure = new AtomicReference<>(); + Thread firstThread = new Thread( + () -> runGlobalLoop(scheduler, threadServer, firstServer, ready, start, done, failure), + "scheduler-server-first" + ); + Thread secondThread = new Thread( + () -> runGlobalLoop(scheduler, threadServer, secondServer, ready, start, done, failure), + "scheduler-server-second" + ); + + firstThread.start(); + secondThread.start(); + try { + assertTrue(ready.await(5, TimeUnit.SECONDS)); + start.countDown(); + assertTrue(done.await(30, TimeUnit.SECONDS)); + } finally { + start.countDown(); + firstThread.interrupt(); + secondThread.interrupt(); + firstThread.join(); + secondThread.join(); + } + + if (failure.get() != null) { + throw new AssertionError("Concurrent scheduler invocation failed", failure.get()); + } + assertEquals(0, wrongRoutes.get()); + } + + private static void runGlobalLoop( + ServerScheduler scheduler, + ThreadLocal threadServer, + Server server, + CountDownLatch ready, + CountDownLatch start, + CountDownLatch done, + AtomicReference failure + ) { + threadServer.set(server); + ready.countDown(); + try { + if (!start.await(5, TimeUnit.SECONDS)) { + throw new AssertionError("Concurrent scheduler start timed out"); + } + for (int iteration = 0; iteration < 20_000; iteration++) { + scheduler.runGlobal(NO_OP); + } + } catch (Throwable throwable) { + failure.compareAndSet(null, throwable); + } finally { + threadServer.remove(); + done.countDown(); + } + } +} diff --git a/src/test/java/top/ellan/mahjong/runtime/ServerSchedulerReflectionCacheTest.java b/src/test/java/top/ellan/mahjong/runtime/ServerSchedulerReflectionCacheTest.java index 2776e4b..34a7031 100644 --- a/src/test/java/top/ellan/mahjong/runtime/ServerSchedulerReflectionCacheTest.java +++ b/src/test/java/top/ellan/mahjong/runtime/ServerSchedulerReflectionCacheTest.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -202,12 +203,26 @@ void neverRoutesConcurrentEntityCallsThroughAnotherEntityScheduler() throws Inte } return task; }); + when(firstEntityScheduler.runDelayed(any(Plugin.class), any(), any(Runnable.class), anyLong())) + .thenAnswer(invocation -> { + if (!Thread.currentThread().getName().equals("scheduler-entity-first")) { + wrongRoutes.incrementAndGet(); + } + return task; + }); when(secondEntityScheduler.run(any(Plugin.class), any(), any(Runnable.class))).thenAnswer(invocation -> { if (!Thread.currentThread().getName().equals("scheduler-entity-second")) { wrongRoutes.incrementAndGet(); } return task; }); + when(secondEntityScheduler.runDelayed(any(Plugin.class), any(), any(Runnable.class), anyLong())) + .thenAnswer(invocation -> { + if (!Thread.currentThread().getName().equals("scheduler-entity-second")) { + wrongRoutes.incrementAndGet(); + } + return task; + }); ServerScheduler scheduler = new ServerScheduler(plugin); CountDownLatch ready = new CountDownLatch(2); @@ -279,6 +294,7 @@ private static void runEntityLoop( } for (int iteration = 0; iteration < 20_000; iteration++) { scheduler.runEntity(entity, NO_OP); + scheduler.runEntityDelayed(entity, NO_OP, 1L); } } catch (Throwable throwable) { failure.compareAndSet(null, throwable);