I have an inherited .NET 4.0 application that runs as a Windows service. I'm not a .NET expert by any stretch, but after writing code for 30+ years I know how to find my way around.
When the service first starts it clocks in at around 70MB private working set. The longer the service runs, the more memory it takes. The increase isn't so dramatic that you notice while just sitting and watching, but we have seen instances where after the application has run for a long time (100+ days) it's up to multiple GB (5GB is the current record). I attached ANTS Memory Profiler to a running instance and found that usage of ExpandoObject seems to account for multiple megabytes of strings which do not get cleaned up by GC. There are likely other leaks, but this was the most noticeable so it got attacked first.
I've learned from other SO posts that "normal" usage of ExpandoObject generates an internal RuntimeBinderException when reading (but not writing) the dynamically assigned attributes.
dynamic foo = new ExpandoObject();
var s;
foo.NewProp = "bar"; // no exception
s = foo.NewProp; // RuntimeBinderException, but handled by .NET, s now == "bar"
You can see the exception happen in VisualStudio, but ultimately it's handled in the .NET internals and all you get back is the value you want.
Except... The string in the exception's Message property appears to stay on the heap and never gets Garbage Collected, even long after the ExpandoObject that generated it goes out of scope.
Simple example:
using System;
using System.Dynamic;
namespace ConsoleApplication2
{
class Program
{
public static string foocall()
{
string str = "", str2 = "", str3 = "";
object bar = new ExpandoObject();
dynamic foo = bar;
foo.SomePropName = "a test value";
// each of the following references to SomePropName causes a RuntimeBinderException - caught and handled by .NET
// Attach an ANTS Memory profiler here and look at string instances
Console.Write("step 1?");
var s2 = Console.ReadLine();
str = foo.SomePropName;
// Take another snapshot here and you'll see an instance of the string:
// 'System.Dynamic.ExpandoObject' does not contain a definition for 'SomePropName'
Console.Write("step 2?");
s2 = Console.ReadLine();
str2 = foo.SomePropName;
// Take another snapshot here and you'll see 2nd instance of the identical string
Console.Write("step 3?");
s2 = Console.ReadLine();
str3 = foo.SomePropName;
return str;
}
static void Main(string[] args)
{
var s = foocall();
Console.Write("Post call, pre-GC prompt?");
var s2 = Console.ReadLine();
// At this point, ANTS Memory Profiler shows 3 identical strings in memory
// generated by the RuntimeBinderExceptions in foocall. Even though the variable
// that caused them is no longer in scope the strings are still present.
// Force a GC, just for S&G
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Console.Write("Post GC prompt?");
s2 = Console.ReadLine();
// Look again in ANTS. Strings still there.
Console.WriteLine("foocall=" + s);
}
}
}
"bug" is in the eye of the beholders, I suppose (my eyes say bug). Am I missing something? Is this normal and expected by the .NET masters in the group? Is there some way to tell it to clear the things out? Is the best way to just not use dynamic/ExpandoObject in the first place?