I encountered exactly the same issue today by profiling memory leaks in my app RepoZ. That tool is supposed to run in the background, checking Git repositories and update Windows Explorer window titles periodically. The latter task has to make some COM calls to "Shell.Application" to find the Explorer windows and determine the path they are currently pointing to.
By using the dynamic
keyword like this ...
dynamic shell = Activator.CreateInstance(...);
foreach (object window in shell.Windows())
{
var hwnd = window.Hwnd;
...
}
... I ended up to a memory dump like that after a few hours:
Combridge
To solve that, I wrote a little helper class called "Combridge" caring to release COM objects and providing quite easy access to methods and properties of the underlying COM object. It is very easy and straightforward, nothing special here. It makes use of Reflection to COM objects, that's why there's some loss in performance (see below).
With it, the code sample from above looks like this:
using (var shell = new Combridge(Activator.CreateInstance(...)))
{
var windows = shell.InvokeMethod<IEnumerable>("Windows");
foreach (var window in windows)
{
var hwnd = window.GetPropertyValue<long>("Hwnd");
...
}
}
You can see the file ExplorerWindowActor on how it is used in RepoZ.
It is not exactly as beautiful as with dynamic
and performance got worse in this first attempt as well. A quick bench showed the following:
Performance
I tested 1000 iterations, in each iteration 10 open Explorer windows were processed. For each window, 4 methods or properties are invoked on that COM object. So we're talking about 40.000 COM calls.
The duration went up from ~2500ms (dynamic
) to ~6000ms (Combridge
). That's from 0.062ms to 0.150ms for each call.
So this takes about 2.4x the time to finish now.
This is significant, I know. But it is okay for my requirements and the memory leak is gone.
That's it - I wanted to share that story with you, hopefully you can use that class (or an improved version of it) to get out of the dynamic hell as well.
~Update~
After 10 hours, RepoZ still runs with a very constant memory footprint.
So with 10 Explorer windows open, 4 COM calls per window and that whole loop two times a second, RepoZ created about 72.000 COM instances and made about 2.880.000 COM calls in total without any increase in memory consumption.
I guess we can say that the issue really comes with dynamic
.