Evolving code and adding new technologies - Part 3

In this part of the series we will see how our code, after having evolved from a simple event call to a concurrent mess, will first and foremost get a bit into shape and then being throttled to produce best results. TL;DR The light at the end of the tunnel is not oncoming traffic.

More parts are here:  Part 1 and Part 2Part 4 and Part 5

Before we start I urge you to go read through the documentation. These are the links that hlped me write this post and learn how to use the Dataflow Library

Let the data flow

GitHub branch

This may be "old" tech for some or all of you but it was new to me when I decided to pop my head out of the "company" man role and into the consultancy role in my career. So lets evolve and enrich that old system with, you guessed it, the TPL Dataflow library. Now mind you, in this blog post this is the first time I am using this library. I will explore, apply and benchmark it to the demo code. The change in the real system skipped this phase and was more radical. We will get there, however I feel that this is an important step to shape up and streamline an otherwise messy code.

Identifying the proper data flow block

After having read the documentation of the data flow library I decided to link a Batch source block to a buffer block and form a pipeline. This way I can free the producers from creating a list of 100 messages and then  pushing it over to the consumer, gather 100 items from all consumers each time and pipeline them to the buffer block where messages will be dealt with one by one. Moreover I will put a block between the batch source block and the buffer block to group messages by time with a margin. This will go into the buffer block and the data consumer on the other side will have a primary message correlation to do as it sees fit. The flow will look as such:

Our Dataflow pipeline


Please notice that the data flow library has degrees of parallelism built in. We will be changing those, as well as the amount of producers and the rate messages are produced, in order to observe the behavior of the library. See we didn't get rid of concurrency woes. We just took a library that keeps them under wraps and makes the code above more of a syntactic sugar with, albeit useful, bells and whistles. Expect surprises!

On to the code  

So on with the changes. Our DataMessage class will look like this:


Notice the overridden ToString method. Not only will it make the code to print out the message data cleaner down the road, when you hover in the debugger over an instance of a class with this method overridden, what you will see is the output you composed inside your override. Be careful though, make the output too big or too complex and you may cause major performance degradation while debugging. Especially if this a collection of hundreds of messages you are inspecting.

By the way in the matter of debugger views I strongly urge you to check out my colleague's Moaid Hathot post on Custom Debugger views for Visual Studio by using debugger attributes. In there you will also see the use of  OzCode which is also mentioned in my Tools of the Trade page. This was a huge eye opener for me. Thanks Moaid!

Before we get down to the specific implementation please notice there are 2 ways to use components of the Dataflow library:
  • As a pipeline, where you link the different process block with each other and let them synchronize and pass data around or
  • Write the "go between" code on your own by using DataflowBlock.Recieve or DataflowBlock.RecieveAsync and do the synchronizing or DataflowBlock.Post or DataflowBlock.SendAsync to the next block on your own.
There really isn't a better option here. It is a matter of implementation, code architecture and specific  needs. For example if different Dataflow blocks needed to exist in different components wrapped around with abstractions then most probably the later would be the case. For our example I chose a pipeline. I wanted 0 interaction with the complexity of concurrency... or didn't I?

The code to produce and have data flow in the pipeline looks like this:


I also added 3 configuration parameters in order to jungle with the throttling of the system in order to understand the behavior of the Dataflow library.


The first one defines the amount of producers, the second the amount of messages the  BatchBlock will hold before pushing them down the pipeline and the third the maximum degree of parallelism for the ActionBlock. The later may also be used on the TransformBlock. I urge you to run the code and play with these values. The findings will be most interesting. 

Some Benchmarks

Playing with the settings produced some interesting results. The data is grouped by time. The larger the number of producers the worst the situation becomes. The delay observed in the previous parts of the series gets worse. Memory allocated is not released on time. On 10 producers increasing the number of  max parallelism gives better results for a couple minutes and then enters a delay as well, all the while messages are numbered in millions. Some actual throttling can be sensed on the buffer of the batch block. Smaller number gives better results. Larger number the system goes downhill.

When having 1-2 producers, again the main throttling parameter is still the buffer size. Also notice the 100 milliseconds sleep on the producer code. Remove it and no matter what all the other values are things turn south fast.  

Conclusion

Although the TPL Dataflow library is a very nice library, it is not built for large volumes of data. Especially not in pipeline mode. Data will flow and will flow well and the complexity will be visibly reduced from your code. It's overall performance is good as long as the amount of data and its rate are reasonable and do not amount to thousands within 60 seconds. Having 10 producers is also not bad as long as they don't overload the pipeline. On this specific case however the need is for thousands of messages per minute, hence the Dataflow library is not the best solution. By the way, in case  you are wondering, it took me 1 day's work net time to implement, debug and benchmark the library. In case you are investing in a mock to see if it fits your needs 1 work day including conclusions is reasonable time for that.     

Comments

Popular posts from this blog

NUnit Console Runner and NLog extension logging for your tests

Tests code coverage in Visual Studio Code with C# and .Net Core

Applying an internal forums system in the company culture - thoughts