Skip to content

Implementation

BallOfEnergy edited this page Oct 13, 2025 · 4 revisions

This page goes over how each part of spool is implemented, mostly for compatibility's sake (though it's also quite interesting!)

Threading Options

Dimension Threading

Dimension threading takes the loaded dimensions and runs them in parallel. This is the most basic form of threading that Spool offers. While it may be simple, some incompatibilities may still arise. These would be related to cross-dimensional transport (players, items, fluids, signals, etc.).

Dimension threading, by nature, can only make the server run as fast as the slowest dimension (generally the overworld). This means that, if the Nether takes 10ms to finish ticking, and the Overworld takes 20ms to finish ticking, the effective MSPT would be 20ms (without Spool, this would be ~30ms).

Beginning of tick

At the beginning of every tick, Spool will check all of the loaded dimensions. If a dimension has been newly loaded since the last tick (the dimension manager does not have a thread for that dimension), a thread will be created. Dimensions that have been unloaded are also checked, as if a dimension is unloaded (the dimension manager has an extra thread that isn't in the list of loaded dimensions), the thread will be destroyed and the resources will be given up for use elsewhere.

During tick

During each tick, all (loaded) dimensions will be run in parallel with each other.

End of tick

At the end of each tick, if B:"Allow processing during sleep?" is false, the thread manager will wait for tasks to finish processing. If this option is true, then Spool will allow the tasks to continue processing throughout the rest of the end of the tick (including sleep time), all the way until the beginning of the next tick. At this point, the manager will then wait until all tasks are finished processing.

Distance-based Threading

Distance threading is Spool's most complex type of threading. This allows players that are far enough apart to run on separate executors. This comes with a lot of overhead, and only introduces performance gains on servers with multiple players. The option will automatically be disabled if it is enabled on a singleplayer world (LAN servers are compatible; it will be re-enabled upon another player joining, then destroyed if there is one remaining player).

Distance threading can allow multiple "large bases" to run in parallel with each-other, allowing for much higher performance, especially on larger servers. While more players on a server normally means lower performance, with this option enabled, more players on a server can sometimes mean much better performance, as the bases can be spread out across multiple threads.

The distance threading manager holds a map of all executors in the server. Players will generally have a single executor bound to them. If two players are close enough together, they will be assigned to the same executor. A player will never have more than one executor.

Beginning of each tick

In the beginning of the tick, all of the caches from the previous tick are cleared. These caches exist so that the manager does not have to constantly recalculate distances and executors millions of times per second.

During the tick

Whenever a block, chunk, or entity is updated (their update function is called), Spool catches the object and calls its own routine.

First, the executor for that object is found. Spool uses the object's position to find the nearby player(s), and sets the executor to be used to the executor of the nearest player. If the nearest player does not have an executor (happens when they have just joined the server), one is created and assigned to them. If no players are online, the updates will be executed on the server's main thread, and this is the last step.

If the chunk that the object is in is force-loaded (chunk loaded using FML's ticket system), a different routine will be followed. First, a special chunk executor map will be checked for the chunk that the object is in. If there's an entry, the object is executed on that chunk's stored executor.

If there isn't an executor entry for that chunk, the nearest player (even if they are outside the range) is found (and their executor stored for future use). If there is no nearest player (nobody online), the updates will be executed on the server's main thread, and this is the last step. After this, Spool initiates its loaded chunk flood-fill algorithm. This takes the chunk that the object being updated is in, and assigns it as the seed for the flood-fill. Next, force-loaded chunks adjacent to the seed will be recursively assigned the seed chunk's executor (executor of the closest player; stored earlier) in a special map. This special map allows force-loaded chunks to find an executor without having to recalculate every time. After this operation is completed, all loaded chunks making up the "blob" of loaded chunks will be inside the special "chunk executor map".

The entire flood-fill system and force-loaded chunk handling system is made to ensure with absolute certainty that adjacent loaded chunks (forced in this case, however the premise applies to all chunks) should never be adjacent with different executors. Adjacent chunks with different executors (bordering each-other) can cause massive desync and potential concurrency issues (causing crashes or indeterminable behavior).

Second, if there are multiple players nearby to the object being updated, their executors will be synchronized. This process that ensures that executors will never overlap the same area (to avoid conflicts and desyncs). Using the players' join times, an executor is chosen as the "prioritized executor". This is the executor that will be synchronized across all players in range. The nearby players then have their executors set to this prioritized executor, with their original executors being torn down and destroyed (with resources being returned to the system for reuse).

At this point, an executor has been found, regardless of object location. The object will then be executed using the executor found in the previous steps (asynchronously).

End of tick

At the end of the tick, all maps (both player and chunk) are checked for "instabilities". A map is considered unstable if each element in the map does not have an executor mapping. This can happen when a player was not assigned an executor, or when a chunk isn't correctly processed during flood-filling.

If a map is found to have an instability, one of two paths will be taken. If the B:"Resolve conflicts?" option is false, then the game will crash to avoid any form of desync or data loss.

If the option is true, Spool will attempt to fully rebuild the map (either player or chunk) from the ground up. The map is cleared, then a map-dependent function is run to repopulate the map.

  • If the map is the player executor map, Spool will loop through every player on the server and attempt to properly create, assign, and synchronize executors across all players in the server.
  • If the map is the chunk executor map, Spool will loop through all of the loaded chunks on the server, and will attempt to assign and synchronize the force-loaded chunks to an applicable player, following the same steps as already listed (during the tick), with the exclusion of the flood-fill algorithm, as Spool is already iterating over every chunk. If there are no players online while the chunk executor map is being rebuilt, the map will be cleared and all force-loaded chunks will just be executed on the main thread.

After this, if the maps are still found to be invalid, Spool will crash the game.

At this point, all objects have been executed, and Spool's distance manager has finished its duty for this tick. If B:"Allow processing during sleep?" is false, the thread manager will wait for all tasks to finish processing. If this option is true, then Spool will allow the tasks to continue processing throughout the rest of the end of the tick (including sleep time), all the way until the beginning of the next tick. At this point, the manager will then wait until all tasks are finished processing.

Experimental Threading

Experimental threading is the most aggressive, volatile, and performance-enhancing form of threading that Spool offers. This puts every update into a pool, allowing threads to grab updates and execute them. This can lead to >20x performance improvements, even with massive servers. The catch to this is that, threading every update is an incredibly dangerous process. Not only does it break the block update order (meaning blocks can update in nondeterministic orders), but it also causes insane concurrency issues. This option should only be considered if you have an insanely large server (or potentially singleplayer if needed), and all mods have explicit compatibility (as found in the compatible/tested mods page on this wiki).

Beginning of each tick

Experimental threading does not do anything at the beginning of the tick.

During the tick

During the tick, all updates for chunks, entities, and blocks will be run on the appropriate pool (asynchronously). Entities are run in the entityManager pool, while chunks and blocks are run in the blockManager pool. If B:"Allow processing during sleep?" is false, the thread manager will wait for all tasks to finish processing. If this option is true, then Spool will allow the tasks to continue processing throughout the rest of the end of the tick (including sleep time), all the way until the beginning of the next tick. At this point, the manager will then wait until all tasks are finished processing.

Thread Managers

ThreadManager

This is the standard thread manager for Spool. Nothing uses this as of now. It can have a set number of threads, and allows for submitting tasks to a pool for them to be executed sometime in the future.

ForkThreadManager

This thread manager allows tasks to spawn new tasks inside themselves. It is used in spool for the experimental threading pools, as it is much more performant than the ThreadManager. It can also have a set number of threads on initialization, and also allows for submitting tasks to a pool for them to be executed.

KeyedPoolThreadManager

This is a more complex type of thread manager, allowing certain values to be bound to a thread in the manager's pool. This manager is used for both the dimension threading system (key being the dimension being executed), and the distance-based threading system (key being Spool's hashcode for the player). This is generally more performant than a standard pool, as tasks can be assigned directly to a thread, instead of the pool adding to a task queue, meaning threads don't have to wait on other threads to receive a task. The thread values bound to the keys are also singular threads, allowing tasks submitted to different threads to be asynchronous, while tasks submitted to the same thread will execute in the order of their submission.

TimedOperationThreadManager

This manager isn't used anywhere in Spool as of now, however it exists to be used later on in Spool's development process. This manager allows for the execution of tasks a specific amount of time into the future. This allows a task to be executed after 1 second for example. It inherits attributes from the base ThreadManager.

Async Profiler

Spool completely replaces the standard Minecraft profiler with its own for asynchronous profiling and multiple thread display on the debug information. This async profiler mostly involves replacing lists with maps of thread IDs to lists, though some implementations were changed, especially the debug command.

Spool uses the config option B:"Use better task profiling?" to tell what to do in the case of a task being executed inside a thread. When this is disabled, all tasks will execute as normal, and only the server thread will show in the profiling output. If this is true, a root section will be injected into the top of executed tasks. This root section allows Spool's async profiler to differentiate the threads from the server thread, and all threads (and their tasks) will be shown in the profiler output. This does lead to performance losses, hence why it is a config option for those who wish to do profiling.

Concurrent World

Spool replaces several core classes found in Minecraft in order to allow for concurrent world access. Every aspect of the world is thread-safe, from block setting/getting to entity storage. The Chunk, ExtendedBlockStorage, NibbleArray, and various generators/world providers are replaced in order to accomplish this.

Concurrent Chunk System

Alongside concurrent world, Spool changes the internal ChunkProviderServer (main "manager" class for chunk management and loading on servers) to use a Future based system instead of the normal Chunk storage. This increases overhead slightly, however it also allows for chunks to be generated concurrently and reduces bugs caused by concurrent world.