
GPExpansion
The ultimate add-on for GriefPrevention 3D Subdivisions
Список изменений
GPExpansion v1.1.18
A single fix, and a short release. v1.1.17 stopped the claim map from calling the code below; this one fixes the code itself.
Shortly after v1.1.17 went out, a watchdog thread dump arrived from a server running an older build. It shows the server thread stalled past ten seconds inside one /claimmap click:
The server has not responded for 10 seconds! Creating thread dump
java.lang.Throwable.fillInStackTrace(Native Method)
java.lang.NoSuchMethodException.<init>
java.lang.Class.getMethod
GPBridge.bruteForceFindClaim
GPBridge.getClaimAt
GPBridge.getDominantClaimInCell
ClaimMapEditorGUI.createCellItem
ClaimMapEditorGUI.populateInventory
ClaimMapEditorGUI.handleClick
The bottom half of that stack is the path v1.1.17 addressed, by sampling map cells less densely and by not calling bruteForceFindClaim at all on GriefPrevention3D and upstream GriefPrevention. The top half is something v1.1.17 did not address: the sweep's dominant cost was never claim geometry. It was the JVM building an exception, complete with stack capture, that the code immediately discarded — once per claim, per probe.
Who needs this. If you are on v1.1.17 with GriefPrevention3D or upstream GriefPrevention, nothing here is observable; the affected method is already unreachable for you. This closes the remaining exposure for legacy forks and fixes the defect at its source rather than routing around it. If you are on v1.1.16 or earlier, the release you need is v1.1.17 — take it first, or take this, which includes it.
No configuration changes, no new permissions, no lang keys. version.config-version stays at 1.1.2.
Bug Fixes
bruteForceFindClaim constructed an exception per claim, per probe
The method resolved its reflective accessors inside the loop over every claim, each behind an if (method == null) guard:
for (Object claim : claims) {
Class<?> cc = claim.getClass();
if (is3D == null) {
try { is3D = cc.getMethod("is3D"); } catch (NoSuchMethodException ignored) {}
}
if (containsY == null) {
try { containsY = cc.getMethod("containsY", int.class); } catch (NoSuchMethodException ignored) {}
}
...
That guard is a cache that can only ever store a hit. A method the claim class does expose is resolved once and the guard closes. A method it does not expose leaves the guard open forever, so getMethod runs again on the next claim, and the next, and every one after — and each miss constructs a NoSuchMethodException.
Constructing an exception is not cheap. fillInStackTrace walks the live stack and materialises a frame array, which is among the most expensive routine operations a JVM performs, and it happens whether or not anyone ever reads the trace. Here nobody did: the catch block is ignored. The plugin was paying full price for a stack capture, tens of frames deep, to discard it — inside the innermost loop of a lookup whose correct answer was "no claim here."
Multiply it out. A miss swept every claim on the server, twice. The map editor probed a 20x20 tile at 400 points and drew 45 tiles per repaint. On a server with a few thousand claims, one inventory click is on the order of millions of stack captures. Ten seconds of unresponsive server thread is the expected result, not a surprising one.
Resolution is now hoisted out of the loop and routed through the handle cache introduced in v1.1.17, which stores misses as well as hits. A method that does not exist is looked up once, ever, and the loop body reduces to the invoke calls that were always the point.
Why this only bites under version skew
On GriefPrevention3D 18.2.7 — the current build target — every accessor the sweep probes is public on Claim: is3D(), containsY(int), getArea(), getGreaterBoundaryCorner(), getLesserBoundaryCorner(). Nothing misses, so every guard closes on the first claim and no exception is ever built.
That is what makes this failure mode nasty. Under matched versions the sweep is merely expensive — O(claims) reflective invokes. It degrades into an exception storm only when GPExpansion probes for something the GriefPrevention build in front of it does not expose: an older fork, a newer one that renamed an accessor, or a divergent third-party build. The plugin gets slower in exact proportion to how unfamiliar the fork is, which is the opposite of how graceful degradation is supposed to work.
The reporting server was running a GPExpansion build older than v1.1.17 — its GPBridge line numbers do not match any current source — so the specific accessor that was missing there cannot be identified from the dump. The mechanism is unambiguous regardless: Class.getMethod reached its throw site, and the frame above it is fillInStackTrace.
Behaviour Changes
- Accessor resolution now samples only the first claim's class. The old code re-attempted resolution per claim, so in a collection mixing claim types it could eventually pick up a method that the first element lacked. Resolution now happens once, against
claims.get(0).getClass(). GriefPrevention stores a singleClaimtype inDataStore.claims, so this is theoretical rather than a change anyone can observe — but it is a real narrowing, and it is the assumption the old guards were already making in the common case. - A missing
containsmethod now returns null immediately instead of throwing out of the loop and being caught by the enclosing handler. Same result, one less exception.
Compatibility
Unchanged from v1.1.17. Built and verified against GriefPrevention3D 18.2.7.
The reachability rule set in v1.1.17 still holds: bruteForceFindClaim runs only when the resolved DataStore signature is the legacy two-argument getClaimAt(Location, boolean). GriefPrevention3D resolves to the four-argument form and upstream GriefPrevention to the three- or four-argument form, so neither reaches it. This release is what happens when a fork does.
Notes
- No API, command, permission, placeholder, or configuration changes.
- Verified by a clean
mvn packageagainst GriefPrevention3D 18.2.7, and by confirming in the GriefPrevention3D source that all five probed accessors are public onClaim— which is why the exception path does not trigger under matched versions. - Not run on a live server, and the watchdog trip has not been reproduced or confirmed fixed against a real workload. The reasoning is mechanical: the exception construction is removed because the lookup that caused it is removed. Confirmation would be a
/claimmapclick at the 20x20 and 200x200 zooms on the reporting server's fork, without a watchdog trip. - Still outstanding, carried over:
BanEnforcementListener's ejection path and/claim banstill fall back togetHighestBlockYAt(), so ejecting from a nether claim can deposit a player on the roof.getSafeDestination()uses-1as its "no ground found" sentinel, which collides with a genuine ground block at y=-1 in 1.18+ worlds.ClaimDataStoreremains a plainHashMapwith no synchronisation.
