SpigotMC/Spigot — pull request #158 by eah2119 (2014-05-06 21:36:43+00:00)
Title: Add alternative hopper ticking

This change offers a config option intending to improve hopper ticking performance while maintaining vanilla hopper behavior. Rather than polling each hopper each tick, this alternative method relies on inventory changes and redstone updates to schedule a hopper tick.

Related to this bug report: http://www.spigotmc.org/threads/default-hopper-check-tweaking.3704/

It's not really a bug, but it sacrifices vanilla behavior for speed. Many servers are making the same choice and I'd much rather have the amazingness of automatic hopper machines without the cost of speed.

———

The vanilla hopper ticking rules are as follows.

1. A hopper update consists of an attempt to push an item followed by an attempt to pull an item or items.

2. The conditions to push an item are as follows:
a. this hopper is not powered by redstone
b. this hopper is not empty
c. this hopper has a non-full tile entity on its output face

3. The conditions to pull an item are as follows:
a. this hopper is not powered by redstone
b. this hopper is not full
c. and either of the following:
i. this hopper has a non-empty tile entity directly above
ii. this hopper has an item entity above
iii. this hopper has a non-empty mine cart inventory above

4. If a hopper succeeds in a push or a pull, then its cooldown is set to 8. If it does not succeed in either, then its cooldown is set to 0 and is updated the following tick.

5. When a hopper *gains* an item or items (be this by a player, a plugin, a dropper, another hopper, etc.), then its cooldown is set to 8.

6. A hopper’s cooldown is the time in ticks that the hopper will receive another update.

7. The cooldown is loaded from and saved to disk upon chunk load and unload, respectively.

Some consequences of these rules:

1. In any consecutive 8 ticks, each hopper will receive at least one update.

2. If a hopper is updated more than once in any consecutive 8 ticks, than only one of these updates will cause a change in contents. The rest will not be observed. Thus, we can avoid ticking some hoppers for some ticks while still maintaining vanilla behavior.

———

Instead of polling each hopper each tick, only update hoppers when there would be an observable change in the contents of tile entities. If a hopper update makes no observable change, it is “inactive” and we do not actually call an update. If a hopper update *does* make an observable change, it is “active”.

A hopper will either be scheduled to tick or it will not. If it is scheduled to tick sometime in the future, it is “active”. If it is not scheduled to tick, it is “inactive”.

Since hoppers are no longer polling for items, minecart inventories, and powered by redstone changes, we must do the polling in the reverse direction - make redstone changes, items entities, and minecart inventories do the polling and hopper ticking.

For hoppers: When a hopper is added or changed, then this hopper must be activated. We do this by scheduling an update next tick if there is not one scheduled already.

For hoppers: When a hopper becomes unpowered by redstone, this hopper must be activated. We do this by scheduling an update next tick if there is not one scheduled already.

For all tile entities: When a tile entity is added or changed and if there is a hopper below, then this hopper must be activated. We do this by scheduling an update next tick if there is not one scheduled already.

For all tile entities: When a tile entity is added or changed and if there are hoppers outputting to this inventory, then these hoppers must be activated. We do this by scheduling an update next tick if there is not one scheduled already.

For item entities: Make hoppers below this entity active. We do this by scheduling an update next tick if there is not one scheduled already. This is done each time the item is ticked.

For minecart inventories: Make hoppers around this entity active. We do this by scheduling an update next tick if there is not one scheduled already. This is done each time the item or minecart is ticked.

Finally, in the case that (for whatever reason) a hopper is inactive when it should be active, we activate a hopper when it receives a random block update. This is analogous to how redstone repeaters, torches, etc. will change to the correct state when receiving a random block update.

Under the polling paradigm, when a hopper update finishes, the hopper will be in a state where it will either update again the following tick or in 8 ticks. In changing to a scheduled update paradigm, if an update ends with a cooldown of 0, the hopper never succeeded in a push or pull. Thus, it is inactive and need not receive another update until it is reactivated. If, however, it ends with a cooldown of 8, another update needs to be scheduled with a delay of 8 ticks.

