This week, I’ve been working on improving the performance of a Web application as we scale it out to more users.
I’ve always used Ants Profiler for this kind of work, but I decided to try out Jet Brain’s dotTrace, because it’s got better support for attaching to remote systems (and it’s cheaper).
Aside from the usual things like caching, simplification of database queries and using multi-threading whenever possible, I spotted a large volume of CPU cycles owned by the DI framework (Ninject) which turned out to be being spent on the creation of WCF clients.
At thebigword, we publish internal libraries up to an internal (private) NuGet server. As part of service development, we also publish the service interface as part of each package, so that other developers in the team can find, use and update them easily.
In the case of using an internal WCF service, an MVC Web application would normally use constructor injection in Ninject (https://github.com/ninject/Ninject/wiki/Injection-Patterns) to inject WCF clients into Manager classes, which are then, in turn, injected into MVC controllers. This allows testing of interactions between application layers with the use of mocks (we mostly use NSubstitute for this) and is a simpler build process than using the SvcUtil.exe to create a class which inherits from ClientBase to create a channel.
However, as per Wenlong Dong’s blog points out the ClientBase<T> has the additional benefit of caching the ChannelFactory object to improve performance.
To replicate this, I tried using a singleton ChannelFactory<T> and creating channels as I needed in Ninject by binding to a Method, but the RAM consumption went crazy, because the channels were never disposed by Ninject, (the singleton ChannelFactory<T> was still in scope and was retaining a reference to the Channel).
I needed a different approach.
Luís Gonçalves’s blog has a great series of posts on his approach to solving the same problem, but it was written with a different version of Ninject and only supported the WCF configuration being set in the Web.Config which I don’t use, so I made a few amendments and a test harness. I also found the code quite hard to understand, not being really that familiar with the internals of Ninject, so I chopped it down to a simple solution which uses a couple of extension methods to Ninject and a cache. Here’s the background methods:
And here’s how you’d configure Ninject:
Just to give you an idea of what to expect by this change, I’ve graphed of the performance of creating 5000 instances of a WCF client in the approach we were using before, and using the cached version. Black is cached, red isn’t. I was surprised by just how slow creating a client for the toy WCF contract in the test harness actually is.
You can see it took about 120ms to create 5000 proxies on my machine with caching, and over 1600ms without.
The LinqPad script test harness is at: http://share.linqpad.net/xnjcgc.linq