The .NET Profiling API and the DNProfiler Tool
http://msdn.microsoft.com/msdnmag/issues/01/12/hood/default.aspx
微软的.NET Common Language Runtime(CLR)内部提供了很多机制来创建更容易使用、更面向对象的平台。包括垃圾回收、标准的跨语言异常处理、广泛的类库、元数据、和已存native代码的互操作性、远程处理。也包括跨cpu指令格式(中间语言,IL)和将IL编译成能够在目标cpu上运行的代码的即时编译器。
随着系统的发展变得越来越复杂,能够理解系统的内部工作机制变得越来越有价值。在Windows®,调查可执行程序装载器、内存管理器的操作机制将展示很多不同的技巧。另外,有些技巧在window 9X 平台下能够正常工作,但是在Windows NT® 和Windows 2000不能工作,反之亦然。在Win32®下,查看进程的内部操作的最好方式是使用调试API,但是它也只能是包含很少的一部分。
在.NET下就完全不同了。因为CLR运行时是任何.NET程序的中心,它提供了一个逻辑位置来插入钩子以便观察.NET的内部。预计到工具开发者、系统级程序员的需要,Microsoft使用.NET来提供了一个非常详细的、一致的方式去观察进程的内部操作。本月,我要描述这种机制、并提供一个程序(DNProfile)来记录.NET运行时的操作。
The .NET Profiling API
观察.NET运行时动作的一种方式时使用剖析API。剖析API的命名不好,因为它对很多种事情都很有用,而不仅仅是剖析。考虑可以被剖析API观察的.NET动作列表,如下:
Figure 1 Functions of the Profiling API
CLR startup and shutdown
Application domain creation/shutdown
Assembly loads/unloads
Module loads/unloads
Class loads/unloads
COM interop VTable creation/destruction
JIT-compiles, code pitching, and pre-JITed method searches
Thread creation/destruction/suspends
Remoting activity
Exception handling
Managed/unmanaged code transitions
Garbage collection and managed heap objects
Method entry/exit
正如Keanu Reeves会说Whoooa! 相对于经过了多年的艰苦探索找到的如何给WINDOWS安装钩子来讲,Microsoft使观察CLR的行为变得非常容易了。列表中的每一个动作可以看作一个完全不同的CALLBACK。理论上讲,仅仅在CALLBACK函数中写需要的代码就可以了。
现在,最好的剖析API文档是.NET SDK下的Profiling.doc,在Tool Developers Guide\docs目录下。对于Beta2 来讲,这个文档和实现稍微有些不同步。在本期刊中,我不会广泛的讨论剖析API的每一个方面,而是重点讨论剖析API能做的大的方面。
.NET的一个大的卖点是它不再使用COM。这不是真的,事实上,剖析API就是基于COM的。使用剖析API包括创建一个进程内服务器,它实现了单一的接口。每一个接口方法代表了Figure 1中的一个事件。在CALLBACK方法内部,代码可以使用另外的COM接口(由CLR提供)来得到观察事件的信息。
个人来讲,我喜欢更简单的API来对所感兴趣的每一个事件进行注册。将API实现作为一个COM对象强迫你写几乎同样的代码以便使用COM对象。既然剖析API是基于COM的,剖析API的用户很可能使用C++。
尽管使用典型的COM而不是使用托管.NET代码起初看起来有些奇怪,仔细考虑之后就会明白。如果用托管代码实现接口,它将对监视的所有事件的产生负面影响。例如,如果实现接口的托管代码触发了异常怎么办?在API没有激活的情况下,异常事件也不会发生。
一旦你有一个实现了Profiling接口的COM服务器,下一步是强迫CLR装载COM服务器、调用它的方法。技巧是设置环境变量。跟注册表、XML文件比较起来,环境变量是挺古老的。
为什么不用注册表(配置文件)告诉CLR应该调用Profiling API?我从Microsoft开发者那里听说的一个原因是那要求.NET程序在启动时检查注册表将会有性能影响。考虑到.NET启动时有很多事情要做,这样的原因考虑的也是很慎重的。虽然如此,使用环境变量可以对所有程序起作用(如果在系统环境变量)或单独的进程。在后者情况下,调试器、剖析器等工具可以在启动程序时指定环境变量。稍后我将讨论所需要的环境变量。
The .NET Profiling API Interfaces
我检查的主要的接口是ICorProfilerCallback,在.NET Framework SDK的Include目录下的CorProf.IDL文件中定义。接口的实现需要使用剖析API。你的工作是提供实现。尽管有很多的方法,不要怕,对于他们中的大多说,可以简单的返回S_OK或E_NOIMPL。对于明确的事件的动作,在合适的CALLBACK方法中写合适的代码。
检查CALLBACK方法,你会发现大部分接收有关于那个事件的另外信息的参数。例如,JITCompilationStarted方法接收了一个名为functionId的UINT类型的参数。你可以对它做什么?答案在于接口ICorProfilerInfo。
ICorProfilerInfo可以提供任何剖析信息。考虑刚才提及的functionId,你可以调用ICorProfilerInfo::GetTokenAndMetaDataFromFunction,它返回给你元数据接口、函数的元数据token。使用元数据接口和token,你可以查询函数名、它所在的类、及你所知道的任何信息。
简单的讲,剖析API包含了两个COM接口。引入接口,由你来实现,是ICorProfilerCallback。当CLR事件发生时,CLR会调用接口中的某一个方法。引出接口,是ICorProfilerInfo,由CLR提供给你,让你在CALLBACK内部使用。
使用ICorProfilerCallback参数的普遍的模式是广泛的使用ID来代表函数、类、模块、程序集,等。ID是不透明的句柄。关于它的有意义的信息从ICorProfilerInfo接口来获得。程序执行过程中如果函数的代码被卸载,然后又被装载、并编译,特定的ID值(如functionId)可能会改变。但是有CALLBACK让你知道这个发生了。
ICorProfilerCallback方法可以分成逻辑的几组。大部分情况下,事件都有Started、Finished方法,并且成对的调用。让我们进行深入研究它,更好的了解我们可以通过CLR观察到什么。除非有特殊说明,下面列出的所有方法都属于ICorProfilerCallback接口。
Initialize/Shutdown Methods
在使用剖析API的进程中,Initialize是第一个被调用的方法。你的代码从这个方法得到ICorProfilerInfo指针。唯一的参数是LPUNKNOWN,对它调用QueryInterface方法得到ICorProfilerInfo指针。Initialize是你告诉剖析API你对哪些事件感兴趣的地方。
为了指出你感兴趣的事件,调用ICorProfilerInfo::SetEventMask方法,传递一个设置了合适的bit的DWORD类型的参数。这些标志来自于CorProf.h文件中的COR_PROF_MONITOR枚举变量。低位值标志被命名为COR_PRF_MONITOR_XXX,告诉CLR调用ICorProfilerCallback的哪些方法。例如,如果你想让ClassLoadStarted方法被调用,你必须设置COR_PRF_MONITOR_CLASS_LOADS标志。
ICorProfilerInfo::SetEventMask方法的参数的另外一些标志以一种方式、或者另一种方式改变CLR的行为.例如,如果你想在执行时监视对象的分配,必须设置COR_PRF_ENABLE_OBJECT_ALLOCATED标志。相似的,COR_PRF_DISABLE_INLINING告诉CLR不要内联任何函数。如果一个方法内联了,你将得不到ENTER、LEAVE通知。
你可以在以后的某个时刻调用ICorProfilerInfo::SetEventMask来修改你所感兴趣的事件。然而,某些事件是不可以改变的,意味着你一旦在Initialize设置了,他们将不能被修改。
当CLR终止进程时,Shutdown方法被调用。在某些情况下,它不会被调用,但是对于一个正常终止的.NET程序来讲,它应该被调用。
Application Domain Creation/Shutdown
这个种类的方法有AppDomainCreationStarted, AppDomainCreationFinished, AppDomainShutdownStarted, and AppDomainShutdownFinished。他们的名字是自描述的。这些方法的主要Token是AppDomainID。需要注意的是在AppDomainCreationStarted 回调方法中,不能使用AppDomainID,因为AppDomain还不存在。然而,一旦收到了AppDomainCreationFinished通知,就可以以AppDomainID为参数调用以为参数调用ICorProfilerInfo::GetAppDomainInfo 来得到新AppDomain的信息。
Assembly Loads/Unloads
在装载、卸载程序集时,AssemblyLoadStarted, AssemblyLoadFinished, AssemblyUnloadStarted, AssemblyUnloadFinished这些方法被调用。主要的Token是AssemblyID。需要注意的是在AssemblyLoadStarted方法中不能使用Token AssemblyID,因为程序集还不存在。然而,一旦收到AssemblyLoadFinished通知,就可以以AssemblyID为参数调用ICorProfilerInfo::GetAssemblyInfo来得到新程序集的信息。
Module Loads/Unloads
装载模块相关的函数有ModuleLoadStarted, ModuleLoadFinished, ModuleUnloadStarted, ModuleUnloadFinished, 和ModuleAttachedToAssembly。前四个函数的名字是自描述的。主要的Token是ModuleID。和AssemblyLoadStarted时的Token一样,传递给ModuleLoadStarted方法的Token也是不可用的。因为模块不存在。但是当收到ModuleLoadFinished通知后,就可以以ModuleID为参数调用ICorProfilerInfo::GetModuleInfo来得到新模块的信息。
当CLR将一个模块和一个程序集相关联起来时,最后一个函数,ModuleAttachedToAssembly,被调用。尽管模块和程序集经常是同一个文件,一个程序集也可能有多个模块。
Class Loads/Unloads
装载类相关的函数有ClassLoadStarted, ClassLoadFinished, ClassUnloadStarted, 和ClassUnloadFinished。这些函数的名字是自描述的。传递给ClassLoadStarted方法的Token也是不可用的。因为类不存在。但是当收到ClassLoadFinished通知后,就可以以ClassID为参数调用ICorProfilerInfo::GetClassIDInfo来得到新类的信息。
JIT Compilation
JIT编译方法(如图2)用到的主要Token是FunctionID。
JITCompilationStarted
JITCompilationFinished
JITCachedFunctionSearchStarted
JITCachedFunctionSearchFinished
JITFunctionPitched
JITInlining
这里,术语函数、方法交互的使用。以FunctionID为参数调用 ICorProfilerInfo::GetFunctionInfo来获得函数的信息。
方法JITCompilationStarted是很有趣的,因为它允许你在JITed之前查看、修改IL。查看ICorProfilerInfo:: GetILFunctionBody来获得细节,不要认为它很容易。方法JITCachedFunctionSearchStarted表示CLR寻找已经被JITed编译成native代码的函数。通过设置输出参数pbUseCachedFunction为FALSE,你可以强制运行时不考虑Pre-JITd的状态,使用最新的JITed版本。
方法JITFunctionPitched表示从内存中删除一个以前JITed的方法。只有在内存很少的情况下,才会发生。最后方法JITInlining表示JITer要内联函数。如果你要计算那个方法的enter/leave通知个数,可以设置输出参数pfShouldInline为FALSE来禁止内联。也可以通过传递给ICorProfilerInfo::SetEventMask的一个标志来禁止进程范围内的内联。
Threading
线程方法如图3
ThreadCreated
ThreadDestroyed
ThreadAssignedToOSThread
RuntimeSuspendStarted
RuntimeSuspendFinished
RuntimeSuspendAborted
RuntimeResumeStarted
RuntimeResumeFinished
RuntimeThreadSuspended
RuntimeThreadResumed
ThreadCreated/Destroyed方法包含了线程的生命周期。例如,概念上讲,一个CLR线程在其生命周期中可以运行在多个Win32线程之上。ThreadAssignedToOSThread方法指示了CLR正在运行在哪个Win32线程之上。
当CLR执行时,有时必须挂起部分或者所有的线程以便执行垃圾收集。RuntimeSuspend,RuntimeResume系列的方法指示CLR线程被挂起(实际比这更复杂,我在这里部深入细节了)。传递给RuntimeSuspendStarted的一个参数指示了线程为什么被挂起。最后两个RuntimeThread方法指示一个线程正在被挂起,并且那总在事件RuntimeSuspend内发生。
COM Interop
当CLR和普通的COM对象互操作时,需要代理接口。两个方法指示代理被创建、销毁。传递到这两个函数的参数是.NET ClassID,相应的COM接口IID,一个指向代理的虚函数表,虚函数表的项数。
Managed/Unmanaged Code Transitions
当托管代码调用非托管代码,或者非托管代码调用托管代码时,函数UnmanagedToManagedTransition 、ManagedToUnmanagedTransition被调用。传递给每个方法FunctionID来代表调用者,可以利用它调用ICorProfilerInfo::GetFunctionInfo来得到更多信息。
Garbage Collection and Managed Heap Objects
尽管没有实际的垃圾收集方法,ObjectAllocated, MovedReferences, ObjectsAllocatedByClass, ObjectReferences, RootReferences这些事件的触发指示了系统正在进行垃圾收集。这些通知携带的信息非常复杂,我不会解释他的全部,仅仅指出一些关键点。
当从托管堆中分配一个对象时,ObjectAllocated方法被调用(需要设置COR_PRF_ENABLE_OBJECT_ALLOCATED标志)。对象被一个ObjectID标示,同时也提供ClassID。可以利用ClassID来调用以便ICorProfilerInfo::GetClassIDInfo获得更多信息。
MovedReferences方法指出一个对象(由ObjectID来标示)已经被移动到内存中。ObjectsAllocatedByClass方法指出最后一次垃圾收集之后,又创建了哪个类的实例。ObjectReferences方法提供了一个特定对象引用的对象列表。最后,RootReferences方法指出了所有根对象引用的对象列表。
Remoting Activity
远程方法如图4
RemotingClientInvocationStarted
RemotingClientSendingMessage
RemotingClientReceivingReply
RemotingClientInvocationFinished
RemotingServerReceivingMessage
RemotingServerInvocationStarted
RemotingServerInvocationReturned
RemotingServerSendingReply
它指出了在远程方法调用的过程中的各种执行点。当程序作为客户端时,方法被调用;当程序作为远程服务器时,方法被调用。在这两种情况下,远程方法的调用,实际的传输消息被区分开来。
Exception Handling
剖析API对异常处理的支持相当复杂,并且非常完全。详细的细节请参考文档。本质上来讲,在异常处理的每一个阶段,API在之前、之后通知你。图5显示了异常方法。
ExceptionThrown
ExceptionSearchFunctionEnter
ExceptionSearchFunctionLeave
ExceptionSearchFilterEnter
ExceptionSearchFilterLeave
ExceptionSearchCatcherFound
ExceptionOSHandlerEnter
ExceptionOSHandlerLeave
ExceptionUnwindFunctionEnter
ExceptionUnwindFunctionLeave
ExceptionUnwindFinallyEnter
ExceptionUnwindFinallyLeave
ExceptionCatcherEnter
ExceptionCatcherLeave
ExceptionCLRCatcherFound
ExceptionCLRCatcherExecute
ExceptionThrown方法是你收到异常的第一个指示。以ObjectID为参数调用m_pICorProfilerInfo::GetClassFromObjec来得到异常的类型。对于堆栈中的每一个托管方法,当CLR查找方法中的try块,执行方法中的filter,或者找到处理异常一个方法时,CLR通知你。当堆栈展开时,执行finally块时,或者执行处理代码时,CLR也会通知你。
Receiving Method Entry and Exit Notifications
剖析API最酷的特性之一是当任意一个托管方法开始执行时,要返回到调用者时,都能够通知你。因为在执行.NET方法之前需要JITed。如果你对方法的enters,leaves感兴趣,可以通知JITer,JITer在方法的开始、结束时,调用你提供的特定的方法。同时也有一个tailcal通知,概念上讲它和优化函数推出相似。不过我从没有在.NET下看到该事件的产生。
不象我描述的所有其他的事件,enter/leave回调方法部属于ICorProfilerCallback接口。而是使用接口ICorProfilerCallback,ICorProfilerInfo来进行设置,以便JITed调用你提供的方法。有趣的是,回调函数有个限制,不能修改寄存器的值。所以你提供的代码很有可能包含一段汇编语言,使用__declspec naked函数。请参考图6的一个例子。
void __declspec( naked ) EnterNaked()
{
__asm
{
push eax
push ecx
push edx
push [esp+16]
call RealEnterFunctionInCPP
pop edx
pop ecx
pop eax
ret 4
}
}
只有一个参数(FunctionID)被传递到enter/leave回调函数中。文档警告,如果回调函数阻塞或很长时间没有返回,将会导致可怕的后果。但是,就我的经验来看,使用ICorProfilerInfo来查询FunctionID的名字,并写入文件中,并被有问题。
投入一些努力,你就可以利用enter/leave写出一个象样的监听程序来显示每一个方法的执行。如果你写了这样的程序的话,你就会发现,仅仅启动一个小.NET程序就有很多的方法调用。因为同时有enter,leave回调函数,可以看到有好多嵌套方法调用。
需要注意的是.NET运行时可能使用pre-JITed的函数。除非你采取特殊的步骤,否则这些方法不会产生回调。有时可以通过监视JITCachedFunctionSearchStarted并且返回FALSE来修正这个问题。
Caveats with the Profiling API
如果想使用剖析API来实现某些功能,需要知道某些很重要的限制。首先,它假定COM服务器是free-threaded。在调用ICorProfilerCallback方法时,.NET运行时不回作任何的同步。如果你用全局数据,必要时你需要使用临界区保护数据。或者使用线程局部存储,请参考DNProfiler例子程序。
另一个限制是在ICorProfilerCallback方法中,不能调用任何托管代码,不管是直接调用还是间接调用。剖析API被有被设计为可重入的,可调用托管代码的,如果违反了,将会很麻烦的。
剖析API提供了获得ICorDebug的方法。这允许你得到特定的调用堆栈细节。但是当你执行进程内调试时,并不是所有的ICorDebug方法都是可用的。查看ICorDebug文档来确定哪些在进程内调试时是可以调用的,哪些是不可以调用的。
最后,剖析API是如此的酷,我想可以同时用它写好多工具。很不幸,某一时刻,只能有一个剖析COM服务器。如果你追求时间信息,你可能在某一时刻只想有一个剖析器。然而,考虑到非剖析工具,如果Microsoft提供一个方案,允许一个工具处理这些事件,并且可以将事件继续发送到下一个工具就好了。
The DNProfiler Sample
为了显示剖析API的很酷的性质,我写了一个简单的实现,记录每一个CALLBACK方法。对于一些方法,代码执行一些额外的工作使它更有意义。例如,当触发了ClassLoadFinished时,DNProfiler接收了一个ClassID,写出了类的名字。
DNProfiler不会显示每一个CALLBACK的所有的信息,但是它做了相当的工作显示发生了什么。本期刊中,我没有实现函数的ENTER/LEAVE CALLBACK,但是Microsoft的例子实现了这些。
DNProfiler将结果写入到和父进程同一目录下的DNProfiler.out的文本文件中。DNProfiler观察到的启动事件的一部分如下:
Figure 8 Sample Output
Initialize
ThreadCreated
ThreadAssignedToOSThread
ThreadCreated
ThreadAssignedToOSThread
AssemblyLoadStarted
ModuleLoadStarted
ModuleLoadFinished:c:\winnt\microsoft.net\framework\v1.0.2914\mscorlib.dll
ModuleAttachedToAssembly: mscorlib
AssemblyLoadFinished: mscorlib Status: 00000000
ClassLoadStarted: System.Object
ClassLoadFinished
ClassLoadStarted: System.ValueType
ClassLoadFinished
ClassLoadStarted: System.ICloneable
ClassLoadFinished
... // lines omitted
ObjectAllocated: System.OutOfMemoryException
ObjectAllocated: System.StackOverflowException
注意到DNProfiler通过缩进表示嵌套。在这个例子中,AssemblyLoadStarted和AssemblyLoadFinished之间,DNProfiler接收到另外三个事件,已经被合适的缩进。
DNProfiler仅仅是个DLL形式的COM组件,没有EXE执行。你或者自己编译它,或者使用REGSVR32对它进行注册。一旦注册成功,最简单的使用方式时使用一个控制台窗口运行Profiling_on.bat文件。
Figure 9 profiling_on.bat
set Cor_Enable_Profiling=0x1
set COR_PROFILER={9AB84088-18E7-42F0-8F8D-E022AE3C4517}
@REM COR_PRF_MONITOR_FUNCTION_UNLOADS = 0x1,
@REM COR_PRF_MONITOR_CLASS_LOADS = 0x2,
@REM COR_PRF_MONITOR_MODULE_LOADS = 0x4,
@REM COR_PRF_MONITOR_ASSEMBLY_LOADS = 0x8,
@REM COR_PRF_MONITOR_APPDOMAIN_LOADS = 0x10,
@REM COR_PRF_MONITOR_JIT_COMPILATION = 0x20,
@REM COR_PRF_MONITOR_EXCEPTIONS = 0x40,
@REM COR_PRF_MONITOR_GC = 0x80,
@REM COR_PRF_MONITOR_OBJECT_ALLOCATED = 0x100,
@REM COR_PRF_MONITOR_THREADS = 0x200,
@REM COR_PRF_MONITOR_REMOTING = 0x400,
@REM COR_PRF_MONITOR_CODE_TRANSITIONS = 0x800,
@REM COR_PRF_MONITOR_ENTERLEAVE = 0x1000,
@REM COR_PRF_MONITOR_CCW = 0x2000,
@REM COR_PRF_MONITOR_REMOTING_COOKIE = 0x4000 |
@REM COR_PRF_MONITOR_REMOTING,
@REM COR_PRF_MONITOR_REMOTING_ASYNC = 0x8000 |
@REM COR_PRF_MONITOR_REMOTING,
@REM COR_PRF_MONITOR_SUSPENDS = 0x10000,
@REM COR_PRF_MONITOR_CACHE_SEARCHES = 0x20000,
@REM COR_PRF_MONITOR_CLR_EXCEPTIONS = 0x1000000,
@REM COR_PRF_MONITOR_ALL = 0x107ffff,
@REM COR_PRF_ENABLE_REJIT = 0x40000,
@REM COR_PRF_ENABLE_INPROC_DEBUGGING = 0x80000,
@REM COR_PRF_ENABLE_JIT_MAPS = 0x100000,
@REM COR_PRF_DISABLE_INLINING = 0x200000,
@REM COR_PRF_DISABLE_OPTIMIZATIONS = 0x400000,
@REM COR_PRF_ENABLE_OBJECT_ALLOCATED = 0x800000,
@REM COR_PRF_ALL = 0x1ffffff,
set DN_PROFILER_MASK=0x1A7ffff
一旦运行,所有的从那个控制台窗口启动的.NET程序将使用DNProfiler。为了设回正常模式,运行相应的Profilng_off.bat文件。
Profiling_On.bat设置了三个环境变量,其中两个由CLR使用。进程启动时,当 Cor_Enable_Profiling环境变量设置为非零时,CLR试图装载由COR_PROFILER环境变量指出的COM服务器。COR_PROFILER环境变量可以是CLSID或ProgID.
第三个环境变量是DN_PROFILER_MASK,由DNProfiler使用。它允许你动态的配置你所感兴趣的方法,而不是硬编码到代码中。在DNProfiler的Initialize方法,它获得DN_PROFILER_MASK的值,并传递到ICorProfilerInfo::SetEventMask中。在.bat文件中,我已经将mask设置为包含任何事件,但是我鼓励你测试其它的值。
Figure 7是DNProfiler的主要的代码段。在CCorProfiler::Initialize方法的开始附近,它是进行初始化的地点。Initialize方法调用了一个私有函数,GetInitializationParameters来读取环境变量、生成输出文件的全路径。
ProfilerCallback.cpp文件中的大部分函数是简单的实现ICorProfilerCallback接口方法。如果函数时一个开始函数,代码将调用ChangeNestingLevel帮助函数。函数更新每线程的跟踪嵌套层数的变量。当相应的函数完成时,代码再次调用ChangeNestingLevel来恢复原始值。
文件的结尾处是一些私有函数,来做共同的功能,如从ClassID查询类名字。ProfilerPrintf函数和fprintf相同,只是它考虑了每个现成的嵌套层数,并且在实际文本之前使用空格填充缩进。
ProfilerCallback.h是描述CProfilerCallback类的头文件。注意CProfilerCallback是如何从ICorProfilerBack接口继承的。在类定义的尾部时一些私有函数、成员变量。
最后的源文件是COMStuff.CPP。它创建了DNProfiler,我想使事情尽可能简单,没有使用ATL,MFC,或不是必需的任何东西。我是一个Framework爱好者。任何情况下,Microsoft的例子采取了相似的方法。我偷用相关的部分代码,并且使它更简单些,结果就是COMStuff.CPP。
Interop Fun with DNProfiler
很容易花费一些时间学习DNProfiler的输出,了解.NET程序的事件顺序。我不会学习所有的感兴趣的事件。有一个需要提及,.NET WINDOWS FORMS包在USER32.DLL的基础上进行构件,你可能怀疑当发送消息时,需要在托管代码、非托管代码间进行来回转换。
确实是这样的。在托管代码、非托管代码间如此之多的递归真令人吃惊。我意思是说,托管代码调用非托管代码,非托管代码接着又调用托管代码。在托管代码内,又需要调用非托管代码,如此下去。当然最后,堆栈展开到原始的级别。
察看Figure 10
ManagedToUnmanagedTransition:
System.Windows.Forms.UnsafeNativeMethods::DispatchMessageW
UnmanagedToManagedTransition: WndProc::Invoke
ManagedToUnmanagedTransition:
System.Windows.Forms.UnsafeNativeMethods::CallWindowProc
UnmanagedToManagedTransition: WndProc::Invoke
ManagedToUnmanagedTransition:
System.Windows.Forms.UnsafeNativeMethods::IntDestroyWindow
UnmanagedToManagedTransition: WndProc::Invoke
ManagedToUnmanagedTransition:
System.Windows.Forms.UnsafeNativeMethods::SendMessage
UnmanagedToManagedTransition: WndProc::Invoke
ManagedToUnmanagedTransition:
System.Windows.Forms.UnsafeNativeMethods::CallWindowProc
它显示了销毁窗口消息的处理过程。在托管代码中,UnsafeNativeMethods::DispatchMessageW被调用,它传递到非托管代码的WndProc::Invoke方法,WndProc::Invoke内部需要调用托管代码UnsafeNativeMethods::CallWindowProc。这个函数接着调用非托管方法WndProc::Invoke。这个方法在托管代码、非托管代码间来回调用,在UnsafeNativeMethods::DispatchMessageW函数返回前,达到了很深的调用层。
Microsoft为.NET提供的剖析接口对工具开发者来讲在很长的一段时间内是很好的。很广泛的、相当有灵活性的.NET工具将都是基于它开发的。这里我给出了它的能力的整体预揽,一定要察看文档、通过Microsoft提供的Profiler例子获得更多的信息。