Skip block entity snapshots on the dispenser logging path - #1007
Open
tricrotism wants to merge 1 commit into
Open
tricrotism wants to merge 1 commit into
tricrotism wants to merge 1 commit into
Conversation
Every dispense copied the dispenser's block entity twice on the region thread (block.getState() in the dispense listeners and inventory.getHolder() in onInventoryInteract), and both copies were only used for their type, location and live inventory. Paper can hand out non-snapshot states for this, so the dispense path now asks for those through a new PaperInterface.getBlockState(Block, boolean), falling back to a normal snapshot on Spigot.
❌ Deploy Preview for coreprotect failed. Why did it fail? →
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Paper and Folia, every dispense makes CoreProtect copy the dispenser's block entity twice on the region thread, and both copies are discarded right away. This change asks Paper for non-snapshot states on that path. Nothing that reads the states needs a copy.
The problem
When a dispenser fires on Paper or Folia,
BlockPreDispenseEventruns on the region thread that owns the dispenser:BlockPreDispenseListener.onBlockPreDispensecallsblock.getState()(paper/listener/BlockPreDispenseListener.java:87). For a block entity, Bukkit'sgetState()builds a snapshot: a full copy of the dispenser block entity, all 9 item slots included.InventoryChangeListener.inventoryTransaction(String, BlockState, ItemStack[]), which callsonInventoryInteractwithinventoryHolder.getInventory().onInventoryInteractcallsinventory.getHolder()(listener/player/InventoryChangeListener.java:132). On Paper,Inventory.getHolder()for a block inventory builds another full snapshot of the same block entity.Both states are then used only for
getType(),getLocation()andgetInventory(). A dispenser clock or an item sorter that fires every few ticks pays for two block entity copies per shot. Folia pays for them on the region thread that owns the dispenser.BlockDispenseListener.java:67has the sameblock.getState()call. It runs on Spigot, and on Paper for droppers until the pre-dispense event has been seen for a dropper.The fix
paper/PaperInterface.java: newBlockState getBlockState(Block block, boolean useSnapshot), next to the existinggetHolder(Inventory, boolean).paper/PaperAdapter.java(Spigot and non-Paper): returnsblock.getState(), same as today.paper/PaperHandler.java(Paper, inherited by all Paper adapters): calls Paper'sblock.getState(useSnapshot). After aLinkageErrorit falls back togetState()for good. This copies howgetHolderis already handled in the same class.paper/listener/BlockPreDispenseListener.javaandlistener/block/BlockDispenseListener.java: the dispenser state comes fromPaperAdapter.ADAPTER.getBlockState(block, false).listener/player/InventoryChangeListener.java:onInventoryInteractuses the existingPaperAdapter.ADAPTER.getHolder(inventory, false), whichonInventoryInteractAsyncand the hopper listeners already use.I went through the adapter instead of calling
block.getState(false)directly becausegetState(boolean)is Paper-only API and the listeners also load on Spigot.BlockDispenseListeneris included so that the Paper dropper case gets the same treatment. On Spigot that line does exactly what it did before.Behaviour change
None. Spigot still takes full snapshots.
Risk
A non-snapshot state is a live view of the block entity, so the question is whether anything reads block entity data from it later or on another thread. I followed every reader:
BlockPreDispenseListenerandBlockDispenseListener:((InventoryHolder) state).getInventory().getStorageContents(), on the region thread during the event.inventoryTransaction(String, BlockState, ItemStack[]):getType(),instanceof InventoryHolder,getInventory(),getLocation(), all on the calling thread before it returns. The state is not stored.onInventoryInteract: the holder is used only forinstanceof,getType()andgetLocation()on the calling thread.getLocation()is cloned before the lambda.ContainerTransactionDispatcherand the consumer is theInventoryobject, the copiedItemStack[](ItemUtils.getContainerState, still on the region thread), the cloned location and the type. NoBlockStategets there.TileStateInventoryHolder.getInventory()returns the live block inventory whether or not the state is a snapshot ("If this block state is not placed this will return the captured inventory snapshot instead"). So the inventory and contents handed on are the same objects and values as before.onInventoryInteractis shared by the otherinventoryTransactioncallers: player container interaction, lectern book take, copper golem andCoreProtectAPI.logContainerTransaction. They all reach the holder only through the same three reads, so they behave the same. An API caller that runs this off the owning thread already reads world state off-thread throughlocation.getBlock().getState(). This change does not make that worse.Older Paper builds without
Block.getState(boolean)hit theLinkageErrorfallback once and continue with snapshots.Testing
Build:
mvn packagepasses.Performance, PathLoad
dispensersmode, 2 GB heap, autosave off: 100 dispensers fired per tick (2,000 per second) for 60 s after a 60 s warmup, timing each dispense call. Two runs of each jar, plus one run with no CoreProtect for the floor.Average tick time on Folia during the run: 8.9 / 8.6 ms upstream, 5.7 / 5.5 ms with this branch, 4.5 ms with no CoreProtect.
Dispensers firing bone meal at moss (PathLoad
bonemealmode, 50 dispenses per tick, same setup) go through the same dispense path plus the bone meal growth logging:Average tick time on Folia during the bone meal run: 5.0 / 5.2 ms upstream, 3.3 / 3.2 ms with this branch, 2.6 ms with no CoreProtect.
Row parity: the 47-step scenario on Paper 26.2 and Folia 1.21.11 with SQLite, compared order-insensitively against upstream. The only differing rows are the random plant that bone meal grows and one far-basin water row whose settling window lands either side of the boundary depending on run pacing (seen on upstream-to-upstream reruns too). The dispense and container rows are identical.