- Download demo projects - 58 Kb
- Download source - 44.6 Kb
- Download my output file from HelloWorld.exe - 605 Kb
Contents
- Introduction
- Background
- What is a Profiler?
- How Does the CLR Enable Profiling?
- The Sample Profiler
- Solutions and Projects
- Output File Results
- Using the code
- The DotNetProfiler Project
- Profiler Initialization
- Profiler Interaction
- Profiler Shutdown
- The ProfilerTest Project
- Running the Pre-built Sample
- Building the Source Code
- Points of Interest
- Other References
- History
Introduction
.Net application들을 프로파일러 툴들이 어떻게 후킹하는지 궁금해 본적 있나요? 이 글은 닷넷 applcation을 위한 커스텀 닷넷 프로파일러를 생성하는 방법을 다루고 있습니다. 이 프로파일러는 실행되기위한 가장기본적인 것만 다루게 될것입니다. 그라나 어떻게 생성되고 여러분들이 더많은 기능을 제공할수 있게 하기위한 뼈대를 제공하게 됩니다.
이 프로파일러는 다음 기능들을 가지고 있습니다.
1)호출된 함수들의 내부적인 맵을 관리합니다.
2)각함수의 호출횟수를 관리합니다.
3)콜 스택의 내부 호출을 관리합니다.
이프로파일러를 사용했을 때의 결과물은 어플리케이션이 구동되는 동안 일어나는 것들을 리포팅하는 아웃풋 파일입니다. 이 파일의 내용을 깊이 보면 여러분들은 기존에 보던방식으로 닷넷 코드를 보진 않을것입니다.
The result of using this profiler is an output file that reports what occurred during the course of an application's run. After seeing the depth of the contents of this file, I guarantee that you will never look at your .NET code the same way again.
NOTE: I want to mention up front that this profiler implementation is for .NET 2.0 applications and the sample projects are VS 2005 projects. I'd love to post a .NET 1.1 / VS 2003 solution, but would have to revert my machine to do so. There are references at the end of this article which point to sample SDK projects for .NET 1.1.
Background
프로파일러란 무엇인가?
프로파일링은 일반적으로 전반적인 응용프로그램의 영역의 시간을 잴수 있는 기능을 가지고 있습니다. 그래서 병목현상을 발견할수 있지요. 인터넷에는 응용프로그램을 프로파일링할수있는 여러개의 프로파일러를 찾을수 있습니다. 닷넷에서 프로파일러는 COM DLL로 작성되어있고 닷넷 프로파일러를 위한 최소한의 필요조건은 ICorProfilerCallback 인터페이스를 구현하는 것입니다. 확장된 인터페이스 ICorProflerCallback2는 닷넷 2.0에서 소개되었고 포함된 샘플에 사용되고 있습니다. 이러한 인터페이스는 여러분들의 프로그램 실행에 알고싶은 모든 것(enter/exits,threads switches, assembly loads/unload, class loads/unload, JIT 컴파일, managed/unmanaged code transtion, garbage collection, exception handling 등등)들에 콜백을 제공합니다.
CLR은 닷넷 응용프로그램들을 구동시키는 엔진입니다. 또한 정확히 어떠한일 들이 벌어지고 있나 알수있게 해주는 로직컬한 장소를 제공합니다. Win32 디버그 API가 8개의 노티피케이션들을 제공해주는 반면 ,ICorProfilerCallback2 프로파일링 인터페이스는 약 80개정도를 제공해줍니다.
CLR이 프로세스를 구동시킬때 두가지의 환경 변수들을 보는데 아래와 같습니다.
1. COR_ENABLE_PROFILING: 이 환경변수는 1또는 0으로 1은 CLR이 프로파일러를 사용해야한다고 0은 사용해서는 안된다 입니다.
2. COR_PROFILER: 지금까지 프로파일하기 원하는 CRL에 대해 말했고 CLR에 어떤 프로파일러를 사용할건지 알려줘야합니다. 때문에 프로파일러는 COM 객체로 구현되어있고 이환경변수는 ICorProfilerCallback2인터페이스를 구현한 coclass의 GUID로 셋팅되어야합니다. 여러분은 꼭 IDL프로파일러 IDL에 이 GUID를 찾아야합니다. 이프로젝트에서는 coclass GUID는 {9E2B38F2-7355-4C61-A54F-434B7AC266C0} 입니다.
이러한 환경변수를 설정하는 방법은 두가지가 있는데요 글로벌하게 모든 닷넷 응용프로그램에 대해서 설정할수 있고요(이건좀 아닌것같음) 다른한가지는 수동으로 Process객체를 사용해서 프로세스를 만들어서 그 프로세스만 설정하게 하는 방법입니다. If you've ever added a Profiling Sessionto your solution in Visual Studio 2005 and run it with the toolbar button, this is likely what's happening behind the scenes.
프로세스가 시작되고 CLR이 어떤 프로파일러를 사용할지 설정되면 프로파일러 객체가 생성되고 OCorProfilerCallback2 인터페이스에 질의를 할겁니다. 이렇게 다 되면 프로파일러에 모든 진행되는 것들에 대해 노티피케이션이 발생합니다. 다음 이미지를 참조해주세요
The Sample Profiler
Solutions and Projects
샘플은 2개의 솔루션을 포함하고 있습니다.
- DotNetProfiler.sln: 이솔루션은 DotNetProfile이라는 하나의 C++ 프로젝트를 포함하고 있고 이프로젝트에서는 ATL을 이용해 실제적인 프로파일러를 구현하고 있습니다.
- ProfilerTest.sln: 이솔루션은 ProfileerLanuncher라는 C# 응용프로그램을 포함하고 이 프로젝트는 버튼이 있어서 버튼을 눌렀을때 프로세스를 생성해 환경변수를 설정합니다 그리고 그 프로세스안에서 다른 응용프로그램을 실행합니다. C# 으로 된HelloWorld 프로젝트는 테스트 응용프로그램으로써 프로파일링 될 대상입니다. 이또한 하나의 버튼이 있어서 클릭할때 "Hello world"라는 메세지 박스를 노출해 합니다.
Output File Results
처음에 언급했듯이 이 글에서 프로파일러는 output파일을 생성합니다. 이 파일은 LOG_FILENAME환경 변수를 사용하여 설정됩니다. 로그파일의 내용은 2개의 파로로 나눠지게 되는데 call stack과 function 요약정보입니다.
Call Stack: 간단하게 함수들의 리스트를 나타나게 되는데 각 함수들이 depth를 들여쓰기로 나타내고 있습니다. 아래의 샘플을 참조해주세요
System.IO.StringWriter.Write, id=70671928, call count = 622 System.Text.StringBuilder.Append, id=21629200, call count = 635 System.IntPtr.op_Inequality, id=21665400, call count = 717 System.String.AppendInPlace, id=9670768, call count = 635 System.Configuration.XmlUtilWriter.AppendAttributeValue, id=73427152, call count = 14 System.Xml.XmlTextReader.get_QuoteChar, id=70046432, call count = 14 System.Xml.XmlTextReaderImpl.get_QuoteChar, id=70049120, call count = 14
이 샘플은 2개의 메인(StringWriter.Write,
XmlUtilWriter.AppendAttributeValue) 호출을 보여주고 있는데요 만약 함수가 이전 함수보다 들여써져 있으면 이 함수는 바로전의 함수로부터 호출되었다는 의미입니다.Function Summary: 프로그램이 구동되는 동안 호출되는 모든 함수들을 나열한 것인데요 여기서 모든의 의미는 모든 Assembly의 모든 함수들을 뜻하는 것입니다. 프로그램이 구동되는동안 리스트의 각함수는 몇번 호출되는지 토탈 숫자를 보여주고 있습니다. 만약 똑같은 함수가 여러번 나타나면 그건 함수가 overload된 것을 의미합니다. 아래의 샘플을 참조해주세요:
System.String.Join : call count = 2 System.String.SmallCharToUpper : call count = 1 System.String.EqualsHelper : call count = 1106
기본적으로 결과 파일의 이름은 "ICorProfilerCallbackLog.log"로 되고 실제적으로 실행되는 Assembly폴더에 쓰여지게 됩니다. (여기서는 \bin\Debug directory of HelloWorld).
Using the code
DotNetProfiler Project
이프로젝트는 ICorProfilerCallback2인터페이스를 구현한 COM DLL을 빌드합니다. 이섹션에서는 프로젝트에서 사용하고있는 클래스들을 다룹니다.
CCorProfilerCallbackImpl: 첫번째로 이 클래스는 아무것도 하지않는 클래스 처럼 보입니다. ㅁ맞습니다. 아무것도 안합니다. 왜냐하면 모든 프로파일러는 ICorProfilerCallback인터페이스에 정의된 모든 함수들을 구현해야하는데 너무많은 함수가 있어서 일단 이 클래서에서 그에 대한 함수를 정의해 놓고 아무것도 구현하지 않습니다. 그리고 우리가 실제적으로 관심이 있는 기능들을 이 클래스를 상속받아 override해주게 됩니다.CFunctionInfo: 이 클래스는 함수의 프로토타입을 표현하기 위해 디자인 됐습니다. CLR은 각 함수에 특정 ID를 부여하게 됩니다. 심지어 overload된 함수들도 각각 특정 ID값을 가지고 됩니다. 우리가 만든 프로파일러는 STL <code><code>map(Hashtable)의 객체들을 ID를 키로해서 관리합니다. 맵에 없는 함수 ID가 있으면 우리는 CFunctionInfo객체를 생성하고 맵에 추가합니다. 이 클래스 객체는 또한 호출 횟수를 관리해서 함수가 호출될때 그함수의 카운터를 증가시킵니다.CProfiler: 이클래스는 프로파일러 구현의 메인 클래스 입니다. 프로파일러를 초기화하고 CFunctionInfo 객체들을 관리합니다. 또한 결과파일을 관리하고 구동기간동안 계속해서 로그를 작성합니다.
Profiler Initialization
CLR이 우리의 프로파일러를 인식한후 첫번째로 Initialize함수를 호출합니다. 호출할때 넘겨지는 파라미터는 ICorProfilerInfo 인터페이시를 구현한 객체의 포인터입니다. 만약 닷넷2.0을 사용하고 있다면 이 객체는 ICorProfilerInfo2인터페이스도 구현되어있습니다. 우리의 CProfiler클래스는 CComQIPtr 스마트 포인터를 관리하지만 우리는 여기서 단순히 ICorProfilerInfo인터페이스 포인터만 사용합니다.
// get the ICorProfilerInfo interface HRESULT hr = pICorProfilerInfoUnk->QueryInterface(IID_ICorProfilerInfo, (LPVOID*)&m_pICorProfilerInfo); if (FAILED(hr)) return E_FAIL; // determine if this object implements ICorProfilerInfo2 hr = pICorProfilerInfoUnk->QueryInterface(IID_ICorProfilerInfo2, (LPVOID*)&m_pICorProfilerInfo2); if (FAILED(hr)) { // we still want to work if this call fails, might be an older .NET version m_pICorProfilerInfo2.p = NULL; }
이 객체는 마치 CLR이 함수에 할당한 ID의 메타정보를 추출하는것과같이 CLR로부터 현제의 컨텐스트 정보를 추출하는데 사용됩니다.
ICorProfilerInfo인터페이스의 정보를 추출했다면 다음으로 CLR에 우리가 어떤 타입의 알림을 받을건지(노티) 알려줘야합니다. 이러한 작업은 SetEventMast함수를 호출함으로써 할수있고 CProfiler에 SetEventMast함수는 enter/leave 알림을 등록합니다. 그러나 나머지는 열거되어있고 주석으로되어있으니 어떤기능이 가능한지 보시면 됩니다.
Collapse | Copy Code
// set the event mask DWORD eventMask = (DWORD)(COR_PRF_MONITOR_ENTERLEAVE); m_pICorProfilerInfo->SetEventMask(eventMask);
다음으로는 "함수의 enter와 leave" 훜를 등록하는단계입니다. 등록을 하기위해서는 세개의 callback함수를 우리의 CProfiler객체에 등록해줘야합니다.
// set the enter, leave and tailcall hooks hr = m_pICorProfilerInfo->SetEnterLeaveFunctionHooks ((FunctionEnter*)&FunctionEnterNaked, (FunctionLeave*)&FunctionLeaveNaked, (FunctionTailcall*)&FunctionTailcallNaked);
FunctionEnterNaked: 함수에 들어올때 호출됩니다.FunctionLeaveNaked: 함수를 빠져 나올때 호출됩니다.FunctionTailcallNaked: 함수의 마직막 엑션이 다른함수를 호출할때 호출 됩니다.
C++에서 callback 함수들은 __declspec(naked)로 선언되어야합니다. Additionally the routines must preserve any CPU registers they use and restore them before returning. 각 callback은 3파트를 가집니다. ("Enter" callback으로 설명됩니다.):
FunctionEnterNaked: 이 assembly 구현은 FunctionEnterGlobal함수가 호출되는동안 레지스터들을 보존합니다.FunctionEnterGlobal: 글로벌 함수는 프로파일러를 호출합니다.Enter: FunctionEnter 알림의 구현 객체입니다.
NOTE: I have to admit that this had me scratching my head as well, however, it's the way that Microsoft implements these functions. This was probably the most difficult part of the profiler to get working properly. I'm not sure why they didn't just add
Enter,LeaveandTailcallas functions to be implemented on theICorProfilerCallbackinterface.마지막단계로 함수를 callback들에게 맵핑하기위한 등록입니다. 이함수가 뭘하려고 만들어졌나 여러정의를 봤지만 잘 설명된것은 전에 실행되지 않았던 함수가 호출되면 호출된 함수의 ID를 콜백해준다는 것이다. 이내용은 CFunctionInfo 객체를 STL 맵에 생성하기에 딱맞는 장소이다. 이러한 방법으로 우리는 "enter" 또는 "leave" 콜백을 가질때 맵의 객체를 얻을수 있습니다.
// set the function mapper callback hr = m_pICorProfilerInfo->SetFunctionIDMapper((FunctionIDMapper*)&FunctionMapper);
Profiler Interaction
Initialization이 일어나면 CProfiler객체는 사용될 준비가 다 됩니다. enter/leave 훜ㄱ과 명세된 COR_PRE_MONITOR_ENTERLEAVE 를 알림 플래그로 등록했기때문에 우리는 함수에 들어갈때 떠날때 알림을 받을수 있습니다. 이러한 작동은 아래와 같습니다.:
- CLR로부터 새로운 함수가 맞닥뜨려진다.
- CLR은 에 ID를 생성함수하고 CProfiler의 FunctionMapper함수를 호출한다.
CProfiler는 ICorProfilerInfo를 사용해서 주어진 ID의 함수이름 추출한다. 프로파일러는 CFunctionInfo객체를 이 정보와함께 생상하고 function ID를 키로해서 STL 맵에 추가한다.- CLR은 다음으로 CProfiler의 FunctionEnterNaked callback 함수를 호출하고 함수의 ID를 넘긴다. 그리고 CProfiler의 Enter함소로 전달 된다.
- Enter 함수안에서 CProfiler는 맵에 함수 ID를 보고 맵에 CFunctionInfo 객체를 찾으면 아웃풋파일에 로그를 기록한다 또한 호출 횟수를 증가신키다. 그리고 콜 스택의 depth를 증가시킨다.
- CLR은 그 함수를 실행한다.
- 다음으로 CLR은 CProfiler읜 FunctionLeaveNaked 콜백함수를 호출하고, 함수 ID를 넘긴다. 이 아ID는 CProfiler의 Leave 함수로 이동된다.
- Leave함수와 TailCall함수내부에는 CProfiler 의 스택 dpeth를 차감한다.
Profiler Shutdown
프로그램이 종료될때 CLR은 CProfiler의 Shotdown함수를 호출합니다. 이 함수의 내부에서 프로파일러는 CFunctionInfo 맵을 거닐고 각 함수의 이름을 기록합니다 그리고 로그파일에 콜카운터를 기록합니다. 그리고 마지막으로 CFunctionInfo객체를 맵에서 해제합니다.
The ProfilerTest Project
이 프로젝트는 닷넷 어플리케이션을 프로파일링하기 위해 환경변수를 설정하는 간단한 응용프로그램입니다. 대부분의 작동은 버튼 클릭 핸들러 안에서 처리됩니다. 자세히 말씀 드리면 핸들러 내부에서 ProcessStartInfo 객체를 생성하고 전 섹션에서 설명한 환경번수들을 설정합니다. 그런다음 이러한 정보들을 이용해서 프로세스가 실행되고 샘플 프로파일러를 호출하기 됩니다.
이 응용프로그램에서 가장 흥미있는점은 아무 프로파일러에 아무 응용프래그램을 사용할수 있습니다. Form1.cs에 위에 선언된 2개의 상수가 있는데 아래와같습니다.:
// profiler GUID
private const string PROFILER_GUID = "{9E2B38F2-7355-4C61-A54F-434B7AC266C0}";
// executable to run
private const string EXECUTABLE_TO_RUN = "HelloWorld.exe";
다른 프로파일러를 사용하기 위해서는 PROFILER_GUID를 사용하고 싶은 프로파일러의 GUID로 변경해주면되고 다른 응용프로그램을 프로파일하고싶으면 EXECUTEABLE_TO_RUN을 프로파일하고 싶은 어플리케이션으로 변경해주면됩니다.
Running the Pre-built Sample
- 디렉토리에 dotnetprofiler_demo.zip 파일의 압축을 푼다
dll을 RegSvr32를 이용해서 등록한다.:
regsvr32.exe DotNetProfiler.dll
등록이 잘 됐는지 확인해주세요
- ProfilerLauncher.exe실행하고. "Launch and Profile!"버튼을 클릭해 HelloWorld.exe를 실행합니다.
- HelloWorld매우 느리게 실행되면 "Say Hello World" 버튼을 클릭하면 "Hello world"가 나옵니다.
- HelloWorld를 종료하고 ProfilerLauncher를 닫습니다.
- "ICorProfilerCallback Log.log" 파일이 dll이있는폴더에 있을겁니다. 노트패드로 열어보세요
Building the Source Code
- Load the DotNetProfiler.sln solution in Visual Studio 2005. The project links with the corguids.lib library, which should be in your C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Lib folder.
- Build the solution.
- Load the ProfilerTest.sln solution in Visual Studio 2005.
- Build the solution. Building it isn't dependent on DotNetProfiler.dll.
- Copy DotNetProfiler.dll, ProfilerLauncher.exe and HelloWorld.exe into their own directory.
- Follow the steps from Running the Pre-built Sample (including registering the DLL).
Points of Interest
- First and foremost... you will be astounded at the amount of calls being made just to execute HelloWorld.exe. If you don't want to run the sample, you can download the sample output of profiling HelloWorld.exe. Now, consider the complexity of your own applications versus the complexity of HelloWorld.exe. Amazing...
- This profiler implementation is purely for show. Because it writes continuously to a file, it's horribly slow. It's not meant to be used in production... it's simply a model for building your own profiler. There are many modifications that could be made to make it better.
- I have still not found any way to debug a custom profiler. All debugging I've done has been through output file messages. If you can find a way to debug one of these, I'd love to know how.
- Because the profiler is attached to the executing process, it is extremely difficult to profile ASP.NET applications and services using this technique.
Other References
There are some good references out there about using ICorProfilerCallback and ICorProfilerCallback2. Here are a few:
- Profiling.doc: This document is the most comprehensive collection of information about
ICorProfilerCallbackavailable. It comes as part of the .NET 1.1 SDK, but is sadly missing from the .NET 2.0 SDK, so I've posted a link to it. - MSDN ICorProfilerCallback Reference and MSDN ICorProfilerCallback2 Reference
- There is an excellent example of implementing
ICorProfilerCallbackin the C:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Tool Developers Guide\Samples\profiler\hst_profiler folder. HST stands for Hot Spot Tracker and this is a much more full featured profiler than my sample. My sample is designed to demonstrate the basics, without going into the depth that hst_profiler does. This article does a very good job of covering some of the details of that profiler. - Another excellent example of implementing
ICorProfilerCallbackis in the C:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Tool Developers Guide\Samples\profiler\gcp_profiler folder. GCP stands for General Code Profiler and this is a much more full featured profiler than my sample. This article does a very good job of covering some of the details of that profiler. - This article does a very good job describing all of the different notifications you can subscribe to using
ICorProfilerCallback. It also comes with it's own implementation of a profiler.
History
- Aug 30, 2006: Initial release
- Sep 4, 2006: Added a link to Profiling.doc under the Other References section.
License
This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.
A list of licenses authors might use can be found here