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

Если юнит-тестирование — неотъемлемая часть вашего процесса разработки, значит вы регулярно запускаете многочисленные тесты проверяющие функциональность приложения. А теперь представьте, что вы можете написать некие специальные «тесты на использование памяти». Например, тест, обнаруживающий утечку при помощи проверки памяти на наличие объектов определенного типа, или тест, который отслеживает трафик памяти и «падает», если трафик (аллоцированный объем) превысит заданный порог. Это в точности то, что позволяет делать 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, чтобы убедиться, что новые «фичи» не создают проблем с памятью.

Комментарии (13)


  1. Razbezhkin
    13.05.2015 08:55

    Спасибо. очень полезная, должно быть, штука.
    А как вообще через код C# можно определить, какие объекты находятся в памяти?


    1. Dywar
      13.05.2015 23:17

      Если почитать два последних поста от Jet, то в них можно найти видео.
      В одном из которых автор доклада бегает по куче и считает объекты используя C# unsafe.

      Можно использовать windbg с расширением SOS.dll, SOSEX.dll.
      Запускаем программу, цепляемся в нужном месте, и смотрим SOH (0,1,2) и LOH.


      1. Razbezhkin
        14.05.2015 11:33

        Спасибо. Не могли бы привести пример, если не затруднит.


        1. Dywar
          14.05.2015 21:23

          1) Человек из JetBrains.
          www.youtube.com/watch?v=KbuJdkPLAjw

          2) Windbg.

          !dumpheap [-stat] [-mt <>] [-type <>] [-strings] [-min] [-max]
          (What classes take space in managed heap)
          
          !dumpgen <genNum> [-free] [-stat] [-type <>] [-nostrings]
          (Dumps the contents of the specified generation (sosex))
          
          !dumpdomain 	(сколько доменов.)
          !EEVersion	(отображает версию GC.)
          !threads		(отображает потоки. -special для специальных потоков)
          !dumpheap 	(garbage collector heap.)
          !dumpheap -type SpaceName.Type (покажет объект в куче.)
          !dumpheap -type Free -stat	(поиск пустых объектов, показывает кол-во всего и пустые из них.)
          !gcroot XXX		(отображает тех кто удерживает объект от сборки.)
          !objsize XXX		(отображает размер объекта (и все объекты на которые он ссылается, включено в него.)
          


          Если попытаться загрузить sos при старте приложения ничего не выйдет, надо аттачиться когда процесс уже прошел первичную инициализацию.


  1. xtraroman
    13.05.2015 11:12
    +5

    Интересная статья, но dotMemory Unit не рекомендуют совмещать с unit тестами, потому что обычно unit тесты гоняют под конфигурацией Debug а dotMemory Unit больше любит Release конфигурацию. А еще статья расширила мой лексикон такими словами как: профиляция, профилятор :).


    1. techdoc Автор
      13.05.2015 11:41

      эмм, я так понимаю это не проблема dotMemory Unit. Просто в Debug билдах, те объекты на которые ссылается отладчик не будут собраны во время GC (даже если они уже не нужны самой программе). Собсно поэтому с точки зрения потребления памяти смотреть надо всегда на Release билды.


      1. areht
        16.05.2015 21:34

        debug билды могут запускаться и не под отладчиком


  1. alexeykuzmin0
    13.05.2015 13:24

    А известно про что-нибудь подобное для C++?


  1. yurash
    13.05.2015 14:04
    +3

    Извините, но в статье-переводе я на «профиляцию» не согласен. Давайте все вместе:
    память профилировали, профилировали да невыпрофилировали, надо память перепрофилировать, перевыпрофилировать


    1. naum
      13.05.2015 15:05

      и да, делали это профайлером


      1. Amper
        13.05.2015 19:22

        Профилятором :)


    1. andreycha
      13.05.2015 15:45

      Это у них в JetBrains такой сленг. Посмотрите их доклады с .NEXT, там то же самое :).


  1. Serg2DFX
    13.05.2015 18:36
    -1

    Как это интегрируется с CI?
    Если я правильно понимаю, то на агенты построений придётся поставить ReSharper или dotCover.

    Поэтому для запуска dotMemory Unit тестов, на вашей машине должен быть устанолен ReSharper 9.1 или dotCover 3.1.
    Перестанут ли работать тесты после завершения триала?