ASayre
RunUO Developer
If you look on the SVN (or attached), you'll find what's been keeping me busy the past few days: A new Save strategy to take the place of the never-quite-working ParallelSaveStrategy.
The Dynamic save strategy parallelizes the Serialization of the world, allowing us to take full advantage of today's multi processor, multi core systems.
I make heavy use of the .NET 4.0 TPL (Task Parallel Library). If you're not on .NET 4.0, you'll need to upgrade to use this. Don't forget to compile with /d:Framework_4_0
What it does for each serialized type (item/mobile/guild):
Divides up the list of that type into large chunks. Each of these chunks gets it's own thread. Each of these threads Serialize to their own MemoryStream. As each of these chunks complete (in parallel), we add each completed memory stream to a queue, to be processed by another Thread.
We also launch up a thread for each type which watches this queue and waits for incoming memoryStreams. This thread then takes the memory stream, copies it to disk, writes the relevent index entries, and continues waiting until the other threads are done adding.
Because of the TPL's dynamic partitioning of the chunks, we have a form of load balancing. I won't go into how it works, as MSDN can provide prettier pictures. Just know that it's a conscious decision to not divide it into numberOfProcessors threads, each with the same number of entities. I toyed with the idea of creating a custom Partitioner, but empirical evidence shows that the TPL's partitioning is already damn good.
Since we're using our IO resources as fast as we can process the entities, and we're using whatever CPU power available, we won't (in theory) be wasting any idle cycles or io left unused.
However, theory is only theory. In practicality, the task parallelism has a bit of overhead. Also, with writes coming in when they come in, total i/o speed drops way down.
This is designed for servers with a lot of spare cycles. It works best on those that would've been otherwise (using the DualSaveStrategy) CPU bound.
Because of the way the system is designed, it's easy enough to have the write to file threads continue even though everything else is finished. This is not enabled by default, because it's dangerous, if you close RunUO during the save, or save again while it's still flushing to disk, bad things happen. These can be avoided, but that's a future work item.
For now, ONLY if you don't want to wait for the IO threads to continue, comment out
Task.WaitAll(saveTasks);
CloseFiles();
And then Uncomment
//Task.Factory.ContinueWhenAll(saveTasks, _ => CloseFiles());
in DynamicSaveStrategy.cs ~ line 75
Note that the above is dangerous as stated for the reasons above. If you're IO bound, the above will help with the save times. If not, there's no reason to do it.
For everyone else who just wants to try out the rest of the Save Strategy (This is mostly untested, except for the most basic of tests so don't use on a production shard!)
Change the if (processorCount > 16) in SaveStrategy.cs to a more reasonable number, like '4'. The reasons for the 16 there is just so it doesn't get used by anyone by default while this code is still in it's infancy.
Please let me know of any comments/concerns/bugs/optimizations!
The Dynamic save strategy parallelizes the Serialization of the world, allowing us to take full advantage of today's multi processor, multi core systems.
I make heavy use of the .NET 4.0 TPL (Task Parallel Library). If you're not on .NET 4.0, you'll need to upgrade to use this. Don't forget to compile with /d:Framework_4_0
What it does for each serialized type (item/mobile/guild):
Divides up the list of that type into large chunks. Each of these chunks gets it's own thread. Each of these threads Serialize to their own MemoryStream. As each of these chunks complete (in parallel), we add each completed memory stream to a queue, to be processed by another Thread.
We also launch up a thread for each type which watches this queue and waits for incoming memoryStreams. This thread then takes the memory stream, copies it to disk, writes the relevent index entries, and continues waiting until the other threads are done adding.
Because of the TPL's dynamic partitioning of the chunks, we have a form of load balancing. I won't go into how it works, as MSDN can provide prettier pictures. Just know that it's a conscious decision to not divide it into numberOfProcessors threads, each with the same number of entities. I toyed with the idea of creating a custom Partitioner, but empirical evidence shows that the TPL's partitioning is already damn good.
Since we're using our IO resources as fast as we can process the entities, and we're using whatever CPU power available, we won't (in theory) be wasting any idle cycles or io left unused.
However, theory is only theory. In practicality, the task parallelism has a bit of overhead. Also, with writes coming in when they come in, total i/o speed drops way down.
This is designed for servers with a lot of spare cycles. It works best on those that would've been otherwise (using the DualSaveStrategy) CPU bound.
Because of the way the system is designed, it's easy enough to have the write to file threads continue even though everything else is finished. This is not enabled by default, because it's dangerous, if you close RunUO during the save, or save again while it's still flushing to disk, bad things happen. These can be avoided, but that's a future work item.
For now, ONLY if you don't want to wait for the IO threads to continue, comment out
Task.WaitAll(saveTasks);
CloseFiles();
And then Uncomment
//Task.Factory.ContinueWhenAll(saveTasks, _ => CloseFiles());
in DynamicSaveStrategy.cs ~ line 75
Note that the above is dangerous as stated for the reasons above. If you're IO bound, the above will help with the save times. If not, there's no reason to do it.
For everyone else who just wants to try out the rest of the Save Strategy (This is mostly untested, except for the most basic of tests so don't use on a production shard!)
Change the if (processorCount > 16) in SaveStrategy.cs to a more reasonable number, like '4'. The reasons for the 16 there is just so it doesn't get used by anyone by default while this code is still in it's infancy.
Please let me know of any comments/concerns/bugs/optimizations!