|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionI have been developing user level applications for Windows CE based devices for about 5 years now. The quality of the development tools for this platform has improved considerably in those 5 years. However, the tool quality still pales in comparison to what is available to a Windows Desktop developer. A debugger exists for these mobile devices, however it is not always possible to use the debugger. Many times when debugging an application a developer will be reduced to using MessageBox or writing data to a log file. For some of the projects that I have helped develop, the developers have created a series of C pre-processor macros for instrumenting their code. These macros can be selectively compiled into the application when instrumentation reporting is desired. Normally the output is written to a log file. Typically a macro is created to indicate when a function is entered, when it is left, the input parameters and the return values, and even reporting macros for certain checkpoints in the code. I like the verbose output that a report like this provides, however, just about every macro scheme that I have encountered was just as verbose. Quite often these macro entries interfere with the readability of the code, they are cumbersome to fill out. They use the I decided to try to harness the power of C++, and create an instrumentation framework that exhibits these characteristics:
This article documents the framework that I have developed. I was able to accomplish most of the goals listed above. Some of the goals were only able to be mere improvements over the code that inspired this framework. For most of the goals however, I believe I have created a much better model that will make it possible to easily instrument the code. Along the way, I discovered that I could not only instrument the code with basic output logging, but I could also include a basic profiling framework for very little extra effort. Here is a list of the profiling features that this framework provides:
FrameworkLet's first describe the framework for the readers that are more interested in using this code rather than how it works. The system is contained in a single header file called FnTrace.h. There are approximately ten to fifteen macros designed to be used by the developer to assist in code implementation. Here are the names of these commands:
Each of these macros provide a specific purpose, and not all of them are required to be used. The selection of macros will be based on what the developer would like to accomplish. RequirementsThis section describes the basic requirements for the
UsageThis section will describe the actions that are required in order to enable function instrumentation for your application. All of the macros that are intended for direct use will also be documented and explained. Activate InstrumentationIn order to activate instrumentation, the following macro will need to be defined before the #define FN_INSTRUMENT
Declare Default Log Level (Optional)You can declare your desired instrumentation log levels for your application. If you do not define these log levels, the default level of #define FN_FUNC_LEVEL_DEF FN_3
#define FN_PARAM_LEVEL_DEF FN_5
#Include "FnTrace.h"Include the FN Trace framework header in each of the files that you intend to instrument. If you are using precompiled headers, I would suggest that you add the include the header file in your precompiled header. Change Instrumentation Levels at RuntimeThe MACRO in this section can be used to change the instrumentation level dynamically at runtime. This could be attached to a UI option or part of a message processor in your main windows FN_INSTRUMENT_LEVEL(F,P);
Initialize Thread InstrumentationFor each thread that will be instrumented, the following entry will need to be defined at the beginning of the thread function to initialize the thread state for the framework: FN_INSTRUMENT_THREAD;
Enable Profiling (Optional)If function profiling is desired, then declare the following MACRO at the top of your FN_ENABLE_PROFILE(1);
You can turn function profiling off at a later point in the program at run time by calling the same macro with a value of FN_ENABLE_PROFILE(0);
Enable Instrumentation for a FunctionFor each function that you would like to instrument in your application, add the following MACRO to the top of the function scope inside of the braces: FN_FUNC(retType, level);
ex:
bool IsEmpty()
{
FN_FUNC(bool, FN_1);
...
}
Notify Framework of Return ValueThe FN Framework is capable of reporting the value that is returned by a function when the function exits. In order to enable this feature, a programming convention will need to be followed. You will need to declare the macro from this section, as well as allocate space for a return value. The name of the return variable is passed to the macro, and at the function exit, this variable should be in the FN_RETVAL(retVar);
ex:
bool IsEmpty()
{
...
bool retVal = false;
FN_RETVAL(retVal);
...
retVal = true;
return retVal;
}
Enable Function Instrumentation with Return ValueThis section is a combination of the previous two macros. This macro can be defined, and make this a one-step process. FN_FUNC_RETVAL(retType, retVar, level);
ex:
bool IsEmpty()
{
bool retVal = false;
FN_FUNC_RETVAL(bool, retVal, FN_1);
...
retVal = true;
return retVal;
}
Enable Full Function InstrumentationFor each function that you would like to instrument in your application, declare the MACRO from this section at the top of the function scope after the braces. This macro will initialize the appropriate state to report the name of the function, each of the parameters that you indicate, and report the return value when the function exits. This MACRO will also initiate profiling for this function if you have enabled it for the framework. In order to use this version of the MACRO, you will need Visual Studio 2005 or greater because of the variadic pre-processor operator FN_FUNC_PARAMS(retType, retVar, level, n, ...);
ex:
BOOL SetRect(LPRECT lprc, int xLeft, int yTop, int xRight, int yBottom)
{
BOOL retVal = FALSE;
FN_FUNC_RETVAL(BOOL, retVal, FN_1, 5, lprc, xLeft, yTop, xRight, yBottom);
...
retVal = TRUE;
return retVal;
}
Any parameter type can be reported as long as there is a conversion available to the // Sample OStream implementation for the pointer to RECT struct
inline std::wostream& operator<<(std::wostream& os, const RECT* _Val)
{
if (_Val)
{
os << "RECT(" << _Val;
os << "): left " << _Val->left;
os << ", top " << _Val->top;
os << ", right " << _Val->right;
os << ", bottom " << _Val->bottom;
}
else
{
//C: A Null pointer was passed in.
os << L"(null);
}
return os;
}
// output:
// RECT(0034EF5A): left 0, top 0, right 640, bottom 480
Report Messages in Function ScopeUse the FN_OUTPUT(output);
ex:
TCHAR windowName[100];
::_tcscpy(windowName, _T("Test Application"));
...
FN_OUTPUT(L"The current window name is: " << windowName);
The #define FN_OUTPUT_CLOG
Parameter ReportingYou can use the following macro to report the name and value of a single variable or function parameter. FN_PARAM(param);
ex:
int x = 100;
FN_PARAM(x);
//result:
//x = 100
Selective Function InstrumentationSome functions are called so frequently that if you were to log every single entrance into the function, it would quickly fill your buffers. In this scenario, you would have too much information and it would no longer be beneficial to instrument your application because there would be too much data to sift through without automated tools. A perfect example of this is a These next two MACROs are meant to be used in conjunction to provide selective instrumentation for a function. The first macro will prepare a function scope for instrumentation, the second MACRO is an instrumentation checkpoint. If a checkpoint is reached, then data will be printed out. If the function is entered, but a checkpoint is never reached, then no data will be reported for the function.
FN_ENTRY(level);
The FN_CHECKPOINT(label, level, n, ...);
ex:
...
case IDM_HELP_ABOUT:
{
FN_CHECKPOINT("IDM_HELP_ABOUT", FN_5, 4, hWnd, message, wParam, lParam);
DialogBox(g_hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, About);
}
break;
...
Any parameter type can be reported as long as there is a conversion available to the Pause and Resume Profile TimingIf function profiling is enabled, this MACRO can be used to stop and start the timer for a function. This could be useful when the function calls a kernel function that blocks for a long period of time, such as FN_PAUSE();
FN_RESUME();
ex:
// Main message loop:
FN_PAUSE();
while (GetMessage(&msg, NULL, 0, 0))
{
FN_RESUME();
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
FN_PAUSE();
}
ImplementationThe All of the object definitions and global variables are declared within the The following sub-sections will document the basic path I followed to arrive with the current implementation. I will indicate my initial motivation for a design, what possibly worked and what failed, why I changed it, and I will indicate what the final solution was for each of the major components of the framework. Message ReportingWhile developing the framework, I used Once I reached the point in my development to create this object, I remembered the Win32 function The one downside, is that I need to have a debugger attached to my application while it is running to listen for the debug strings. The good news is that there are plenty of debugging tools that you can run on your system that are not actually debuggers, but tools that listen to the The only challenge in making this work was to create a In an eye towards portability, I also made it possible to redirect the output to the Function NameMy initial vision for this framework was to find a way to declare a macro that could extract all of the information that it needed at compile time from the declaration of the function, or behind the scene in the disassembly of the program. I did not like the fact that all of the basic instrumentation MACRO implementations that I had seen required the developer to type in the name of the function. This made instrumenting the application cumbersome and error prone. If you cut and paste, it would be too easy to either forget to fill in the correct name of the function when you first instrument, or change the name in each place the instrumentation MACRO is used in a function if the function name changes. My first thought was to use the compiler defined MACROS The first reason I dumped this idea, is that it was another piece of software that I would have to write. Sadly it is not the reason that I did not go this route. The real reason is that I found out about some Microsoft Specific pre-processor macros, namely Return ValueAnother aspect that I have not liked about the instrumentation MACROs I have seen up until now, is that in order to report a ...
INSTRUMENT_OUT_1("FunctionA: returns %s", name);
return name;
}
Again, the name of the function would have to be typed in, a cumbersome In order to tackle this, I created the an object on the stack at the entry of the function. During the destructor of this object, I was going to search the stack or the function registers for the One intermediate solution to this problem was to use a MACRO that redefines the My final solution, was to pre-declare a bool retVal = false;
FN_RETVAL(retVal);
...
if (x < minVal)
{
return retVal;
}
...
retVal = true;
return retVal;
}
Parameter ValuesI probably spent most of my development time working on a simple and robust way to report the parameter values for a function call. I really did not like the way that most #define TRACE_1(msg, p0)
#define TRACE_2(msg, p0, p1)
#define TRACE_3(msg, p0, p1, p2)
#define TRACE_4(msg, p0, p1, p2, p3)
#define TRACE_5(msg, p0, p1, p2, p3, p4)
#define TRACE_6(msg, p0, p1, p2, p3, p4, p5)
#define TRACE_7(msg, p0, p1, p2, p3, p4, p5, p6)
Just to get the ball rolling, I declared this macro: #define FN_PARAM(param) FN_OUTPUT(#param << " = " << ##param##)
This required a I started investigating the Stack Frames assembly, and I looked at the x86 Base Pointer, EBP (also known as Frame Pointer). From here, I could walk up the stack, and the first parameter was going to be I was already using the After realizing the deficiencies of the automatic stack method, I thought about what other problems could arise. A major problem with automatically trying to grab the parameters, is that many times a parameter could be an "out only" parameter. This would mean the data is invalid coming into the function and worthless to report to the log. When this new factor was coupled with the previous fact that type information was now gone, it seemed to me that there was too much work in this new method for very little gain. The final step in this path was discovering the Boost pre-processor library. This library is a very cool set of expanded pre-processor macros. Some of the macros are quite complicated and it took me a bit of time to sift through the documentation and find the few that I actually needed. I settled on the Ironically, I decided to provide the cumbersome form of macros that inspired this framework in the beginning, but only to help support compilers that do not support the variadic pre-processor MACRO Thread SafetyThread Safety is provided by allocating a set of variables to each thread through Thread-Local Storage (TLS). The TLS mechanism in the Windows Desktop compiler turns out to be a much simpler implementation than the path I had to take to add support for the Windows CE counterpart. The Windows Desktop supports a compiler-defined definition For Windows CE based systems, I had to use the WIN32 APIs designed to provide TLS support:
Currently there are four values that are allocated for each thread in the Windows implementation of the framework:
Each of these parameters is conditionally initialized during the thread creation macro, based on the compiler and other A series of helper macros have been created and utilized to abstract the access to the TLS variables as much as possible in an attempt to reduce the impact that the access to these variables will have on the overall performance on programs instrumented with this framework. ProfilingProfiling was really added as an after thought because all of the pieces fell into place that it would not take much more effort to add this feature. Currently, the profiling features are very primitive, and provide a very limited amount of detail about the application. In fact, the timer resolution is reported in milliseconds, therefore the actual use for the timing functionality is quite limited. Therefore, at this point, this feature is really about proof-of-concept. All of the profiling features are currently facilitated by a Function Use CountThe first feature that was quite simple to implement was the ability to count the number of times that a function is entered. This is accomplished incrementing a counter in the
Function Execution TimeThis feature keeps track of the total amount of time that the CPU spends within the scope of a particular function. More precisely, the amount of time that the local function state object exists on the stack for each particular instantiation of a function. This feature was a bit tricky to complete and make entirely useful. The main issue was the fact that a single function call may call into other functions. Time spent in these "callee" functions should not necessarily be counted toward the execution time of "caller". If it were not this way, your applications My first attempt was to simply create a stack object in the TLS, that would allow the start time of each function to be pushed onto the stack. Then when the function exited, the item at the top would be popped off the stop and subtracted from the current time. This would indicate the total usage of the current function. This is when I realized the first issue I described above. The usage for each function lower on the stack was continually reported as a much larger value than I had expected. What I needed to do was create a mechanism that would keep track of the usage of each function further down the stack. Then when I got to the end of a function, I would subtract the total time spent further down the stack, from the current execution time for the current function. This concept seemed pretty straight forward when I first started. To help facilitate this, I changed the stack object to use an stl One last issue to tackle was to allow the profiling to account for function calls that this framework was not actually managing, such as blocking calls or calls into libraries that are time consuming. In order to give the developer some sort of control over the timing instrumentation I added the The function timing feature has much potential to be useful. Before it can reach its full potential I will need to convert the time to a high-precision timer, much higher than millisecond resolution. Here are a few possible uses for measuring the execution time of your applications functions:
Bug: Profiling Summary Does not Print to LogWhen I first created all of the tools, I wrote the data to DemoThe demo application is simply an example of how to use the different macros. It is an application that is compatible with both the Windows Desktop and Window Mobile devices. It is the default application that is generated by the Visual Studio 2005 project wizard, that I have instrumented with the OutputHere is a sample of the output to expect from the demo application: 3068454610: int __cdecl WinMain(struct HINSTANCE__ *,struct HINSTANCE__ *,wchar_t *,int)
3068454610: hInstance = B6DE9E56
3068454610: hPrevInstance = 00000000
3068454610: lpCmdLine =
3068454610: nCmdShow = 5
3068454610: int __cdecl InitInstance(struct HINSTANCE__ *,int)
3068454610: hInstance = B6DE9E56
3068454610: nCmdShow = 5
3068454610: unsigned short __cdecl MyRegisterClass(struct HINSTANCE__ *,wchar_t *)
3068454610: hInstance = B6DE9E56
3068454610: szWindowClass = INSTRUMENT
3068454610: elapsed: 51ms
3068454610: return (50540)
3068454610: long __cdecl WndProc(struct HWND__ *,unsigned int,unsigned int,long)
3068454610: WM_CREATE
3068454610: hWnd = 7C079270
3068454610: message = 1
3068454610: wParam = 0
3068454610: lParam = 470153100
3068454610: elapsed: 122ms
3068454610: leave;
3068454610: long __cdecl WndProc(struct HWND__ *,unsigned int,unsigned int,long)
3068454610: WM_ACTIVATE
3068454610: hWnd = 7C079270
3068454610: message = 6
3068454610: wParam = 1
3068454610: lParam = 0
3068454610: elapsed: 133ms
3068454610: leave;
3068454610: elapsed: 988ms
3068454610: return (1)
3068454610: long __cdecl WndProc(struct HWND__ *,unsigned int,unsigned int,long)
3068454610: IDM_OK
3068454610: hWnd = 7C079270
3068454610: message = 273
3068454610: wParam = 40000
3068454610: lParam = 2080871584
3068454610: long __cdecl WndProc(struct HWND__ *,unsigned int,unsigned int,long)
3068454610: WM_ACTIVATE
3068454610: hWnd = 7C079270
3068454610: message = 6
3068454610: wParam = 0
3068454610: lParam = 0
3068454610: elapsed: 136ms
3068454610: leave;
3068454610: long __cdecl WndProc(struct HWND__ *,unsigned int,unsigned int,long)
3068454610: WM_DESTROY
3068454610: hWnd = 7C079270
3068454610: message = 2
3068454610: wParam = 0
3068454610: lParam = 0
3068454610: elapsed: 114ms
3068454610: leave;
3068454610: elapsed: 0ms
3068454610: leave;
3068454610: elapsed: 0ms
3068454610: return (0)
ConclusionThis is a work in progress that was inspired by the need to instrument production level application code, that would remain in place and would not reduce the readability of the code. This framework is built around the mechanics available in C++, and attempt to be as non-intrusive as possible. I have just completed my first pass of this framework, and I will now attempt to use it. I will be making improvements to this article if I find deficiencies in the framework. I welcome any comments or suggestions to help improve on its design and implementation. In the future, I hope to improve the profiling reporting functionality as well as include features to record and report code coverage paths and other useful metrics. History
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||