Pull to refresh

Можно ли совместить юнит тесты и профилирование памяти?

Reading time5 min
Views8K
Original author: A.Totin
Профиляторы памяти с трудом можно назвать «утилитами для ежедневного использования». Чаще всего разработчики задумываются о профилировании своего продукта перед самым релизом. Подобный подход вполне может работать, но лишь до тех пор, пока какая-нибудь проблема с памятью, обнаруженная в последний момент (например, утечка памяти или большой трафик памяти) не разрушит все ваши планы. Одним из решений могло бы быть профилирование на регулярной основе, но вряд ли кто-то захочет тратить на это столь драгоценное время. Тем не менее, решение кажется есть.

Если юнит-тестирование — неотъемлемая часть вашего процесса разработки, значит вы регулярно запускаете многочисленные тесты проверяющие функциональность приложения. А теперь представьте, что вы можете написать некие специальные «тесты на использование памяти». Например, тест, обнаруживающий утечку при помощи проверки памяти на наличие объектов определенного типа, или тест, который отслеживает трафик памяти и «падает», если трафик (аллоцированный объем) превысит заданный порог. Это в точности то, что позволяет делать dotMemory Unit фреймворк. dotMemory Unit распространяется в виде NuGet пакета и позволяет выполнять следующие сценарии:
  • Проверка памяти на наличие объектов определенного типа.
  • Проверка трафика памяти.
  • Сравнение снимков (далее 'снэпшотов') памяти.
  • Сохранение снэпшотов на диск с целью последующего анализа в dotMemory (профиляторе памяти от JetBrains).

Иными словами, dotMemory Unit расширяет возможности вашего юнит-тестинг фреймворка функциональностью профилятора памяти.

Как это работает?


  • dotMemory Unit распространяется как NuGet пакет устанавливаемый в ваш тест проект:
    PM> Install-Package JetBrains.DotMemoryUnit

  • dotMemory Unit требуется юнит-тест 'раннер', входящий в состав ReSharper. Поэтому для запуска dotMemory Unit тестов, на вашей машине должен быть установлен ReSharper 9.1 или dotCover 3.1.
  • После установки dotMemory Unit пакета, в меню ReSharper появится дополнительный пункт Run Unit Tests under dotMemory Unit. В этом режиме, тест раннер будет выполнять вызовы dotMemory Unit наряду с остальным кодом. Если вы запустите этот тест как обычно (без поддержки dotMemory Unit), все вызовы dotMemory Unit фреймворка будут проигнорированы.

  • dotMemory Unit совместим со всеми юнит-тестинг фреймворками поддерживаемыми ReSharper, в том числе MSTest и NUnit.
  • Отдельный 'лаунчер' для интеграции с CI системами по типу JetBrains TeamCity запланирован в одном из последующих релизов.
  • dotMemory Unit абсолютно бесплатен.

Пример 1: Проверка памяти на наличие определенных объектов


Давайте начнем с чего-нибудь простого. Один из наиболее полезных сценариев — это определение утечки путем проверки памяти на наличие объектов определенного типа.
[Test]
public void TestMethod1()
{
    ... // делаем что-нибудь

    // предполагаем, что в памяти осталось 0 объектов типа Foo
    dotMemory.Check(memory =>   //1, 2
    {
        Assert.That(memory.GetObjects(where => where.Type.Is<Foo>()).ObjectsCount, Is.EqualTo(0));    //3
    });
}

  1. Лямбда передается в метод Check статического класса dotMemory. Этот метод будет вызван только в том случае, если вы запустите этот тест при помощи меню Run Unit Tests under dotMemory Unit.
  2. Объект memory передаваемый в лямбду содержит данные обо всех объектах в памяти в текущей точке выполнения программы.
  3. Метод GetObjects возвращает набор объектов соответствующих условию передаваемому в очередной лямбде. Например, данная строка кода выбирает из памяти только объекты типа Foo. Выражение Assert предполагает, что в памяти должно быть 0 объектов типа Foo.

    Обратите внимание, что dotMemory Unit не обязывает вас использовать какой-то определенный синтаксис для Assert. Просто используйте синтаксис того фреймворка, для которого написан ваш тест. Например, строчка из примера выше (написана для NUnit) может быть переписана для MSTest:
            Assert.AreEqual(0, memory.GetObjects(where => where.Type.Is<Foo>()).ObjectsCount);   
    


