DryIoc: Memory leak with ResolveManyBehavior.AzLazyEnumerable?

In a certain situation we have observed a memory leak in ResolveMany where KeyedFactoryDelegateCache gets filled up with “identical” values.

We have something like the following where we iterate over requestType and it’s parent types and have previously registered a singleton for IIncomingRequestInterceptor<RequestTypeBase>:

var interceptorType = typeof(IIncomingRequestInterceptor<>).MakeGenericType(requestType);

var interceptors = container.ResolveMany(interceptorType).ToList();

The above line of code will add an item to the KeyedFactoryDelegateCache for each call to ResolveMany.

var interceptors = container.ResolveMany(interceptorType, ResolveManyBehavior.AsFixedArray).ToList();`

This line of code will only add an item to the KeyedFactoryDelegateCache for each unique interceptorType.

If we assume that there will be only one item (which is not true in the real use case) and use Resolve we get an item in DefaultFactoryDelegateCache for each unique interceptorType (with something registered for it).

I don’t quite know how to make a useful test for this since those caches are deeper in the implementation and the actual return values from ResolveMany are just fine.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Comments: 26 (13 by maintainers)

Most upvoted comments

Hm, interesting, I had totally missed that one.

Below is a modified test that fails on 3.0.2 and works on the latest preview. But what are those additional objects in the DryIoc namespace that seems to be accumulating?

(Oh, 4 of them are the resulting objects from ResolveMany, but there are a few more)

using System.Linq;
using JetBrains.dotMemoryUnit;
using NUnit.Framework;

namespace DryIoc.IssuesTests
{
	[TestFixture]
	public class GHIssue32_Memory_leak_with_ResolveManyBehavior_AzLazyEnumerable
	{
		[DotMemoryUnit(CollectAllocations = true, FailIfRunWithoutSupport = false)]
		[Test]
		public void Test()
		{
			var c = new Container();
			c.Register(typeof(IIncomingRequestInterceptor<>), typeof(FooInterceptor<>));
			c.Register(typeof(IIncomingRequestInterceptor<>), typeof(BarInterceptor<>));

			var memoryCheckPoint = dotMemory.Check();

			var interceptorType = typeof(IIncomingRequestInterceptor<>).MakeGenericType(typeof(int));

			var interceptors1 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsLazyEnumerable).ToList();
			var interceptors2 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsLazyEnumerable).ToList();

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Namespace.Like("DryIoc")).ObjectsCount, Is.EqualTo(28).Or.EqualTo(30)));

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Type.Like("DryIoc.FactoryDelegate")).ObjectsCount, Is.EqualTo(2)));


			var interceptors3 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsLazyEnumerable).ToList();
			var interceptors4 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsLazyEnumerable).ToList();

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Namespace.Like("DryIoc")).ObjectsCount, Is.EqualTo(41)));

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Type.Like("DryIoc.FactoryDelegate")).ObjectsCount, Is.EqualTo(2)));

			var interceptors5 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsLazyEnumerable).ToList();
			var interceptors6 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsLazyEnumerable).ToList();

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Namespace.Like("DryIoc")).ObjectsCount, Is.EqualTo(53)));

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Type.Like("DryIoc.FactoryDelegate")).ObjectsCount, Is.EqualTo(2)));

			var interceptors7 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsLazyEnumerable).ToList();
			var interceptors8 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsLazyEnumerable).ToList();

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Namespace.Like("DryIoc")).ObjectsCount, Is.EqualTo(65)));

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Type.Like("DryIoc.FactoryDelegate")).ObjectsCount, Is.EqualTo(2)));
		}

		public interface IIncomingRequestInterceptor<T> { }
		private class FooInterceptor<T> : IIncomingRequestInterceptor<T> { }
		private class BarInterceptor<T> : IIncomingRequestInterceptor<T> { }
	}
}

I did some more thinking about what dotMemoryUnit does and even though the documentation does not state so explicitly I think it is safe to assume that whenever Check is called, all objects that can be collected have been. If not it would not be possible to write a useful test. I tested by adding some GC.Collects and could see no change in behaviour whatsoever.

To cleanup the the results bit I changed the filter to where.Assembly.Is(typeof(Container).Assembly)) so that any actual resolve results (and other stuff) get filtered away but DryIoc, ImTools and FastExpressionCompiler all are included. Wont work if using the source version though.

The next step was to change the resolve calls to this

var interceptors1 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsLazyEnumerable).ToArray();

that I believe to be the same as

var interceptors1 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsFixedArray).ToArray();

if you take into account the end result and the expected objects to be referenced at this point. The contents of interceptors1 after each call should be identical and no other objects should still be referenced, though some “static” objects might turn up, but those cannot increase with multiple calls. Do you see any reason why that should not be true? If so that is obviously the key point where my argument fails.

It also means that a different test is needed for:

var lazyEnumerable = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsLazyEnumerable);

var interceptors1 = lazyEnumerable.ToArray();

var interceptors2 = lazyEnumerable.ToArray();

(and possibly a different one for .ToList())

to validate that the second (and third etc.) call to lazyEnumerable.ToArray() yields no additional objects beyond what ends up in interceptors2, they would indeed be temporary if they were held by lazyEnumerable, but only if lazyEnumerable itself is not longlived in any way.

If we start with the FixedArray version, this test runs fine in 3.0.2 and 3.1.0-05 and nothing “funny” turns up if manually inspected with memprofiler or VS2017. No additional objects inside DryIoc are referenced after that first ResolveMany.

using System.Linq;
using JetBrains.dotMemoryUnit;
using NUnit.Framework;

namespace DryIoc.IssuesTests
{
	[TestFixture]
	public class GHIssue32_Memory_leak_with_ResolveManyBehavior_AsFixedArray
	{
		[DotMemoryUnit(CollectAllocations = true, FailIfRunWithoutSupport = false)]
		[Test]
		public void Test()
		{
			var c = new Container();
			c.Register(typeof(IIncomingRequestInterceptor<>), typeof(FooInterceptor<>));
			c.Register(typeof(IIncomingRequestInterceptor<>), typeof(BarInterceptor<>));

			var memoryCheckPoint = dotMemory.Check();

			var interceptorType = typeof(IIncomingRequestInterceptor<>).MakeGenericType(typeof(int));

			var interceptors1 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsFixedArray).ToArray();

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Assembly.Is(typeof(Container).Assembly)).ObjectsCount, Is.EqualTo(39).Or.EqualTo(36)));

			var interceptors2 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsFixedArray).ToArray();

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Assembly.Is(typeof(Container).Assembly)).ObjectsCount, Is.EqualTo(39).Or.EqualTo(36)));

			var interceptors3 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsFixedArray).ToArray();

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Assembly.Is(typeof(Container).Assembly)).ObjectsCount, Is.EqualTo(39).Or.EqualTo(36)));

			var interceptors4 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsFixedArray).ToArray();
			
			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Assembly.Is(typeof(Container).Assembly)).ObjectsCount, Is.EqualTo(39).Or.EqualTo(36)));
		}

		public interface IIncomingRequestInterceptor<T> { }
		private class FooInterceptor<T> : IIncomingRequestInterceptor<T> { }
		private class BarInterceptor<T> : IIncomingRequestInterceptor<T> { }
	}
}

If we move on to AsLazyEnumerable we need this code to make a working test, the righthand values are for 3.0.2 and the lefthand for 3.1.0-05. So in 3.0.2, 12 additional objects are allocated for each ResolveMany call, and in 3.1.0-05, 4 additional objects are allocated for each call. Progress, but not quite there yet, or?.

using System.Linq;
using JetBrains.dotMemoryUnit;
using NUnit.Framework;

namespace DryIoc.IssuesTests
{
	[TestFixture]
	public class GHIssue32_Memory_leak_with_ResolveManyBehavior_AsLazyEnumerable
	{
		[DotMemoryUnit(CollectAllocations = true, FailIfRunWithoutSupport = false)]
		[Test]
		public void Test()
		{
			var c = new Container();
			c.Register(typeof(IIncomingRequestInterceptor<>), typeof(FooInterceptor<>));
			c.Register(typeof(IIncomingRequestInterceptor<>), typeof(BarInterceptor<>));

			var memoryCheckPoint = dotMemory.Check();

			var interceptorType = typeof(IIncomingRequestInterceptor<>).MakeGenericType(typeof(int));

			var interceptors1 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsLazyEnumerable).ToArray();

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Assembly.Is(typeof(Container).Assembly)).ObjectsCount, Is.EqualTo(53).Or.EqualTo(50)));

			var interceptors2 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsLazyEnumerable).ToArray();

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Assembly.Is(typeof(Container).Assembly)).ObjectsCount, Is.EqualTo(57).Or.EqualTo(62)));

			var interceptors3 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsLazyEnumerable).ToArray();

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Assembly.Is(typeof(Container).Assembly)).ObjectsCount, Is.EqualTo(61).Or.EqualTo(74)));

			var interceptors4 = c.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsLazyEnumerable).ToArray();

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Assembly.Is(typeof(Container).Assembly)).ObjectsCount, Is.EqualTo(65).Or.EqualTo(86)));

		}

		public interface IIncomingRequestInterceptor<T> { }
		private class FooInterceptor<T> : IIncomingRequestInterceptor<T> { }
		private class BarInterceptor<T> : IIncomingRequestInterceptor<T> { }
	}
}

So what are those objects? Well, memprofiler starts warning about disposed objects that are not gc:ed, likely because someone holds a reference to them. It should not be the interceptorsX arrays, they are all object[] but coincidentally apparently are very hard to get GC:ed before they go out of scope. The others seems to be related to the state machine for yield in ResolveMany which is odd, the state machine should be terminated given that we do ToArray, I think? Could it be that the state machine gets referenced by the calling method (Test()) in some behind the scenes magic that also needs to go out of scope to be GC:ed?

And we now get this which runs fine in 3.1.0-05 and fails in 3.0.2 with 11 additional objects per call and I learnt something about c# internals I did not know…

using System;
using System.Linq;
using JetBrains.dotMemoryUnit;
using NUnit.Framework;

namespace DryIoc.IssuesTests
{
	[TestFixture]
	public class GHIssue32_Memory_leak_with_ResolveManyBehavior_AsLazyEnumerable_2
	{
		public object[] ResolveHelper(Type interceptorType, IResolver r)
		{
			var interceptors = r.ResolveMany(interceptorType, behavior: ResolveManyBehavior.AsLazyEnumerable).ToArray();

			return interceptors;
		}


		[DotMemoryUnit(CollectAllocations = true, FailIfRunWithoutSupport = false)]
		[Test]
		public void Test()
		{
			var c = new Container();
			c.Register(typeof(IIncomingRequestInterceptor<>), typeof(FooInterceptor<>));
			c.Register(typeof(IIncomingRequestInterceptor<>), typeof(BarInterceptor<>));

			var memoryCheckPoint = dotMemory.Check();

			var interceptorType = typeof(IIncomingRequestInterceptor<>).MakeGenericType(typeof(int));

			var interceptors1 = ResolveHelper(interceptorType, c);

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Assembly.Is(typeof(Container).Assembly)).ObjectsCount, Is.EqualTo(52).Or.EqualTo(49)));

			var interceptors2 = ResolveHelper(interceptorType, c);

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Assembly.Is(typeof(Container).Assembly)).ObjectsCount, Is.EqualTo(52).Or.EqualTo(49)));

			var interceptors3 = ResolveHelper(interceptorType, c);

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Assembly.Is(typeof(Container).Assembly)).ObjectsCount, Is.EqualTo(52).Or.EqualTo(49)));

			var interceptors4 = ResolveHelper(interceptorType, c);

			dotMemory.Check(memory =>
				Assert.That(memory.GetDifference(memoryCheckPoint)
					.GetNewObjects(where => where.Assembly.Is(typeof(Container).Assembly)).ObjectsCount, Is.EqualTo(52).Or.EqualTo(49)));
		}

		public interface IIncomingRequestInterceptor<T> { }
		private class FooInterceptor<T> : IIncomingRequestInterceptor<T> { }
		private class BarInterceptor<T> : IIncomingRequestInterceptor<T> { }
	}
}

Oh, sorry. Using v3.0.2.