FlowTrace Implementation Highlights


Now I want to discuss a few of the implementation highlights. The first big issue I ran into was that in the future, managed threads won't have a one-to-one mapping with Win32 threads. The first versions of the Microsoft .NET Framework do have a one-to-one mapping. I first implemented FlowTrace using the reliable standby of thread local storage to ensure each thread had specific data. As I was perusing through Profiling.DOC, I noticed a special thread notification, ICorProfilerCallback::ThreadAssignedToOSThread. The description mentions that in the context of a CLR runtime thread, "During its execution lifetime, a given Runtime thread may be switched between different threads, or not—at the whim of both the Runtime and external components running within the process." That sure got my attention, and after checking with Microsoft, I realized the simple thread local storage solution wasn't going to work because FlowTrace would break in the future.

Fortunately, thread creation and destruction notifications in ICorProfilerCallback will give you the managed thread ID as a parameter, and you can call ICorProfilerInfo::GetCurrentThreadID to learn the thread ID at any time, so you'll have no issues identifying a managed thread. The downside is that I had to create my own "managed thread local storage" in a global Standard Template Library (STL) map class. Of course, to protect against multithreaded corruption, I had to protect it with a critical section. Since many of the Profiling API callback methods in ICorProfilerCallback make very explicit warnings against blocking when processing the methods, I was a little concerned. However, after a huge amount of testing, I don't think it has any noticeable effect.

The second big issue I had to deal with was how to support skipping all the startup code calls done by the System.AppDomain.SetupDomain method in the main thread. After doing some experimentation on numerous managed applications, I noticed that three threads run the application at startup. The Profiling API documentation mentions that by injecting a profiler into the managed process, a special thread dedicated to just the profiler starts up, but, fortunately, does not execute any managed code. I found that the first thread creation notification was always the main thread in the application, and the second thread notification was always the finalizer thread. Once I discovered how to identify the threads, I could come up with a plan for how to do the skip startup code processing. When skipping startup code, I have to shift to a state in which I don't record any processing on the main thread until after the function leave hook sees the call to System.AppDomain.ResetBindingRedirects come through.

I could see which thread was running as the finalizer, but I wanted to add the option to ignore it. My original idea was to set the FunctionIDMapper hook so that I could check the managed thread ID. If the managed thread ID was the finalizer thread, I could set the pbHookFunction parameter to FALSE so that the CLR would not call the hook function. When testing with the simplest IL assembly language program, everything worked great.

However, when I tested FlowTrace with a simple Microsoft Windows Forms application, I got assertions that the managed thread-specific data was NULL. Since I was ignoring the finalizer thread, I didn't add that managed thread to the managed map because I wanted to keep the map as small as possible. When I broke in the assertions, I noticed that the enter function hook was called on the finalizer thread. I thought for sure I had screwed up the algorithm, but a very careful inspection didn't explain why I was seeing a call on the finalizer thread.

After recording just those special cases on the finalizer thread and scouring the documentation, I finally figured out what was going on. In Windows Forms applications, you still have some of the oddities of COM, including the annoyance of apartment threading models. What I was seeing were cross-thread marshaled calls from the main thread into the finalizer thread. Interestingly, the CLR never called the FunctionIDMapper hook. Therefore, there was no way for me to short-circuit the hook calls. I was hoping I wouldn't have to check for the finalizer thread in the hook functions for performance reasons, but there was nothing I could do. So to avoid logging the finalizer calls, I had to check for the finalizer thread.

That worked like a charm, and if requested, I turned off the finalizer logging. A day or so later after I'd written the code, I realized I needed to utilize the FunctionIDMapper only when the user specifically requested I not monitor the finalizer thread. Originally, I was setting it in all cases.

The last item I had to tackle was ensuring that I kept my output straight no matter what. That meant I had to monitor any exceptions that unwound out of functions because I'd never see the leave function hook for them. Handling exception unwinding turned out to be quite simple to accomplish. All I needed to do was keep a running unwind count for the thread when the CLR called ICorProfilerCallback::ExceptionUnwindFunctionLeave. Once the exception hit ICorProfilerCallback::ExceptionCatcherLeave, I simply subtracted the number of unwound functions from the current indenting level.




Debugging Applications for Microsoft. NET and Microsoft Windows
Debugging Applications for MicrosoftВ® .NET and Microsoft WindowsВ® (Pro-Developer)
ISBN: 0735615365
EAN: 2147483647
Year: 2003
Pages: 177
Authors: John Robbins

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net