Introduction
Did you ever try to find a wired multithreading or synchronization bug using the normally provided method like TRACE()
or OutputDebugString()
? Did your debug compile running fine, but your release application got a page fault?
A lot of useful information, which can be generated automatically, is missing in the traditional approaches and you have to provide it on your own. Most of the macros only work in debug mode and are not available in release mode.
But the major point is that, the timing and the flow of the application to be debugged is changed by using the debug tools.
I tried to find a thread synchronization bug in one of my applications and I developed the trace routines provided here as a solution.
Problems
The first problem is the timing, which is changed significantly, for example, by OutputDebugString()
. Unfortunately, the time depends on the debugger attached to show the output.
I have done 1.000.000 traces in a loop and measured the time for it:
TRACE -Macro
Debug-Code
No Debugger attached | 28 sec. |
TRACE -Macro
Debug-Code
VS.NET 2003 attached | 1600 sec. (based on 20.000 traces) |
TRACE -Macro
Debug-Code
DbgView | 210 sec. (based on 100.000 traces) |
Tracer
Debug-Code | 9 sec. |
Tracer
Release-Code | 5 sec. |
As you can see, Tracer is much faster and available even in release mode code.
The second problem which comes with OutputDebugString()
is that, this routine seems to be synchronized itself. By using this routine in two concurrent threads very often, you can shift the timing by the internal synchronization of OutputDebugString()
***.
Basics
The trace classes write their output to normal text files on your drive. By using some special tricks, there is as less synchronization and latency as possible. For each thread tracing some information, the tracer creates one file in the application's directory named like "<exe-name>. trace.<threadid>". Each line in the tracing has the form:
00000128fd9f283f-00000320 : CCameraView::SetOptions() entered
The first part is the hardware timer value generated by QueryPerformanceCounter()
. The second value is the thread id, which is generated by the tracing. By using the provided TraceMerge.exe utility, the different files can be merged to one big file, sorted by time.
The tracing is switched on and off using an environment variable. The name can be provided in the startup call.
Internals
The tracing code is implemented as a singleton class. The singleton can be accessed using CTracer::Get()->Trace()
. The singleton maintains a map containing a TLSDataObject
object for each thread. This class contains a big write buffer containing all the trace strings and the handle to the trace file. A critical section is provided for protecting the buffer.
When doing the first trace for a specific thread, the tracer create a TLSDataObject
, stores it in the map and in a thread local storage (TLS) index. By using the TLS, a thread can access its own TLSDataObject
without any synchronization to other threads. Only the insertion in the map (so the first trace) has to be synchronized.
Tracer starts a background thread which writes down the buffers of each thread. So for a short time, the tracing has to be stopped. But notice, that only the background thread and one application thread at a time are linked together.
Threads, which have been closed, are removed from the map from time to time to release the write buffer memory.
Using the Code
Include "singleton.h", "tracer.cpp", "tracer.h", "CritSecLocker.cpp" and "CritSecLocker.h" to your project. Include "tracer.h" in all sources you want to trace.
In InitInstance()
(or something similar), startup the tracing using:
CTracer::Get()->Startup("MYAPP_TRACE");
In ExitInstance()
(or something similar) don't forget to call:
CTracer::Get()->Shutdown();
In your code, use:
CTracer::Get()->Trace("A number %d",123);
to produce a trace line. Note that appending "\n
" is not allowed because it breaks the merging tool.
If you want to flush all available buffers, you may use:
CTracer::Get()->Flush();
This writes all buffers of all threads and flushes the Win32 file buffers too. It's useful, for example, before aborting the application in an exception handler.
If you have a member function doing a lot of traces, providing a local reference is useful:
void foo(void)
{
CTracer &l_tracer = CTracer::Get();
...
l_tracer->Trace("fewfewoifnwefewf");
...
l_tracer->Trace("fewfewoifnwefewf");
}
Using TraceMerger
To create one trace file from all the different files for each thread, a small utility has been provided.
Use it like this:
TraceMerger.exe <Application Exe Path>
TraceMerger
will get all the trace files and merge sort them based on time. The results will be saved to a new file named <Application Exe Path>.trace.merge.
License
This package is licensed under The Artistic License.
*** If you know something about the OutputDebugString()
synchronization, tell it to me...