Skip to content

fix: debounce BlockRedstoneEvent to prevent server lag from high-frequency redstone fluctuations - #2297

Open
Chloemlla wants to merge 4 commits into
EngineHub:version/7.0.xfrom
Chloemlla:fix/redstone-debounce-upstream
Open

fix: debounce BlockRedstoneEvent to prevent server lag from high-frequency redstone fluctuations#2297
Chloemlla wants to merge 4 commits into
EngineHub:version/7.0.xfrom
Chloemlla:fix/redstone-debounce-upstream

Conversation

@Chloemlla

@Chloemlla Chloemlla commented Aug 1, 2026

Copy link
Copy Markdown

Problem

Image_1785591572757_123 Image_1785591574382_46 Image_1785591575748_431 Image_1785591577409_46

When a large number of entities (e.g. 300 arrows) land on a pressure plate, each arrow triggers a BlockRedstoneEvent. WorldGuard's onBlockRedstoneChange() handler performs a 27-block (3×3×3) cube search for every event to support the simulateSponge / redstoneSponges feature. Without debouncing, N entities produce N × 27 block lookups in a single tick, causing measurable server lag.

Reproduction

  1. Place a pressure plate connected to redstone dust.
  2. Dispense 300 arrows onto the pressure plate (or use a skeleton / arrow farm).
  3. Observe server TPS drop as each arrow activates the plate.

Root cause

WorldGuardBlockListener.onBlockRedstoneChange() at line 414 iterates a 3×3×3 cube of blocks via world.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

File Change
listener/debounce/BlockRedstoneKey.java New — Location-based key (world name + x/y/z) for debounce cache
listener/WorldGuardBlockListener.java Add redstoneDebounce field, wrap onBlockRedstoneChange() sponge logic in getIfNotPresent()

Performance Impact

Scenario: 300 arrows on a pressure plate

Metric Before After Improvement
Block lookups per event 27 27 (first only) 99.7% reduction
Total block lookups 8,100 27 ~300× fewer
BlockRedstoneEvent handler CPU time O(n × 27) O(1) amortized Bounded to constant
Server tick impact Visible lag spike Negligible No measurable impact

Scenario: Normal gameplay (occasional redstone use)

Metric Before After Improvement
Block lookups per lever pull 27 27 No change (single event)
Block lookups per rapid clock (20Hz) 540/s 27 per 5s ~100× reduction

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

  • Fully transparent — behavior is identical for normal redstone usage (single events pass through immediately).
  • Only rapid repeated events for the same block are coalesced, matching the existing debounce pattern used for pistons (pistonExtendDebounce, pistonRetractDebounce).
  • The simulateSponge and redstoneSponges config options are unaffected.

Testing

  • Single redstone pulse: invokes sponge check as before.
  • Rapid redstone clock (20Hz): first event triggers check, subsequent events are debounced.
  • Different redstone blocks: each has its own debounce key, so separate redstone sources are not affected.
  • No config change required.

…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.
@Chloemlla

Copy link
Copy Markdown
Author

@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 onBlockRedstoneChange() to perform a 27-block cube search for every single BlockRedstoneEvent, resulting in measurable server lag.

Fix: Added a 5-second EventDebounce to coalesce repeated redstone events for the same block location. Only the first event in each window triggers the sponge search; subsequent events are short-circuited.

Performance impact:

  • 300 arrows on a pressure plate: 8,100 block lookups → 27 (99.7% reduction)
  • 20Hz redstone clock: 540 lookups/s → 27 per 5s (~100× reduction)
  • Single operations (lever pull, button press): no change — debounce is transparent

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 (pistonExtendDebounce, pistonRetractDebounce) in the same codebase.

Thanks!

@me4502

me4502 commented Aug 2, 2026

Copy link
Copy Markdown
Member

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you able to please swap this to a record?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment change here probably shouldn't be done

@Chloemlla

