Introduction
We are trying to catch potential memory leaks using BenchmarksDotNet
.
For the simplicity of example, here is an unsophisticated TestClass
:
public class TestClass
{
private readonly string _eventName;
public TestClass(string eventName)
{
_eventName = eventName;
}
public void TestMethod() =>
Console.Write($@"{_eventName} ");
}
We are implementing benchmarking though NUnit tests in netcoreapp2.0
:
[TestFixture]
[MemoryDiagnoser]
public class TestBenchmarks
{
[Test]
public void RunTestBenchmarks() =>
BenchmarkRunner.Run<TestBenchmarks>(new BenchmarksConfig());
[Benchmark]
public void TestBenchmark1() =>
CreateTestClass("Test");
private void CreateTestClass(string eventName)
{
var testClass = new TestClass(eventName);
testClass.TestMethod();
}
}
The test output contains following summary:
Method | Mean | Error | Allocated |
--------------- |-----:|------:|----------:|
TestBenchmark1 | NA | NA | 0 B |
The test output also contains all the Console.Write
output which proves that 0 B
here means no memory was leaked rather than no code was run because of compiler optimization.
Problem
The confusion begins when we attempt to resolve TestClass
with TinyIoC
container:
[TestFixture]
[MemoryDiagnoser]
public class TestBenchmarks
{
private TinyIoCContainer _container;
[GlobalSetup]
public void SetUp() =>
_container = TinyIoCContainer.Current;
[Test]
public void RunTestBenchmarks() =>
BenchmarkRunner.Run<TestBenchmarks>(new BenchmarksConfig());
[Benchmark]
public void TestBenchmark1() =>
ResolveTestClass("Test");
private void ResolveTestClass(string eventName)
{
var testClass = _container.Resolve<TestClass>(
NamedParameterOverloads.FromIDictionary(
new Dictionary<string, object> {["eventName"] = eventName}));
testClass.TestMethod();
}
}
The summary indicates 1.07 KB was leaked.
Method | Mean | Error | Allocated |
--------------- |-----:|------:|----------:|
TestBenchmark1 | NA | NA | 1.07 KB |
Allocated
value increases proportionally to the number of ResolveTestClass
calls from TestBenchmark1
, the summary for
[Benchmark]
public void TestBenchmark1()
{
ResolveTestClass("Test");
ResolveTestClass("Test");
}
is
Method | Mean | Error | Allocated |
--------------- |-----:|------:|----------:|
TestBenchmark1 | NA | NA | 2.14 KB |
This indicates that either TinyIoC
is keeping the reference to each resolved object (which does not seem to be true according to source code) or BenchmarksDotNet
measurements include some additional memory allocations outside of method marked with [Benchmark]
attribute.
The config used in both cases:
public class BenchmarksConfig : ManualConfig
{
public BenchmarksConfig()
{
Add(JitOptimizationsValidator.DontFailOnError);
Add(DefaultConfig.Instance.GetLoggers().ToArray());
Add(DefaultConfig.Instance.GetColumnProviders().ToArray());
Add(Job.Default
.WithLaunchCount(1)
.WithTargetCount(1)
.WithWarmupCount(1)
.WithInvocationCount(16));
Add(MemoryDiagnoser.Default);
}
}
By the way, replacing TinyIoC
with Autofac
dependency injection framework didn't change the situation much.
Questions
Does it mean all DI framework have to implement some sort of cache for resolved objects? Does it mean BenchmarksDotNet
is used in wrong way in a given example? Is it a good idea to hunt for memory leaks with the combination of NUnit
and BenchmarksDotNet
in the first place?