Boost Efficiency with a Performance-tuned Debugging Strategy

Boost Efficiency with a Performance-tuned Debugging Strategy

fficient coding practices are hard to develop and even harder to maintain. Most likely, your coding process suffers from one of two problems: Your code throws an exception or otherwise breaks and you don’t know where the error occurred, or you have tried to write debugging code, but its inefficiency has affected the performance of your application (perhaps without you even being aware of it).

This month’s article will take a slightly different approach from what you might be accustomed to reading. Rather than simply explain how to write a particular piece of code, or use a design technique, I will present a simple case study quantifying the increased efficiency that my debugging techniques yield.

The subject matter covered in this article is aimed at mid?senior level java developers who have tried to develop a coherent debugging strategy or are interested in fine-tuning their existing one.

The problem is two-fold. First, every application can benefit from a well-designed debugging strategy, but few applications have one. And second, most debugging strategies contain wasteful overhead that degrades application performance.

Design a consistent, flexible debugging strategy that is scalable and maximizes efficiency.

Common Debugging Techniques
Virtually every serious java application is going to have at least one, and probably multiple try/catch blocks (if for no other reason than that the compiler forces you to catch certain potential behaviors). Although exception handling can be a useful way to deal with runtime errors, it is generally not sophisticated enough to identify what went wrong in the code to produce the erroneous data that threw the exception. How many times have you received the infamous “NullPointerException” and gone to the portion of the code in question and thrown up your hands because there’s no way to tell which exact piece of code threw the exception. Even if you knew exactly where the null reference occurred, how did it come to have a null value? At what point did it cease to have a valid value? Was it earlier in the method, was it null before the reference entered the method, is the value in the database null?

After this frustrating scenario occurred several more times, you no doubt decided to develop a consistent debugging framework to maximize your development time and increase efficiency. Perhaps it was as simple as reporting key variable values and application-level events by sending them to a log file. This is a good solution. When an error occurs or an exception is thrown, often you can trace back through the log to the first time that a reference or value was assigned to the offending variable. You are now able to track down in a few minutes, an error that used to take an hour to find before you began logging system activity.

This technique has successfully increased your efficiency. Unfortunately, your code’s efficiency is very likely suffering.

Cutting Out the Fat
Most debugging techniques are inefficient and degrade code performance. This is fine for development but unacceptable for production-quality code. Consider the following example:

logIt( “Entering while loop” );while ( x < y ){	logIt( "Inside while loop" );	x*=5;	logIt( "x is equal to " + x );} // end while( x < y )logIt( "Exited while loop" );private void logIt( String message ){ //write to log file} //end logIt()

This will produce very useful debugging information. The problem is that if you do this across your entire application, then the bulk of your system processing will become the creation of and garbage collection of String objects (not to mention the I/O processing occurring within the “logIt()” method). This is particularly crucial when we move this code into production!

What’s the solution to this? Many developers come to the conclusion that we simply need to be able to turn debugging off for production. Often, this is done by creating a boolean primitive that is set to “false” for production. Then this member is evaluated in the logging method. For example:

