.NET – управляемая среда выполнения. Это означает, что в ней представлены высокоуровневые функции, которые управляют вашей программой за вас (из Introduction to the Common Language Runtime (CLR), 2007 г.):
Среда выполнения предусматривает множество функций, поэтому их удобно разделить по следующим категориям:
- Основные функции, которые влияют на устройство других. К ним относятся:
- сборка мусора;
- обеспечение безопасности доступа к памяти и безопасности системы типов;
- высокоуровневая поддержка языков программирования.
- Дополнительные функции– работают на базе основных. Многие полезные программы обходятся без них. К таким функциям относятся:
- изолирование приложений с помощью AppDomains;
- защита приложений и изолирование в песочнице.
- Другие функции – нужны всем средам выполнения, но при этом они не используют основные функции CLR. Такие функции отражают стремление создать полноценную среду программирования. К ним относятся:
- управление версиями;
- отладка/профилирование;
- обеспечение взаимодействия.
Видно, что хотя отладка и профилирование не являются основными или дополнительными функциями, они находятся в списке из-за ‘стремления создать полноценную среду программирования’.
Оставшаяся часть поста описывает, какие функции мониторинга, обеспечения наблюдаемости и интроспекции существуют в Core CLR, почему они полезны и каким образом среда предоставляет их
Диагностика
Для начала взглянем на диагностическую информацию, которой нас обеспечивает CLR. Традиционно для этого используется отслеживание событий для Windows (ETW).
Событий, о которых CLR предоставляет информацию, достаточно много. Они связаны со:
- сбором мусора (GC);
- JIT-компиляцией;
- модулями и доменами приложений;
- работой с тредами и конфликтами при блокировках;
- а также многим другим.
Например, здесь возникает событие во время загрузки в AppDomain, здесь событие связано с выбросом исключения, а здесь с циклом выделения памяти сборщиком мусора.
Perf View
Если вы хотите увидеть события в системе трассировки (ETW), связанные с вашими .NET приложениями, я рекомендую использовать великолепный инструмент PerfView и начать с этих обучающих видео или этой презентации PerfView: The Ultimate .NET Performance Tool. PerfView получил широкое признание за предоставляемую бесценную информацию. Например, инженеры Microsoft регулярно используют его для анализа производительности.
Общая инфраструктура
Если вдруг непонятно из названия, трассировка событий в ETW доступна только под Windows, что не очень вписывается в кроссплатформенный мир .NET Core. Можно использовать PerfView для анализа производительности под Linux (с помощью LTTng). Однако этот инструмент с командной строкой, под названием PerfCollect, только собирает данные. Возможности анализа и богатый пользовательский интерфейс (в том числе flamegraphs) в настоящий момент доступны только в решениях для Windows.
Но если вы всё-таки хотите проанализировать производительность .NET под Linux, есть и другие подходы:
Вторая ссылка сверху ведёт на обсуждение новой инфраструктуры EventPipe, над которой работают в .NET Core (помимо EventSources & EventListeners). Цели её разработки можно посмотреть в документе Cross-Platform Performance Monitoring Design. На высоком уровне эта инфраструктура позволит создать единое место, куда CLR будет отсылать события, связанные с диагностикой и производительностью. Затем эти события будут перенаправляться к одному или более логерам, которые, например, могут включать ETW, LTTng и BPF. Необходимый логер будет определяться, в зависимости от ОС или платформы, на которой запущена CLR. Подробное объяснение плюсов и минусов различных технологий логирования см. в .NET Cross-Plat Performance and Eventing Design.
Ход работы по EventPipes отслеживается в рамках проекта Performance Monitoring и связанных с ним ‘EventPipe’ Issues.
Планы на будущее
Наконец, существуют планы по созданию контроллера профилирования производительности Performance Profiling Controller, перед которым стоят следующие задачи:
Контроллер должен управлять инфраструктурой профилирования и представлять данные о производительности, созданные компонентами .NET, отвечающими за диагностику рабочих характеристик, в простом и кроссплатформенном виде.
Согласно замыслу контроллер должен обеспечивать следующие функциональные возможности через HTTP-сервер, получая все необходимые данные из инфраструктуры EventPipes:
REST APIs
- Принцип 1: простое профилирование: профилировать среду выполнения в течение периода времени X и возвращать трассировку.
- Принцип 1: продвинутое профилирование: начинать отслеживание (вместе с конфигурацией)
- Принцип 1: продвинутое профилирование: завершать отслеживание (ответом на этот вызов будет сама трассировка).
- Принцип 2: получать статистику, связанную со всеми счётчиками EventCounters или определённым EventCounter.
HTML-страницы, просматриваемые через браузер
- Принцип 1: текстовое представление всех стеков управляемого кода в процессе.
- Создаёт моментальные снимки запущенных процессов для использования в качестве простого диагностического отчёта.
- Принцип 2: отображение текущего состояния (возможно с историей) счётчиков EventCounters.
- Обеспечивает обзор существующих счётчиков и их значений.
- НЕРЕШЁННАЯ ПРОБЛЕМА: не думаю, что существуют нужные публичные API, чтобы подсчитывать EventCounters.
Я очень хочу увидеть, что в итоге получится с контроллером профилирования производительности (КПП?). Думаю, если его встроят в CLR, он принесёт много пользы .NET. Такой функционал есть в других средах выполнения.
Профилирование
Ещё одно эффективное средство, которое есть в CLR – API профилирования. Его (в основном) используют сторонние инструменты для подключения к среде выполнения на низком уровне. Подробнее про API можно узнать из этого обзора, но на высоком уровне с его помощью можно выполнять обратные вызовы, которые активируются, если:
- происходят события, связанные со сборщиком мусора;
- выбрасываются исключения;
- загружаются/выгружаются сборки;
- и многое другое.
Изображение со страницы BOTR Profiling API – Overview
Кроме того, у него есть другие эффективные функции. Во-первых, вы можете установить обработчики, которые вызываются каждый раз, когда выполняется метод .NET, будь то в самой среде или из пользовательского кода. Эти обратные вызовы известны как обработчики Enter/Leave. Вот здесь есть хороший пример, как их использовать. Однако для этого нужно понять конвенции вызовов для разных ОС и архитектур центрального процессора, что не всегда просто. Также не забывайте, что API профилирования это COM-компонент, доступ к которому можно получить только из кода C/C++, но не из C#/F#/VB.NET.
Во-вторых, профилировщик может переписать IL-код любого .NET-метода перед JIT-компиляцией с помощью SetILFunctionBody() API. Этот API действительно эффективен. Он лежит в основе многих инструментов APM .NET. Подробнее о его использовании можно узнать из моего поста How to mock sealed classes and static methods и сопутствующего кода.
ICorProfiler API
Оказывается, чтобы API профилирования работал, в среде выполнения должны быть всяческие ухищрения. Просто посмотрите на обсуждение на странице Allow rejit on attach (подробную информацию о ReJIT см. в ReJIT: A How-To Guide).
Полное определение всех интерфейсов и обратных вызовов API профилирования можно найти в vminccorprof.idl (см. Interface description language). Оно разбивается на 2 логические части. Одна часть – это интерфейс Профилировщик -> Среда выполнения (EE), известный как ICorProfilerInfo
:
// Объявление класса, который реализует интерфейсы ICorProfilerInfo*, позволяющие
// профилировщику взаимодействовать со средой выполнения. Таким образом, библиотека DLL профилировщика получает
// доступ к частным структурам данных среды выполнения и другим вещам, которые никогда не должны
// экспортироваться за пределы среды.
Это реализуется в следующих файлах:
Другая основная часть – обратные вызовы Среда выполнения -> Профилировщик, которые группируются под интерфейсом ICorProfilerCallback
:
// Этот модуль использует обёртки вокруг вызовов
// интерфейсов ICorProfilerCallaback* профилировщика. Если коду в среде нужно вызвать
// профилировщик, он должен пройти через EEToProfInterfaceImpl.
Эти обратные вызовы реализуются в следующих файлах:
- vmeetoprofinterfaceimpl.h
- vmeetoprofinterfaceimpl.inl
- vmeetoprofinterfaceimpl.cpp
- vmeetoprofinterfacewrapper.inl
Наконец, стоит заметить, что API профилирования могут не работать на всех ОС и архитектурах, на которых работает .NET Core. Вот один из примеров: ELT call stub issues on Linux. Подробную информацию см. в Status of CoreCLR Profiler APIs.
Profiling v. Debugging
В качестве небольшого отступления нужно сказать, что профилирование и отладка всё-таки немного пересекаются. Поэтому полезно понимать, что предоставляют различные API в контексте .NET Runtime (взято из CLR Debugging vs. CLR Profiling).
Разница между отладкой и профилированием в CLR
Отладка | Профилирование |
---|---|
Предназначена для поиска проблем с корректностью кода | Предназначено для диагностики и поиска проблем с производительностью |
Может иметь очень высокий уровень вмешательства | Как правило, имеет низкий уровень вмешательства. Хотя профилировщик поддерживает изменение IL-кода или установку обработчиков enter/leave, всё это предназначено для инструментирования, а не для радикального изменения кода. |
Основная задача – полный контроль цели. Это включает инспекцию, контроль выполнения (например команда set-next-statement) и внесение модификаций (функция Edit-and-Continue). | Основная задача – инспекция цели. Для этого предусмотрено инструментирование (изменение IL-кода, установка обработчиков enter/leave) |
Обширный API и толстая модель объекта, заполненная абстракциями. | Небольшой API. Абстракций мало или отсутствуют. |
Высокий уровень интерактивности: действия отладчика контролируются пользователем (или алгоритмом). Фактически редакторы и отладчики часто интегрированы (IDE). | Интерактивность отсутствует: данные обычно собираются без вмешательства пользователя, после чего анализируются. |
Небольшое количество критических изменений, если нужна обратная совместимость. Мы думаем, что миграция с версии 1.1 на версию 2.0 отладчика, будет простой или не очень сложной задачей. | Большое количество критических изменений, если нужна обратная совместимость. Мы думаем, что миграция с версии 1.1 на версию 2.0 отладчика, будет тяжёлой задачей, идентичной его полному переписыванию. |
Отладка
Разработчики по-разному понимают, что такое отладка. Например, я спросил в твиттере «как вы отлаживаете .NET программы” и получил множество разных ответов. При этом ответы действительно содержали хороший список инструментов и методов, поэтому я рекомендую их посмотреть. Спасибо, #LazyWeb!
Я думаю, что лучше всего всю суть отладки отражает это сообщение:
CLR предусматривает обширный список возможностей, связанных с отладкой. Однако зачем нужны эти средства? Как минимум три причины указаны в этом великолепном посте Why is managed debugging different than native-debugging?:
- Отладку неуправляемого кода можно абстрагировать на аппаратном уровне, но отладку управляемого кода необходимо абстрагировать на уровне IL-кода.
- Для отладки управляемого кода требуется много информации, которая недоступна до выполнения.
- Отладчик управляемого кода должен координировать действия со сборщиком мусора (GC)
Поэтому для удобства использования CLR должен предоставлять API отладки высокого уровня, известный как ICorDebug
. Он показан на рисунке ниже, отображающем общий сценарий отладки (источник: BOTR):
ICorDebug API
Принцип реализации и описание разных компонентов взято из CLR Debugging, a brief introduction:
Вся поддержка отладки в .Net реализуется поверх dll-библиотеки, которую мы называем The Dac. Этот файл (обычно под названием
mscordacwks.dll
) является структурным элементом как для нашего публичного API отладки (ICorDebug
), так и двух частных API отладки: SOS-Dac API и IXCLR.
В идеальном мире все бы использовалиICorDebug
, наш публичный API. Однако вICorDebug
не хватает множества функций, которые нужны разработчикам инструментов. Эта проблема, которую мы пытаемся исправить, где можем. Однако эти улучшения присутствуют только в CLR v.next, но не в более ранних версиях CLR. Фактически поддержка отладки по аварийному дампу появилась вICorDebug
API только с выходом CLR v4. Все, кто используют аварийные дампы для отладки в CLR v2 не смогут применитьICorDebug
совсем.
(Дополнительную информацию см. в SOS & ICorDebug)
На самом деле ICorDebug
API делится более чем на 70 интерфейсов. Я не буду приводить их все, но покажу, по каким категориям их можно разбить. Подробную информацию см. в Partition of ICorDebug, где этот список был опубликован.
- Верхний уровень: ICorDebug + ICorDebug2 – интерфейсы верхнего уровня, которые великолепно служат в качестве коллекции объектов ICorDebugProcess.
- Обратные вызовы: События отладки управляемого кода отсылаются через методы к объекту обратного вызова, реализуемого отладчиком.
- Процесс: Этот набор интерфейсов представляет работающий код и включает API, связанные с обработкой событий.
- Инспекция кода / типов: В основном работает со статическими PE-образами, но есть и удобные методы для реальных данных.
- Контроль выполнения: Возможность наблюдать за выполнением треда. На практике это означает возможность задавать точки останова (F9) и делать пошаговое прохождение кода (F11 вход в код, F10 обход кода, S+F11 выход их кода). Функция контроля выполнения ICorDebug работает только в управляемом коде.
- Треды + стеки вызовов: Стеки вызовов являются основой для функций инспекции, реализуемых отладчиком. Работа со стеком вызовов осуществляется с помощью следующих интерфейсов. ICorDebug поддерживает отладку только управляемого кода и, соответственно, можно отслеживать стек только управляемого кода.
- Инспекция объектов: Инспекция объектов – часть API, которая позволяет видеть значения переменных в отлаживаемом коде. Для каждого интерфейса я привожу метод MVP, который, как мне кажется, должен кратко описывать цель этого интерфейса.
Как и с API профилирования, уровни поддержки API отладки отличаются в зависимости от ОС и архитектуры процессора. Например, на август 2018 всё ещё нет решения для Linux ARM по диагностике и отладке управляемого кода. Подробную информацию о поддержке Linux можно посмотреть в посте Debugging .NET Core on Linux with LLDB и репозитории Diagnostics от Microsoft, которая стремится сделать отладку .NET программ под Linux проще.
Наконец, если хотите посмотреть, как ICorDebug
API выглядят в C#, взгляните на обёртки в библиотеке CLRMD, включая все доступные обратные вызовы (подробнее о CLRMD будет рассказано далее в этом посте).
SOS и DAC
Компонент доступа к данным (DAC) подробно рассматривается на странице BOTR. По сути, он обеспечивает внепроцессный доступ к структурам данных CLR, чтобы информацию внутри них можно было прочитать из другого процесса. Таким образом, отладчик (через ICorDebug
) или расширение ‘Son of Strike’ (SOS) может получить доступ к запущенному экземпляру CLR или дампу памяти и найти, например:
- все запущенные треды;
- объекты в управляемой куче;
- полную информацию о методе, в том числе машинный код;
- текущую трассировку стека.
Небольшое отступление: если хотите узнать, откуда взялись эти странные названия и получить небольшой урок истории .NET, посмотрите этот ответ на Stack Overflow.
Полный список команд SOS впечатляет. Если использовать его вместе с WinDBG, то можно узнать, что происходит внутри вашей программы и CLR на очень низком уровне. Чтобы увидеть, как всё реализовано, давайте посмотрим на команду !HeapStat
, которая выводит описание размеров различных куч, которые использует .NET GC:
(Изображение взято из SOS: Upcoming release has a few new commands – HeapStat)
Вот поток кода, который показывает, как SOS и DAC работают вместе:
- SOS Полная команда
!HeapStat
(ссылка) - SOS Код в команде
!HeapStat
, который работает с Workstation GC (ссылка) - SOS Функция
GCHeapUsageStats(..)
, которая выполняет самую тяжёлую часть работы (ссылка) - Shared Структура данных
DacpGcHeapDetails
, которая содержит указатели на основные данные в куче GC, такие как сегменты, битовые маски и отдельные поколения (ссылка). - DAC Функция
GetGCHeapStaticData
, которая заполняет структуруDacpGcHeapDetails
(ссылка) - Shared Структура данных
DacpHeapSegmentData
, которая содержит информацию об отдельном сегменте кучи GC (ссылка) - DAC
GetHeapSegmentData(..)
, которая заполняет структуруDacpHeapSegmentData
(ссылка)
Сторонние отладчики
Поскольку Microsoft опубликовала API отладки, сторонние разработчики смогли использовать интерфейсы ICorDebug
. Вот список тех, которые мне удалось найти:
- Debugger for .NET Core runtime от Samsung
- Отладчик позволяет использовать интерфейс адаптера отладки GDB/MI или VSCode, чтобы исправлять ошибки в приложениях .NET из-под среды выполнения .NET Core.
- Вероятно, написан как часть проекта по портированию .NET Core в их Tizen OS
- dnSpy – отладчик .NET и редактор сборок
- Очень мощный инструмент. Это отладчик, редактор сборок, шестнадцатеричный редактор, декомпилятор и многое другое.
- MDbg.exe (.NET Framework Command-Line Debugger)
- Доступен в виде NuGet-пакета. Его также можно скачать с репозитория GitHub или сайта Microsoft.
- Однако в данный момент MDBG, кажется, не поддерживает работу с .NET Core. Подробную информацию см. в Port MDBG to CoreCLR и ETA for porting mdbg to coreclr.
- JetBrains ‘Rider’ позволяет проводить отладку .NET Core на Windows.
- С этим инструментом наблюдались некоторые проблемы в связи с лицензированием
- Более подробную информацию можно найти в этой ветке HackerNews
Дампы памяти
Последнее, о чём мы поговорим, это дампы памяти, которые можно получить из работающей системы и проанализировать вне её. Среда выполнения .NET всегда отлично поддерживала создание дампов памяти под Windows. А теперь, когда .NET Core стала кроссплатформенной, появились инструменты, выполняющие ту же задачу на других ОС.
При использовании дампов памяти иногда сложно получить правильные, совпадающие версии файлов SOS и DAC. К счастью, Microsoft недавно выпустила dotnet symbol
CLI-инструмент, который:
может скачивать все необходимые для отладки файлы (наборы символов, модули, SOS и DAC файлы для определённого модуля coreclr) для любого определённого дампа ядра, минидампа или файлов любой поддерживаемой платформы, в том числе в формате ELF, MachO, Windows DLL, PDB и портативный PDB.
Наконец, если вы хотя-бы чуть-чуть занимаетесь анализом дампов памяти, рекомендую взглянуть на великолепную библиотеку CLR MD, которую Microsoft выпустила несколько лет назад. Я уже писал про её функции. Вкратце, с помощью библиотеки можно работать с дампами памяти через интуитивно понятный C# API, содержащий классы, которые обеспечивают доступ к ClrHeap, GC Roots, CLR Threads, Stack Frames и многому другому. Фактически CLR MD может реализовывать большинство (если не все) из SOS-команд.
Узнать о том, как она работает можно из этого поста:
Управляемая библиотека ClrMD – это обёртка вокруг API отладки, предназначенных только для внутреннего применения в CLR. Несмотря на то что эти API очень эффективны для диагностики, мы не поддерживаем их в виде публичных, задокументированных релизов, поскольку их использование сложно и тесно связано с другими особенностями реализации CLR. ClrMD решает эту проблему, предоставляя лёгкую в использовании, управляемую обёртку вокруг этих API отладки низкого уровня.
Автор: Stanislav Sidristij