———

The way I have implemented these rules is through a nextTick long value. It holds the time at which this hopper will be ticked next. Each tick, a hopper checks if nextTick is equal to the current time. If it is, it updates. If it is not, it doesn’t. A hopper is active if the nextTick value is such that this hopper is guaranteed to tick sometime in the future. It is inactive otherwise.

Each tick, each hopper also has a lastTick long value. It holds the last time this hopper’s turn came around in the tick loop. Consider the following scenario:

If it is tick 387, but this hopper has not been ticked in this tick yet, then lastTick is 386. If we want to schedule it to tick the next time it gets the chance, then we set nextTick to 387.

If, however, the tick is 387, but this hopper *has* been ticked in this tick, then lastTick is 387. If we want to schedule it to tick the next time it gets the change, then we set nextTick to 388.

If we want a hopper to update the next chance it gets, we cannot simply use the current tick time. We must also use the value of lastTick. Similarly, if we want to determine whether a hopper is active (aka cooling down), we must use the value of lastTick.

———

A note about the config: This adds a config option, boolean hopper-alt-ticking. If it is false, use the previous method of ticking with hopper-check. If it is true, use this alternative ticking. When true, hopper-check does nothing.

This does not completely bypass idle hoppers in the game loop (they still lag the server somewhat). I feel this would require many more code insertions. A general rule is this option decreases ticking time for idle hoppers and increases ticking time for active hoppers (the opposite of what hopper-check 8 does). Here are some benchmarks I took.

For a world with a 24x24x24 idle hopper brick:
With this patch:
With hopper-alt-ticking false and hopper-check 8: http://aikar.co/timings.php?url=7390246
With hopper-alt-ticking false and hopper-check 0: http://aikar.co/timings.php?url=7390256
With hopper-alt-ticking true: http://aikar.co/timings.php?url=7390261
Same build, but without this patch:
With hopper-check 8: http://aikar.co/timings.php?url=7390285
With hopper-check 0: http://aikar.co/timings.php?url=7390275

For a world with 24x24x24 hoppers, each pushing items:
With this patch:
With hopper-alt-ticking false and hopper-check 8: http://aikar.co/timings.php?url=7390309
With hopper-alt-ticking false and hopper-check 0: http://aikar.co/timings.php?url=7390315
With hopper-alt-ticking true: http://aikar.co/timings.php?url=7390318
Same build, but without this patch:
With hopper-check 8: http://aikar.co/timings.php?url=7390337
With hopper-check 0: http://aikar.co/timings.php?url=7390326

Based off of that data,

false & 8 outperforms false & 0 when the number of idle hoppers is greater than 8.361% of the total

true outperforms false & 0 when the number of idle hoppers is greater than 44.41% of the total

true outperforms false & 8 when the number of idle hoppers is greater than 87.155% of the total

Put simply, if you have a lot of idle hoppers, make hopper-alt-ticking true. Else, you’re better off with another option depending on how much you want to trade off performance for vanilla behavior.

The additional logic and config checks added in this commit increase ticking time by about 1%, so I think it’s negligible for what this provides.

———

World used for the first speed test: https://www.dropbox.com/s/j15k5dyjvtxkyc0/hopperBrick.zip

The second speed test: https://www.dropbox.com/s/o877rk2a2itp8f5/hopperChanging.zip

And behavior test: https://www.dropbox.com/s/ot8f7f5r4v1xf96/hoppertest.zip

The behavior test attempts to be sure hoppers behave correctly. There are a few mechanisms there that demo the fault of hopper-check 8, notably an auto smelter and auto distributor. They work fine with hopper-check 0 or alt-hopper-ticking true, but do not distribute items evenly with hopper-alt-ticking false and hopper-check 8.