At first glace, System.IO.Pipelines appears to be an interesting solution for reducing the complexity of server sockets and efficient memory management. Without going into any details, the blog post suggests that Pipelines could provide an application with higher performance and a better / less complex server architecture. In this topic, I explore that idea using a Conquer Online server as a demo.
What isn't mentioned in the blog post is the general architectures supported by Pipelines. The only workflow promoted by Pipelines via the blog post is an asynchronous one, where memory is written to asynchronously and read from asynchronously using two separate tasks. This scales nicely for a single data source, such as a single TCP socket accepting RPC; however, multiple tasks processing multiple data sources from multiple clients does not scale. A queue needs to be inserted for managing requests appropriately; therefore, two tasks per client connection just for receiving data in sequence doesn't seem appropriate. Rather, two tasks working asynchronously using a pipeline as a channel is overkill. That's the job of worker roles that dequeue work.
A second workflow is promoted through the API for synchronous reads and writes. An asynchronous socket system could synchronously manage memory for a single asynchronous callback operation. This seems like the best approach for managing data request from multiple data sources. That would be one task per client managing receives.
For this test, Pipelines was implemented for synchronous memory management, which would be the most likely use case for a high performing game server that manages data using a worker role and queue. When analyzing performance, I wanted to see if it were any better than just keeping a fixed length buffer, such as in the case for SocketAsyncEventArgs. Since we're using Pipelines for synchronous memory management, the framework becomes simply weighed down by thread safety. Therefore, the obvious answer is that it shouldn't perform better than a fixed length array, and it doesn't. Pipelines takes ~41000 ticks while a fixed length buffer takes ~1800 ticks (tick count includes ReceiveAsync, which accepts and uses the buffer from both tests).
So, besides performance, what else might Pipelines offer programs that wish to use the framework for synchronous memory management? The first answer that comes to mind is inherent in the question: memory management. Pipeline memory blocks grow as needed using an initial size hint. Received data is copied from a 2048 byte buffer to the dynamic memory buffer, and the application only processes bytes that it received. A maximum buffer size can also be specified by overriding the Pipe's MemoryPool option. All of this can be accomplished using fixed length Array Segments, and splitting on the received byte count. If you don't expect to be processing lots of packets per client, such as the case for Conquer Online's packets which are always smaller than 1024 bytes, then there's not a lot that Pipelines has to offer.
Although I feel that the Pipelines framework isn't helpful for the specific design of a game server, it does greatly simplify general worker role implementations for WCF. Rather than dealing with the complexity of Azure Queues, Microsoft Message Queuing, or implementing your own queue system using blob storage or memory for producer/consumer tasks, Pipelines offers a very clean and elegant solution for managing work from a single data source. Pipelines may also be helpful for multiple sources and a single, generic consumer, such as in the case of reading and processing map files for Conquer Online.
Stepping back, it seems like Microsoft is learning from other languages such as Go, which offers the same functionality as a language primitive (channels) while also attempting to wrap and simplify its functionality. Hopefully we see more efforts like this in the future as these languages continue to compete against C#.
With all of that said, what are your thoughts on my findings? Do you feel that my research was a good field test for Pipelines that went wrong, or did I take a wrong turn somewhere in the implementation? How have you worked with Pipelines so far, and has it helped simplify complex server code? I'd be curious to know. Thanks for reading.