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例子獲得更多的資訊。