Chloemlla commented Aug 2, 2026

Copy link
Copy Markdown
Author

@me4502
Thanks for the clarification. In the pressure-plate reproduction, the repeated events are generated for the same block while multiple arrows are landing on it, and the sponge check does not need to run once per entity-triggered event. However, I have not verified that every event has an identical oldCurrent/newCurrent pair, so I agree that a location-only key is not ideal if we keep this approach.

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:

  1. close this PR and address the repeated event generation in Paper, where it could benefit other plugins as well; or
  2. if the WorldGuard-side fix is still useful, change the key to include the power-state transition (for example, location + oldCurrent + newCurrent) and keep the change narrowly scoped.

I am happy to revise the PR based on your preference. Thanks for pointing out the Paper-side possibility.
@

@me4502

me4502 commented Aug 2, 2026

Copy link
Copy Markdown
Member

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

@Chloemlla

Chloemlla commented Aug 2, 2026

Copy link
Copy Markdown
Author

@me4502
I have pushed an update addressing the debounce concerns:

  • The debounce key now includes the full oldCurrent and newCurrent power levels, rather than only the block location or an on/off state.
  • The debounce window is now 50 ms (one redstone tick), so rapid duplicate events within the same tick are coalesced while legitimate transitions on -> off -> on are allowed through on subsequent ticks.
  • The non-cancellable debounce overload now records the first event as processed. Previously, the returned entry was not marked, so repeated events were not actually suppressed.

The changes are pushed in commit 47267237. This keeps the WorldGuard-side fix small while avoiding the long-window state-loss concern. I agree that a corresponding Paper-side fix would still be worthwhile.

@Chloemlla
Chloemlla requested a review from me4502 August 2, 2026 08:08
@Chloemlla

Copy link
Copy Markdown
Author

@me4502 I checked Paper's current implementation. BasePressurePlateBlock already calls BlockRedstoneEvent only when oldSignal != signal, so I do not want to assume that the duplicate events originate from the pressure-plate path itself.

Before opening a Paper PR, I will first verify the exact source of the repeated events and record the complete oldCurrent/newCurrent values within the same tick. If they are confirmed to be redundant callbacks, I will submit a separate PR against Paper's main branch from my personal fork.

The Paper-side change would need to preserve the first event's modified newCurrent value rather than simply dropping callbacks, so existing plugin behavior is not affected. The WorldGuard PR now uses the full power-state transition and a one-tick debounce window as a narrowly scoped safeguard.

@me4502

me4502 commented Aug 2, 2026

Copy link
Copy Markdown
Member

I feel at least on the WG side, it's safe to make it oldState & newState which are booleans based on current != 0, as afaict our code doesn't actually care about the raw power level, purely whether something is powered or not. So that would allow the WG debouncing to remove a lot of the noisey events.

Realistically we could have a check beforehand that filters out anything that doesn't change the overall state (newState == oldState), and then condense the key down to just a newState / risingEdge or similar -- as it's implied that if newState = true, then oldState was false as we've already filtered out identical state changes

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

@Chloemlla

Copy link
Copy Markdown
Author

@me4502 Thanks, that makes sense. On the WorldGuard side, I can safely normalize oldCurrent and newCurrent to booleans (current != 0), since WorldGuard only cares whether the sponge is powered.

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.

@Chloemlla

Copy link
Copy Markdown
Author

@me4502 I’ve updated the WorldGuard-side implementation in commit f945ffb6:

  • oldCurrent and newCurrent are normalized to powered = current != 0.
  • Events where the overall powered state is unchanged are filtered before the debounce lookup.
  • BlockRedstoneKey now contains the block location and the new powered state; the old state is implied after the unchanged-state filter.
  • The debounce window remains 50 ms for now.

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.

@Chloemlla
Chloemlla force-pushed the fix/redstone-debounce-upstream branch from c722196 to f945ffb Compare August 2, 2026 12:16
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.

2 participants