private void logIt( String message ){	if ( getDebug() == true )		// write to log file}

Great! So we’ve managed to eliminate the unnecessary I/O processes, but what about all the calls to this method? The code is still littered with String objects that are created, and garbage collected regardless of whether or not debugging is turned on, not to mention the overhead from method calls to a method that doesn’t do anything. It may seem like I’m making a big deal over nothing, but do not underestimate the effect that object creation and garbage collection have upon the performance of your code.

Putting It to the Test
In the previous section, I made some bold claims about the inefficiency of some common coding practices and debugging techniques. Here is where I support those claims.

I wrote a test application called DebugTester to compare the two debugging techniques. Two identical loops execute a specified number of times. Each loop utilizes a different debugging technique. One loop creates and passes a String object to a logging method?the way that most debugging techniques are implemented, while the other checks a static final boolean variable first, to determine if debugging is turned on. Here is the source code for the class:

public class DebugTester{        public static final boolean DEBUG = false;

public static void main( String[] args ) { long begin, end; int max = 500000;

if ( args.length == 1 ) int max = Integer.parseInt( args[0] );

begin = System.currentTimeMillis(); for ( int i = 0; i < max; i++ ) logIt( "The current value of i is " + i ); end = System.currentTimeMillis(); System.out.println( "The first loop executed in " +
(end – begin) );

begin = System.currentTimeMillis(); for ( int i = 0; i < max; i++ ) { if ( DEBUG ) logIt( "The code won't get to here." ); } end = System.currentTimeMillis(); System.out.println( "The second loop executed in " +
(end – begin) );

} //end main()

public static void logIt( String msg ) { if ( DEBUG ) System.out.println( msg ); } //end logIt()

} //end DebugTester

The calls to “System.currentTimeMillis()” return the current system time in milliseconds. By checking this before and after each loop and comparing the difference between the values, the execution time of each loop can be determined.

The following table shows the results of executing the program with increasing numbers of loop iterations:

Clearly, these numbers are going to vary, depending upon the speed of your system’s processor, what compiler you are using, and how your JVM happens to prioritize the processes involved. The last factor is especially important. The processes that the JVM goes through to determine processor priority will produce slightly different results on the same machine, testing the same input multiple times.

The important thing to notice is that it took 4 million iterations through the loop before the optimized technique even registered as taking any time at all! How can this be possible? The key is the final static boolean variable DEBUG. A good compiler will recognize this as a constant and will compile the code to read:

if ( false )	logIt( “The code won’t get to here” );

So when debugging is turned off, any lines of debugging that appear in your code effectively disappear!

Flexibility and Scalability
The debugging technique demonstrated in the previous section, while very efficient, is neither flexible or scalable. First, we’ll deal with the issue of increasing flexibility. Consider the following scenario. Your application needs to maintain a log of crucial system activity that exacts a minimum performance penalty. During the development phase, this log should report every database query and the exact value of the results returned. During the production phase, only critical exceptions should be logged and there will also be times when the values returned from the database should be logged.

How can a system be developed to meet these demands? The solution is to create a Debug class containing one or more logging methods, and a static final integer instead of a boolean primitive. Rather than simply declaring that debugging is turned on or off, the integer would indicate what the debugging level of the system is currently at. For example, 6 levels could be created: 1?Critical Failures, 2?Serious Errors, 3?Standard Exceptions, 4?Database Values, 5?Key Variable Values, 6?Interesting System Events. Then you could have your debugging statements evaluate the current debugging level and declare how serious the information is that they are reporting. Consider the following code sample:

if ( Debug.dbgLevel <= 3 ) Debug.logIt( "blah blah blah" );

With this one line of code, we have indicated that the dbgLevel variable (of type int) must be compared to the integer ‘3’. If the current debug level is equal to or lower than 3, then the following line is not executed. Essentially, this debugging line is a debug level 3. If the debugging level is set to three or greater, then this line executes, otherwise, it doesn’t. We now have a flexible solution, but what about scalability?

To create a scalable debugging framework, we can build upon the flexible framework outlined above. One way to do this would be to pass the debug level into the “logIt()” method. This would allow the Debug class to take that value and process the other String parameter in a particular way, depending upon the nature of the debugging info. Critical errors could spawn a separate thread to send an e-mail to the System Administrator informing her of the error, serious errors and standard exceptions could be recorded in an error database as well as a flat file, database query values could be logged to a special file, and all other information could be logged to a common file. Any or all of these could be changed to meet the changing needs of the system, and the system as a whole could scale to meet the demands of the growing enterprise.

As we advance in the software development industry, the code we produce is expected to be clean, efficient, and reliable. Perhaps even more than that, we are expected to be able to rapidly diagnose and solve any problems in our code. A key component in meeting these expectations is to have a consistent, well-designed debugging strategy. We have outlined some of the common performance pitfalls in typical debugging techniques, examined some more efficient approaches, and seen how our debugging framework can be made more flexible and able to scale to meet the changing technical and business needs.

Sample Applications
There are two sample applications for you to get a feel for how to apply the debugging techniques outlined in this article: demonstrates a minimal debugging framework that should be sufficient for any project that needs debugging info during development but none during production. demonstrates a more robust debugging framework that utilizes the debugging level technique, allowing for increased flexibility in how much information is recorded and how the system responds to certain circumstances.


About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist