ICorProfilerCallback2: профилировщик CLR не регистрирует все вызовы Leave

Я пытаюсь написать профилировщик, который регистрирует все вызовы методов .Net в процессе. Цель состоит в том, чтобы сделать его высокопроизводительным и сохранить, скажем, последние 5-10 минут в памяти (фиксированный буфер, циклическая перезапись старой информации) до тех пор, пока пользователь не инициирует запись этой информации на диск. Предполагаемое использование — отследить редко повторяющиеся проблемы с производительностью.

Я начал с проекта SimpleCLRProfiler из https://github.com/appneta/SimpleCLRPProfiler. Профилировщик использует интерфейс обратного вызова ICorProfilerCallback2 профилирования .Net. Я получил его для компиляции и работы в моей среде (Win 8.1, .Net 4.5, VS2012). Однако я заметил, что иногда отсутствуют вызовы Leave, для которых были зарегистрированы вызовы Enter. Пример вызова Console.WriteLine (вывод DbgView я уменьшил до минимально необходимого для понимания):

Line 1481: Entering System.Console.WriteLine
Line 1483: Entering SyncTextWriter.WriteLine
Line 1485: Entering System.IO.TextWriter.WriteLine
Line 1537: Leaving SyncTextWriter.WriteLine

Два входящих вызова не имеют соответствующих исходящих вызовов. Профилированный код .Net выглядит так:

Console.WriteLine("Hello, Simple Profiler!");

Соответствующие методы SimpleCLRPProfiler:

HRESULT CSimpleProfiler::registerGlobalCallbacks() 
{
   HRESULT hr = profilerInfo3->SetEnterLeaveFunctionHooks3WithInfo(
      (FunctionEnter3WithInfo*)MethodEntered3, 
      (FunctionEnter3WithInfo*)MethodLeft3, 
      (FunctionEnter3WithInfo*)MethodTailcall3);

   if (FAILED(hr))
      Trace_f(L"Failed to register global callbacks (%s)", _com_error(hr).ErrorMessage());

   return S_OK;
}

void CSimpleProfiler::OnEnterWithInfo(FunctionID functionId, COR_PRF_ELT_INFO eltInfo)
{
    MethodInfo info;
   HRESULT hr = info.Create(profilerInfo3, functionId);
   if (FAILED(hr)) 
      Trace_f(L"Enter() failed to create MethodInfo object (%s)", _com_error(hr).ErrorMessage());

   Trace_f(L"[%p] [%d] Entering %s.%s", functionId, GetCurrentThreadId(), info.className.c_str(), info.methodName.c_str());
}

void CSimpleProfiler::OnLeaveWithInfo(FunctionID functionId, COR_PRF_ELT_INFO eltInfo)
{
   MethodInfo info;
   HRESULT hr = info.Create(profilerInfo3, functionId);
   if (FAILED(hr)) 
      Trace_f(L"Enter() failed to create MethodInfo object (%s)", _com_error(hr).ErrorMessage());

   Trace_f(L"[%p] [%d] Leaving %s.%s", functionId, GetCurrentThreadId(), info.className.c_str(), info.methodName.c_str());
}

Есть ли у кого-нибудь идеи, почему .Net Profiler не будет выполнять вызовы Leave для всех методов ухода? Между прочим, я проверил, что OnLeaveMethod не завершается неожиданно перед любой трассировкой из-за исключения или чего-то подобного. Это не так.

Спасибо, Кристоф


person Christoph    schedule 20.06.2015    source источник
comment
Оптимизация хвостового вызова, возможно?   -  person stakx - no longer contributing    schedule 20.06.2015
comment
Ну, есть шаблон, отсутствующие методы обычно встроены, поэтому не могут генерировать выход. SyncTextWriter.WriteLine() не является, у него есть [MethodImpl]. Мне не совсем понятно, почему вы видите Enter, вам, вероятно, следует также смотреть ICorProfilerCallback::JITInlining().   -  person Hans Passant    schedule 20.06.2015
comment
Стакс, ты понял! Я не регистрировал хвостовые вызовы. На самом деле, я даже не знал об этой концепции, поэтому я полностью проигнорировал этот метод ловушки. Я нашел хорошее объяснение здесь: blogs.msdn.com/b/davbr/archive/2007/06/20/. Я предлагаю вам создать официальный ответ на эту проблему, и я отмечу его как таковой. Спасибо!   -  person Christoph    schedule 21.06.2015


Ответы (1)


Поскольку stakx, похоже, не возвращается к моему вопросу, чтобы дать официальный ответ (и получить признание), поэтому я сделаю это за него: как намекнул stakx, я не регистрировал хвостовые вызовы. На самом деле, я даже не знал об этой концепции, поэтому я полностью проигнорировал этот метод ловушки (он был подключен, но пуст). Я нашел хорошее объяснение хвостовых вызовов здесь: Блог Дэвида Бромана по CLR Profiling API: Enter, Leave, Tailcall Hooks Part 2: Небылицы о хвостовых вызовах< /а>.

Цитирую ссылку выше:

Вызов хвоста — это оптимизация компилятора, которая сохраняет выполнение инструкций и экономит чтение и запись памяти стека. Когда последнее, что делает функция, — это вызывает другую функцию (и другие условия благоприятны), компилятор может рассмотреть возможность реализации этого вызова как хвостового вызова вместо обычного вызова.

Рассмотрим этот код:

static public void Main() {
    Helper();
}

static public void Helper() {
    One();
    Three();
}

static public void Three() {
    ...
}

При вызове метода Three без оптимизации хвостового вызова стек будет выглядеть следующим образом.

Three
Helper
Main

С оптимизацией хвостового вызова стек выглядит так:

Three
Main

Таким образом, перед вызовом Three из-за оптимизации метод Helper уже был извлечен из стека, и в результате в стеке стало на один метод меньше (меньше использование памяти). а также были сохранены некоторые выполнения и операции записи в память.

person Christoph    schedule 23.06.2015