dotMemory Unit позволяет выбирать объекты практически по любому условию, получать данные по количеству объектов и использовать их в Assert выражениях. Например, можно убедиться что Large object heap не содержит объектов:
        Assert.That(memory.GetObjects(where => where.Generation.Is(Generation.Loh)).ObjectsCount, Is.EqualTo(0));   


Пример 2: Проверка трафика памяти


Тест для проверки трафика памяти (аллоцированного объема данных) выглядит еще проще. Все что от вас требуется, это «пометить» тест при помощи аттрибута AssertTraffic. В следующем примере, мы предполагаем, что объем памяти аллоцированной тестом TestMethod1 не превышает 1000 байт.
[AssertTraffic(AllocatedMemoryAmount = 1000)]
[Test]
public void TestMethod1()
{
    ... // какой-то код
}


Пример 3: Сложные сценарии проверки трафика памяти


Если вам нужна более подробная информация о трафике (например, данные об аллокациях объектов определенного типа), вы можете использовать подход схожий с тем, что показан в примере 1. Лямбды передаваемые в метод dotMemory.Check позволяют фильтровать данные по всевозможным условиям.
var memoryCheckPoint1 = dotMemory.Check();  // 1

foo.Bar();

var memoryCheckPoint2 = dotMemory.Check(memory =>
{
    // 2
    Assert.That(memory.GetTrafficFrom(memoryCheckPoint1).Where(obj => obj.Interface.Is<IFoo>()).AllocatedMemory.SizeInBytes,
        Is.LessThan(1000));
});

bar.Foo();

dotMemory.Check(memory =>
{
    // 3
    Assert.That(memory.GetTrafficFrom(memoryCheckPoint2).Where(obj => obj.Type.Is<Bar>()).AllocatedMemory.ObjectsCount,
        Is.LessThan(10));
});

  1. Для того, чтобы отметить временной промежуток на котором вы хотите анализировать трафик, используйте «чекпойнты», создаваемые все тем же методом dotMemory.Check (как вы возможно догадались, этот метод просто снимает снэпшот памяти в момент вызова).
  2. Чекпоинт, определяющий начальную точку интервала, передается в метод GetTrafficFrom.
    Например, данная строка предполагает что общий размер объектов имплементирующих интерфейс IFoo и созданных на промежутке между memoryCheckPoint1 и memoryCheckPoint2 не превышает 1000 байт.
  3. Вы можете получать данные относительно любого из ранее созданных чекпоинтов. Так, данная строка запрашивает данные о трафике между текущим вызовом dotMemory.Check и memoryCheckPoint2.


Пример 4: Сравнение снэпшотов


Так же как и во «взрослом» профиляторе dotMemory, вы можете использовать чекпоинты не только для анализа трафика, но и для сравнения их друг с другом. В примере ниже, мы предполагаем, что ни один из объектов принадлежащих пространству имен MyApp не пережил сборку мусора в интервале между memoryCheckPoint1 и вторым вызовом dotMemory.Check.
    var memoryCheckPoint1 = dotMemory.Check();

    foo.Bar();

    dotMemory.Check(memory =>   
    {
        Assert.That(memory.GetDifference(memoryCheckPoint1)
            .GetSurvivedObjects().GetObjects(where => where.Namespace.Like("MyApp")).ObjectsCount, Is.EqualTo(0));   
    });


Заключение


dotMemory Unit очень гибок и позволяет вам полностью контролировать использование памяти вашим приложением. Используйте «тесты для памяти» также как вы используете обычные тесты:
  • После того как вы самостоятельно обнаружите утечку памяти, напишите тест, который покрывает эту часть кода.
  • Пишите интеграционные тесты с использованием dotMemory Unit, чтобы убедиться, что новые «фичи» не создают проблем с памятью.
Tags:
Hubs:
+11
Comments23

Articles