Skip to content

Fix FoliaTickReporter never resolving getTickReport5s, bypassing the TPS cache#333

Open
Leleawa wants to merge 1 commit into
xGinko:masterfrom
Leleawa:fix/folia-tickreporter-method-lookup
Open

Fix FoliaTickReporter never resolving getTickReport5s, bypassing the TPS cache#333
Leleawa wants to merge 1 commit into
xGinko:masterfrom
Leleawa:fix/folia-tickreporter-method-lookup

Conversation

@Leleawa

@Leleawa Leleawa commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Problem

FoliaTickReporter looks up RegionScheduleHandle#getTickReport5s with Object.class as the return type:

this.getTickReport5s = ReflectionUtil.findMethod(
        TickRegionScheduler.RegionScheduleHandle.class,
        "getTickReport5s",
        Object.class,
        long.class
);

ReflectionUtil.findMethod resolves this through MethodHandles.Lookup#findVirtual, which matches on the full MethodType, return type included. The real signature is:

public final TickData.TickReportData getTickReport5s(final long currTime)

TickReportData != Object, so the lookup throws NoSuchMethodException and getTickReport5s is always null on every Folia server.

Minimal reproduction of the lookup semantics:

findVirtual(Object.class) -> NoSuchMethodException  (handle stays null)
unreflect(getMethod)      -> FOUND, invoke returned TickReportData

Impact

With the handle null, all four public methods take the Server fallback branch:

if (getTickReport5s != null) {
    return tps_cache.get(...);   // never reached
}
return server.getTPS()[0];       // always taken, and NOT cached

That path bypasses the Caffeine cache entirely. CraftServer#getTPS() rebuilds the 1m/5m/15m tick reports on every call, and each report sorts the full tick history — roughly 18k samples for the 15m window.

Because regionalactivity's Pathfinding listener calls shouldCancelBecauseLagging() on every EntityPathfindEvent, this runs constantly.

Spark profile from an affected Folia server (12 region threads, 92% parked, so percentages below are of the ~27.9s of actual non-idle work):

RegionalActivityModule.shouldCancelEvent()        11016ms   39.5%
└─ shouldCancelBecauseLagging()                   11008ms
   └─ FoliaTickReporter.getTPS()                  10972ms
      └─ CraftServer.getTPS()                     11076ms
         └─ TickData.generateTickReport()         11100ms
            └─ Arrays.sort()                      10580ms   37.9%

For scale, inside PathNavigation.createPath() (9552ms total), 9388ms — 98% — was this listener rather than actual pathfinding.

Fix

  1. Add ReflectionUtil#findMethodAnyReturn, which unreflects a Method looked up by name and parameter types only. findMethod's existing contract is untouched, so no other call sites are affected.
  2. Use it in FoliaTickReporter, so the cached 5s-window path works as originally intended.
  3. Move the getTickReport5s == null check into the two cache loaders. The old structure meant a failed lookup necessarily implied an uncached, expensive path; now the Server fallback is rate-limited by the cache no matter what, should the lookup ever break again in a future Folia release. This also removes 12 lines.

Testing

  • ./gradlew build passes. The ClassDowngrader errors in the log are pre-existing — a baseline build on unmodified master produces the identical count (382).
  • Verified in the shipped AnarchyExploitFixes-Folia jar bytecode that the constructor now calls findMethodAnyReturn, and that getTPS() has no path to server.getTPS() outside the cache loader.
  • Deployed on a live Folia server: MSPT back to normal.

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 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant