7A. Tracing

Tracing

The most useful general tool for debugging software is tracing. Put prints in your program that turn on the lights so that you can see what is happening while the program runs.

What to show in a trace

Never print raw numbers in a trace print. Trace

  printf("%d %d\n", x, y);
is worse than useless. It writes a bunch of numbers. Never print a silly string like
  printf("I am here\n");
That is about like writing "taken last Thursday" on the back of a photograph. (Yes, a person I knew did that.)

  1. The most important rule is to make traces clear, informative and easy to read. The remaining rules offer suggestions for how to do that, but this rule overrides them where appropriate.

  2. Say who is talking. There will typically be many trace prints in your software. Each one should say where it is by giving the name of the function in which it occurs. A typical way to do that in function insert is

        printf("insert: ");
    
    showing the name of the function as if you are writing a play and this is a line for insert to speak.

  3. Say where this trace occurs in the function. Is it at the beginning? The end? At the top of a loop body?

  4. Provide relevant information. If you are showing the size of something, write "size = ...", where ... tells the size. Be sure to include important information. You will want to see it for diagnosing errors.

  5. Do not run consecutive traces all onto one line. Normally, print a newline (\n) at the end of each trace. Put spaces between different pieces of information.

Leaving traces in the program

(This looks ahead to if-statements.)

After you have finished debugging a function, do not get rid of trace prints in that function. Leave them in place. Do not comment-out the traces. Surround each one by a test such as

  if(traceLevel > 0)
  {
    ...
  }
where traceLevel is a global variable (declared outside of functions). Global variables are shared by all functions that occur after their declaration. Normally, the standards for this course forbid the use any global variables, but a variable to control tracing is an exception. If traceLevel is 0, traces are skipped. You turn traces on by setting traceLevel to a positive integer. For example,
  if(traceLevel > 1)
  {
    ...
  }
says only to do this trace when the trace level 2 or higher.

You can also have more than one variable to control tracing. You typically have a variable for each module or collection of related modules, so that you can trace only what is happening in those modules.

Experts plan for mistakes. They write traces in the software right from the beginning, and do not take the traces out ever.

Output buffering

It is inefficient for a computer to write a single character into a file. Instead, it is much better to accumulate a bunch of characters (a few thousand) and write them all at once.

Buffering

Taking that into account, printf stores characters that it prints into an array, called an output buffer, until something causes printf to flush the buffer by writing it out.

What is the significance of buttering to tracing?

If a trace is being written to a terminal window, and a trace is written that does not end on a newline character, that trace will not show up until later when a newline character is written. If something causes your program to stop before a newline is written, you can be misled into thinking that the trace was not reached.

If the trace is written into a file, a lot of traced material can be delayed, and you might not see an accurate picture of what is going on in your program.

Forcing a flush

In C++, you can force the standard output buffer to be flushed by performing statement

  fflush(stdout);

Adding a compile-time switch (Optional)

Sometimes you want to leave trace prints in a a piece of software, but also want to be able to avoid the overhead of constantly checking if tracing is turned on in a version that is shipped to customers. In a C++ program it is easy to get rid of trace prints automatically using the preprocessor. First, either do or do not define a preprocessor symbol DEBUG. To define DEBUG, write

  #define DEBUG
Then
  #ifdef DEBUG
    if(tracing > 0)
    {
      ...
    }
  #endif
allows you to avoid compiling the entire debug print, including the test of whether tracing > 0. Everything between #ifdef DEBUG and #endif will be omitted from the program.

Avoiding clutter (Optional)

An unfortunate drawback of trace prints is that they clutter function definitions, making the definitions more difficult to read. One way to avoid that is to move the trace prints out of the definition. Here is an example.

  #ifdef DEBUG
  #  define TRACE_INSERT1\
       if(tracing > 0)\
       {\
         printf("insert [top]: k = %i\n", k);\
       }
  #else
  #  define TRACE_INSERT1 {}
  #endif

  void insert(int k)
  {
    TRACE_INSERT1
    ... (body of insert)
  }

(A preprocessor definition can only be one line long. But a backslash at the end of a line causes the end-of-line to be ignored. That is why there are backslashes ending lines in the definiton of TRACE_INSERT1.)

Now each trace print is replaced by one short line inside the function body, so it has minimal impact on the readability of the body.

Exercises

  1. What is tracing? Answer

  2. What is the purpose of tracing? Answer

  3. What happens if you forget to print a newline character in a trace? Answer

  4. What should a trace print show? Is it reasonable for it to write "I am here"? Answer

  5. How does output buffering affect traces? Answer