Honestly, some of these changes felt like hopium "surely this will fix the lag issues", even tho in my mind these didn't make sense Yeah, these methods do show up in the profiler, but does these *really* make sense? I don't think so, I think these are just useless patches that only attempt to hide the pain, instead of tackling the REAL issue that is causing these methods to show up in the profiler
✨ SparklyPaper ✨
SparklyPower's Paper fork, with blazing high-performance improvements for large servers!
Our fork has handmade patches to add and optimize some of the things that we have in our server, with some cherry-picked patches from other forks.
Features
This does not include all patches included in SparklyPaper, only the patches handmade for SparklyPaper! To see all patches, check out the "patches" directory.
SparklyPaper's config file is sparklypaper.yml, the file is, by default, placed on the root of your server.
- Configurable Farm Land moisture tick rate when the block is already moisturised
- The
isNearWatercheck is costly, especially if you have a lot of farm lands. If the block is already moistured, we can change the tick rate of it to avoid these expensiveisNearWaterchecks. - (Incompatible with the Blazingly Simple Farm Checks feature)
- The
- Skip
distanceToSqrcall inServerEntity#sendChangesif the delta movement hasn't changed- The
distanceToSqrcall is a bit expensive, so avoiding it is pretty nice, around ~15% calls are skipped with this check. Currently, we only check if both Vec3 objects have the same identity, that means, if they are literally the same object. (that works because Minecraft's code reuses the Vec3 object when caching the current delta movement)
- The
- Skip
MapItem#update()if the map does not have the defaultCraftMapRendererpresent- By default, maps, even those with custom renderers, fetch the world data to update the map data. With this change, "image in map" maps that have removed the default
CraftMapRenderercan avoid these hefty updates, without requiring the map to be locked, which some old map plugins may not do. - This has the disadvantage that the vanilla map data will never be updated while the CraftMapRenderer is not present, so if you readd the default renderer, the server will need to update the map data, but that's not a huuuge problem, after all, it is a very rare circumstance that you may need the map data to always be up-to-date when you have a custom renderer on the map.
- But still, if you made your own custom "image on map" plugin, don't forget to
mapView.isLocked = trueto get the same performance benefits in vanilla Paper!
- By default, maps, even those with custom renderers, fetch the world data to update the map data. With this change, "image in map" maps that have removed the default
- Fix concurrency issues when using
imageToBytesin multiple threads- Useful if one of your plugins is parallelizng map creation on server startup
- Skip dirty stats copy when requesting player stats
- There's literally only one
getDirtycall. Because the map was only retrieved once, we don't actually need to create a copy of the map just to iterate it, we can just access it directly and clear it manually after use.
- There's literally only one
Avoid unnecessaryItemFrame#getItem()callsWhen ticking an item frame, on each tick, it checks if the item on the item frame is a map and, if it is, it adds the map to be carried by the entity playerHowever, thegetItem()call is a bit expensive, especially because this is only really used if the item in the item frame is a mapWe can avoid this call by checking if thecachedMapIdis not null, if it is, then we get the item in the item frame, if not, then we ignore thegetItem()call.- Replaced by Warriorrrr's "Rewrite framed map tracker ticking" patch (Paper #9605)
- Optimize
EntityScheduler'sexecuteTick- On each tick, Paper runs
EntityScheduler'sexecuteTickof each entity. This is a bit expensive, due toArrayDeque'ssize()call because it ain't a simple "get the current queue size" function, due to the thread checks, and because it needs to iterate all server entities to tick them. - To avoid those hefty calls, instead of iterating all entities in all worlds, we use a set to track which entities have scheduled tasks that we need to tick. When a task is scheduled, we add the entity to the set, when all entity tasks are executed, the entity is removed from the set. We don't need to care about entities that do not have any tasks scheduled, even if the scheduler has a
tickCount, becausetickCountis relative and not bound to the server's current tick, so it doesn't matter if we don't increase it. - Most entities won't have any scheduled tasks, so this is a nice performance bonus, even if you have plugins that do use the entity scheduler because, for 99,99% of use cases, you aren't going to create tasks for all entities in your server. With this change, the
executeTickloop intickChildrenCPU % usage drops from 7.60% to 0.00% (!!!) in a server with ~15k entities! Sweet!- Yeah, I know... "but you are cheating! the loop doesn't show up in the profiler because you replaced the loop with a for each!" and you are right! Here's a comparison of the
tickChildrenfunction CPU usage % between vanilla Paper and SparklyPaper, removing all other functions from the profiler result: 7.70% vs 0.02% (wow, such improvement, low mspt)
- Yeah, I know... "but you are cheating! the loop doesn't show up in the profiler because you replaced the loop with a for each!" and you are right! Here's a comparison of the
- Of course, this doesn't mean that
ArrayDeque#size()is slow! It is mostly that because theexecuteTickfunction is called each tick for each entity, it would be better for us to avoid as many useless calls as possible.
- On each tick, Paper runs
- Blazingly Simple Farm Checks
- Changes Minecraft's farm checks for crops, stem blocks, and farm lands to be simpler and less resource intensive
- If a farm land is moisturised, the farm land won't check if there's water nearby to avoid intensive block checks. Now, instead of the farm land checking for moisture, the crops themselves will check when attempting to grow, this way, farms with fully grown crops won't cause lag.
- The growth speed of crops and stems are now fixed based on if the block below them is moist or not, instead of doing vanilla's behavior of "check all blocks nearby to see if at least one of them is moist" and "if the blocks nearby are of the same time, make them grow slower".
- In my opinion: Who cares about the vanilla behavior lol, most players only care about farm land + crop = crop go brrrr
- Another optimization is that crop behavior can be changed to skip from age zero to the last age directly, while still keeping the original growth duration of the crop. This way, useless block updates due to crop growth can be avoided!
- Lazily create
LootContextfor criterions- For each player on each tick, enter block triggers are invoked, and these create loot contexts that are promptly thrown away since the trigger doesn't pass the predicate.
- To avoid this, we now lazily create the LootContext if the criterion passes the predicate AND if any of the listener triggers require a loot context instance.
- Spooky month optimizations
- The quintessential patch that other performance forks also have for... some reason??? I thought that this optimization was too funny to not do it in SparklyPaper.
- Caches when Bat's spooky season starts and ends, and when Skeleton and Zombies halloween starts and ends. The epoch is updated every 90 days. If your server is running for 90+ days straight without restarts, congratulations!
- Avoids unnecessary date checks, even tho that this shouldn't really improve performance that much... unless you have a lot of bats/zombies/skeletons spawning.
- Cache coordinate key used for nearby players when ticking chunks
- The
getChunkKey(...)call is a bit expensive, using 0.24% of CPU time with 19k chunks loaded. - So instead of paying the price on each tick, we pay the price when the chunk is loaded.
- Which, if you think about it, is actually better, since we tick chunks more than we load chunks.
- The
- Optimize
canSee(...)checks- The
canSee(...)checks is in a hot path (ChunkMap#updatePlayers()), invoked by each entity for each player on the server if they are in tracking range, so optimizing it is pretty nice. - First, we change the original
HashMapto fastutil'sObject2ObjectOpenHashMap, because fastutil'scontainsKeythroughput is better. - Then, we add a
isEmpty()check before attempting to check if the map contains something. This seems stupid, but it does seem that it improves the performance a bit, and it makes sense,containsKey(...)does not attempt to check the map size before attempting to check if the map contains the key.
- The
- Cache last
shouldTickBlocksAtresult when ticking block entities- The
shouldTickBlocksAtcall is expensive, it requires pulling chunk holder info from an map for each block entity (even if the block entities are on the same chunk!) every single time - So here's a quick and dirty optimization: We cache the last
shouldTickBlocksAtresult and, if the last chunk position is the same as our cached value, we use the last cachedshouldTickBlocksAtresult! - We could use a map for caching, but here's why this is way better than using a map: The block entity ticking list is sorted by chunks! Well, sort of... It is sorted by chunk when the chunk has been loaded because all loaded block entities are appended to the list, however, this also means that newly placed blocks will be appended to the end of the list until the chunk unloads and loads again.
- But here's the thing: We don't care if this is bad, the small performance hit of when a player placed new block entities is so small ('tis just a long comparsion after all), that the huge performance boost from already placed block entities is way bigger.
- Most block entities are things that players placed to be there for a long time anyway (like hoppers, etc), and the block entities will be automatically sorted after the chunk is unloaded and loaded again, so it ain't that bad.
- We also cache the chunk's coordinate key when creating the block entity, which is actually "free" because we just reuse the already cached chunk coordinate key from the chunk!
- The
- Check how much MSPT (milliseconds per tick) each world is using in
/mspt - Parallel World Ticking
- "mom can we have folia?" "we already have folia at home" folia at home: Parallel World Ticking
While we could cherry-pick everything from other forks, only patches that I can see and think "yeah, I can see how this would improve performance" or patches that target specific performance/feature pain points in our server are cherry-picked! In fact, some patches that are used in other forks may be actually borked...
Support
Because this is a fork made for SparklyPower, we won't give support for any issues that may happen in your server when using SparklyPaper. We know that SparklyPaper may break some plugins, but unless we use these plugins on SparklyPower, we won't go out of our way to fix it!
If you only care about some of the patches included in SparklyPaper, it is better for you to create your own fork and cherry-pick the patches, this way you have full control of what patches you want to use in your server, and even create your own changes!
Downloads
There are two kinds of builds: One with the Parallel World Ticking feature, and another without it. If you don't want to risk using a very experimental feature that may lead to server crashes and corruption, or if you aren't a developer that can't fix plugin issues related to the feature, then use the version without Parallel World Ticking! (We do run Parallel World Ticking in production @ SparklyPower tho, we live on the edge :3)
- SparklyPaper: https://github.com/SparklyPower/SparklyPaper/actions/workflows/build.yml
- SparklyPaper (without Parallel World Ticking): https://github.com/SparklyPower/SparklyPaper/actions/workflows/build-without-pwt.yml
Click on a workflow run, scroll down to the Artifacts, and download!

