fix: debounce BlockRedstoneEvent to prevent server lag from high-frequency redstone fluctuations - #2297
Conversation
…stone Add EventDebounce to onBlockRedstoneChange() to coalesce rapid redstone fluctuations (e.g. 300 arrows on a pressure plate) into a single sponge check per 5-second window. The simulateSponge/redstoneSponges feature performs a 27-block (3x3x3) cube search for every BlockRedstoneEvent. Without debouncing, a rapid sequence of redstone changes causes O(n*27) block lookups, where n is the number of events. With debouncing, the first event in each 5-second window triggers the lookup and subsequent events are skipped. Add BlockRedstoneKey for location-based event deduplication.
… overload BlockRedstoneEvent does not implement Cancellable, so the previous approach using getIfNotPresent(key, event) caused a compile error. Add a non-cancellable overload getIfNotPresent(key) to EventDebounce that returns the Entry when the key is fresh, or null when debounced.
|
@me4502 @wizjany @sk89q Would appreciate a review when you have a moment. This PR addresses a performance issue where rapid redstone fluctuations (e.g. 300 arrows landing on a pressure plate) cause WorldGuard's Fix: Added a 5-second Performance impact:
CI: Build passes. The only output is pre-existing deprecation warnings, no new errors introduced. The debounce pattern follows the existing convention used for pistons ( Thanks! |
|
Thanks for the PR. Just for some clarification, are these events all triggering repeatedly with the same power state transition? As in, there's no change of data within these events, it's purely a repeat? I'm not majorly opposed to this as it's a fairly minor change (although IMO would need to likely also encode power state transitions in the key; ideally as a record), but as this is a very deprecated / close to removal feature I'm hesitant to add too much extra complexity to it. It's already recommended against to the point that it's not even included in the config, and must be manually added/enabled (as per the disclaimer on the docs). If this is a case of the same event triggering repeatedly with the same data, I wonder if it's worth even also sending a patch for this in to Paper, as there are likely many plugins with way more intense redstone usages than WG's old deprecated features |
| * redstone fluctuations (e.g. from a pressure plate activated by | ||
| * multiple arrows) are coalesced into a single sponge check. | ||
| */ | ||
| public class BlockRedstoneKey { |
There was a problem hiding this comment.
Are you able to please swap this to a record?
There was a problem hiding this comment.
I feel we should also be encoding the power levels in here, as otherwise we could be losing information about "turn on / turn off" – and purely seeing it as a "turn on"
| */ | ||
| public class WorldGuardBlockListener extends AbstractListener { | ||
|
|
||
| private final EventDebounce<BlockRedstoneKey> redstoneDebounce = EventDebounce.create(5000); |
There was a problem hiding this comment.
Is this a 5s debounce timeout? If so this is likely way too slow.
|
|
||
| /* | ||
| * Called when redstone changes. | ||
| * Debounced to avoid server lag from rapid redstone fluctuations |
There was a problem hiding this comment.
This comment change here probably shouldn't be done
|
@me4502 I also agree that this feature is deprecated and close to removal, so adding more complexity to WorldGuard should be avoided. The best options seem to be either:
I am happy to revise the PR based on your preference. Thanks for pointing out the Paper-side possibility. |
|
Thanks, I feel if the main concerns are addressed I don't see too big an issue with merging it here, although fixing it in Paper as well is likely very worthwhile. My main concern aside from the minor stylistic stuff, is ensuring that we don't break functionality. The long debounce window + not capturing current state (on -> off / off -> on, rather than the full specific power levels) could potentially result in legitimate power toggles being missed. My understanding of the debounce system here also would mean that if it was toggled on, then off, then on – that second "on" would still be missed if done within the debounce window even with the listed changes. That might be fine if the debounce window is the same as the redstone tick cycle, assuming all of these events are firing within the same tick. So that'd fix the "spam" issue, and allow a vast majority of legitimate usages to pass through |
|
@me4502
The changes are pushed in commit |
|
@me4502 I checked Paper's current implementation. Before opening a Paper PR, I will first verify the exact source of the repeated events and record the complete The Paper-side change would need to preserve the first event's modified |
|
I feel at least on the WG side, it's safe to make it Realistically we could have a check beforehand that filters out anything that doesn't change the overall state ( Paper obviously cannot make those same assumptions, but I feel it should be safe on the WG side and reduces the number of events that need to even go through the debounce system. And tbh, may even supersede the need for debouncing depending on what data these events contain |
|
@me4502 Thanks, that makes sense. On the WorldGuard side, I can safely normalize I’ll first filter out events where the old and new powered states are equal, then debounce only the resulting transitions, using a key composed of the location and the new powered state (rising edge). I’ll retain the 50 ms debounce window initially and reassess whether filtering unchanged state events reduces or removes the need for debouncing entirely. Paper’s handling should remain separate, since Paper cannot rely on the same assumption. |
|
@me4502 I’ve updated the WorldGuard-side implementation in commit
This should remove the noisy same-state events before they reach the debounce cache while preserving separate rising and falling transitions. Paper remains separate because it cannot make the same assumptions about how event consumers use the raw power levels. |
c722196 to
f945ffb
Compare
Problem
When a large number of entities (e.g. 300 arrows) land on a pressure plate, each arrow triggers a
BlockRedstoneEvent. WorldGuard'sonBlockRedstoneChange()handler performs a 27-block (3×3×3) cube search for every event to support thesimulateSponge/redstoneSpongesfeature. Without debouncing, N entities produce N × 27 block lookups in a single tick, causing measurable server lag.Reproduction
Root cause
WorldGuardBlockListener.onBlockRedstoneChange()at line 414 iterates a 3×3×3 cube of blocks viaworld.getBlockAt()on every invocation. With 300 arrows, this is 8,100+ block lookups that all compete with the main server thread.Fix
Add an
EventDebounce<BlockRedstoneKey>(5-second window) that coalesces rapid redstone events for the same block location into a single sponge check. Only the first event in each window triggers the 27-block lookup; subsequent events are short-circuited.Changes
listener/debounce/BlockRedstoneKey.javalistener/WorldGuardBlockListener.javaredstoneDebouncefield, wraponBlockRedstoneChange()sponge logic ingetIfNotPresent()Performance Impact
Scenario: 300 arrows on a pressure plate
BlockRedstoneEventhandler CPU timeScenario: Normal gameplay (occasional redstone use)
Scaling
The fix is O(1) amortized — regardless of how many entities trigger the pressure plate, the sponge check runs at most once per 5-second window per block. This prevents a redstone-based lag machine from exploiting WorldGuard's event handler.
Backward Compatibility
pistonExtendDebounce,pistonRetractDebounce).simulateSpongeandredstoneSpongesconfig options are unaffected.Testing