ICorProfilerCallback2:CLR 探查器不记录所有离开调用

ICorProfilerCallback2: CLR profiler does not log all Leave calls

我正在尝试编写一个记录进程中所有 .Net 方法调用的分析器。目标是使其具有高性能,并在内存中保留最后 5-10 分钟(固定缓冲区,循环覆盖旧信息),直到用户触发将该信息写入磁盘。预期用途是追踪很少重现的性能问题。

我从 https://github.com/appneta/SimpleCLRProfiler 的 SimpleCLRProfiler 项目开始。分析器使用 .Net 分析的 ICorProfilerCallback2 回调接口。我让它在我的环境中编译和工作(Win 8.1、.Net 4.5、VS2012)。但是,我注意到有时记录了 Enter 调用的 Leave 调用会丢失。 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!");

相关的 SimpleCLRProfiler 方法是:

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 不会对所有离开方法执行离开调用?顺便说一句,我检查过 OnLeaveMethod 不会由于异常等原因在任何跟踪之前意外退出。它没有。

谢谢,克里斯托夫

由于 stakx 似乎不会回到我的问题来提供官方答案(并获得荣誉)所以我会为他做: 正如 stakx 所暗示的那样,我没有记录尾调用。事实上,我什至没有意识到这个概念,所以我完全忽略了那个钩子方法(它是有线的但空的)。我在这里找到了对尾调用的很好解释:David Broman's CLR Profiling API Blog: Enter, Leave, Tailcall Hooks Part 2: Tall tales of tail calls.

我引用上面的link:

Tail calling is a compiler optimization that saves execution of instructions and saves reads and writes of stack memory. When the last thing a function does is call another function (and other conditions are favorable), the compiler may consider implementing that call as a tail call, instead of a regular call.

考虑这段代码:

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

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

static public void Three() {
    ...
}

当方法 Three 被调用时,如果没有尾调用优化,堆栈将如下所示。

Three
Helper
Main

通过尾调用优化,堆栈如下所示:

Three
Main

所以在调用 Three 之前,由于优化,方法 Helper 已经从堆栈中弹出,因此,有堆栈上少了一个方法(更少的内存使用),也节省了一些执行和内存写入操作。