Введение
Мы пытаемся поймать потенциальные утечки памяти, используя BenchmarksDotNet
.
Для простоты примера вот незамысловатый TestClass
:
public class TestClass
{
private readonly string _eventName;
public TestClass(string eventName)
{
_eventName = eventName;
}
public void TestMethod() =>
Console.Write($@"{_eventName} ");
}
Мы реализуем бенчмаркинг с помощью тестов NUnit в 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();
}
}
Выходные данные теста содержат следующую сводку:
Method | Mean | Error | Allocated |
--------------- |-----:|------:|----------:|
TestBenchmark1 | NA | NA | 0 B |
Выходные данные теста также содержат все выходные данные Console.Write
, что доказывает, что 0 B
здесь означает отсутствие утечки памяти, а не запуск кода из-за оптимизации компилятора.
Проблема
Путаница начинается, когда мы пытаемся разрешить TestClass
с TinyIoC
контейнером:
[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();
}
}
Сводка указывает на утечку 1,07 КБ.
Method | Mean | Error | Allocated |
--------------- |-----:|------:|----------:|
TestBenchmark1 | NA | NA | 1.07 KB |
Allocated
значение увеличивается пропорционально количеству ResolveTestClass
вызовов от TestBenchmark1
, сводка по
[Benchmark]
public void TestBenchmark1()
{
ResolveTestClass("Test");
ResolveTestClass("Test");
}
is
Method | Mean | Error | Allocated |
--------------- |-----:|------:|----------:|
TestBenchmark1 | NA | NA | 2.14 KB |
Это указывает на то, что либо TinyIoC
сохраняет ссылку на каждый разрешенный объект (что, судя по исходному коду, не соответствует действительности), либо BenchmarksDotNet
измерения включают некоторые дополнительные выделения памяти вне метода, отмеченного атрибутом [Benchmark]
.
Конфиг, используемый в обоих случаях:
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);
}
}
Кстати, замена TinyIoC
на Autofac
фреймворк внедрения зависимостей не сильно изменила ситуацию.
Вопросы
Означает ли это, что все DI-фреймворки должны реализовывать какой-то кеш для разрешенных объектов? Означает ли это, что BenchmarksDotNet
используется неправильно в данном примере? Стоит ли в первую очередь искать утечки памяти с помощью комбинации NUnit
и BenchmarksDotNet
?