From da3e855e3748c81fe0c41a41f36e42607700ca10 Mon Sep 17 00:00:00 2001 From: Leleawa <131422030+Leleawa@users.noreply.github.com> Date: Tue, 21 Jul 2026 17:10:46 +0800 Subject: [PATCH] Fix FoliaTickReporter never resolving getTickReport5s MethodHandles.Lookup#findVirtual matches on the full MethodType, return type included. The lookup passed Object.class as the return type, but RegionScheduleHandle#getTickReport5s returns TickData.TickReportData, so it always threw NoSuchMethodException and the handle stayed null. Every getTPS()/getMSPT() call therefore fell through to the Server fallback, which bypasses the Caffeine cache entirely. Server#getTPS() rebuilds the 1m/5m/15m tick reports on each call, sorting the full tick history (~18k samples for the 15m window). On a profiled Folia server this accounted for ~40% of all non-idle region thread time, nearly all of it in Arrays.sort, driven mostly by the regionalactivity Pathfinding listener calling shouldCancelBecauseLagging() on every EntityPathfindEvent. Add ReflectionUtil#findMethodAnyReturn, which unreflects a Method looked up by name and parameter types only, and use it here. The cached path now resolves the 5s report as intended. Also move the null check into the cache loaders, so the Server fallback can no longer be reached on an uncached path if the lookup ever breaks again in a future Folia release. Co-Authored-By: Claude Opus 4.8 --- .../aef/utils/reflection/ReflectionUtil.java | 25 ++++++++++++++++ .../aef/utils/tickdata/FoliaTickReporter.java | 30 ++++++++----------- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/shared/src/main/java/me/xginko/aef/utils/reflection/ReflectionUtil.java b/shared/src/main/java/me/xginko/aef/utils/reflection/ReflectionUtil.java index 5252e1d4c..7c4336622 100644 --- a/shared/src/main/java/me/xginko/aef/utils/reflection/ReflectionUtil.java +++ b/shared/src/main/java/me/xginko/aef/utils/reflection/ReflectionUtil.java @@ -118,6 +118,31 @@ public static boolean hasClass(final @NonNull String className) { return findClass(className) != null; } + /** + * Gets a handle for a class method, regardless of its return type. + * + *

{@link #findMethod} matches on the exact {@link MethodType}, return type included, so it + * cannot look up a method whose return class is not available at compile time. Use this instead + * when only the name and parameters are known.

+ * + * @param holderClass a class + * @param methodName a method name + * @param parameterClasses an array of method parameter classes + * @return a method handle or {@code null} if not found + */ + public static @Nullable MethodHandle findMethodAnyReturn(final @Nullable Class holderClass, final String methodName, final Class... parameterClasses) { + if (holderClass == null) return null; + for (final Class parameterClass : parameterClasses) { + if (parameterClass == null) return null; + } + + try { + return LOOKUP.unreflect(holderClass.getMethod(methodName, parameterClasses)); + } catch (final NoSuchMethodException | IllegalAccessException e) { + return null; + } + } + /** * Gets a handle for a class method. * diff --git a/shared/src/main/java/me/xginko/aef/utils/tickdata/FoliaTickReporter.java b/shared/src/main/java/me/xginko/aef/utils/tickdata/FoliaTickReporter.java index b3a1837da..aadd735eb 100644 --- a/shared/src/main/java/me/xginko/aef/utils/tickdata/FoliaTickReporter.java +++ b/shared/src/main/java/me/xginko/aef/utils/tickdata/FoliaTickReporter.java @@ -24,10 +24,11 @@ public FoliaTickReporter(Server server, Duration cacheTime) { this.tps_cache = Caffeine.newBuilder().expireAfterWrite(cacheTime).build(); this.mspt_cache = Caffeine.newBuilder().expireAfterWrite(cacheTime).build(); - this.getTickReport5s = ReflectionUtil.findMethod( + // Returns TickData.TickReportData, which is not on the compile classpath, so the return + // type must not be part of the lookup. + this.getTickReport5s = ReflectionUtil.findMethodAnyReturn( TickRegionScheduler.RegionScheduleHandle.class, "getTickReport5s", - Object.class, long.class ); } @@ -49,10 +50,7 @@ public void disable() { @Override public double getGlobalTPS() { - if (getTickReport5s != null) { - return tps_cache.get(RegionizedServer.getGlobalTickData(), this::extractTpsValue); - } - return server.getTPS()[0]; + return tps_cache.get(RegionizedServer.getGlobalTickData(), this::extractTpsValue); } @Override @@ -60,18 +58,12 @@ public double getTPS() { final ThreadedRegionizer.ThreadedRegion region = TickRegionScheduler.getCurrentRegion(); if (region == null) return getGlobalTPS(); - if (getTickReport5s != null) { - return tps_cache.get(region.getData().getRegionSchedulingHandle(), this::extractTpsValue); - } - return server.getTPS()[0]; + return tps_cache.get(region.getData().getRegionSchedulingHandle(), this::extractTpsValue); } @Override public double getGlobalMSPT() { - if (getTickReport5s != null) { - return mspt_cache.get(RegionizedServer.getGlobalTickData(), this::extractMsptValue); - } - return server.getAverageTickTime(); + return mspt_cache.get(RegionizedServer.getGlobalTickData(), this::extractMsptValue); } @Override @@ -79,13 +71,14 @@ public double getMSPT() { final ThreadedRegionizer.ThreadedRegion region = TickRegionScheduler.getCurrentRegion(); if (region == null) return getGlobalMSPT(); - if (getTickReport5s != null) { - return mspt_cache.get(region.getData().getRegionSchedulingHandle(), this::extractMsptValue); - } - return server.getAverageTickTime(); + return mspt_cache.get(region.getData().getRegionSchedulingHandle(), this::extractMsptValue); } + // The Server fallbacks below are resolved inside the cache loaders on purpose. Server#getTPS and + // Server#getAverageTickTime rebuild the 1m/5m/15m tick reports on every call, which sorts the + // whole tick history, so they must never be reached on an uncached path. private double extractTpsValue(TickRegionScheduler.RegionScheduleHandle handle) { + if (getTickReport5s == null) return server.getTPS()[0]; try { Object report = getTickReport5s.invoke(handle, System.nanoTime()); return extractAverage(report); @@ -95,6 +88,7 @@ private double extractTpsValue(TickRegionScheduler.RegionScheduleHandle handle) } private double extractMsptValue(TickRegionScheduler.RegionScheduleHandle handle) { + if (getTickReport5s == null) return server.getAverageTickTime(); try { Object report = getTickReport5s.invoke(handle, System.nanoTime()); return extractAverage(report) / 1000000.0;