The prerelease version of Netscape's JavaScript Debugger component for Netscape Communicator has been one of the most welcome hunks o' code to reach my desktop in a long while. In his forthcoming article on the JavaScript Debugger, Angus Davis presents a good overview of how to use the debugger in your client-side development. Still, there are many occasions when I fall back on some of my older debugging techniques to give me a different perspective on what's happening inside my code.
In this article, I'll provide you with a simple but powerful external .js library file that preserves a trace of designated values while a script runs. You specify which values are to be monitored and where in the code this happens. As an added bonus, this library file also measures time intervals between tracking statements -- all the better to help you identify where performance bottlenecks are slowing down your program. If you're still authoring in Navigator 3, you can use this debugging method in lieu of the Communicator-level debugger. Some of the techniques here can also apply to server-side JavaScript.
BUG PATROL
I've long held that 90 percent of the bugs in JavaScript code are related to unexpected expression evaluation and faulty object references. At times you think that an expression has one type of data in it, when, in truth, something entirely different is there (a benefit and penalty for using a loosely typed language); object references can also go awry if you accidentally duplicate object names or overlook the nesting of objects in their containers (for example, a text box inside a form inside a document).
Single-stepping through running code is an extremely valuable debugging aid when you know where the problem is. But often your application has grown so large that you may be able to debug it more efficiently by seeing a trace of execution with intermediate values along the way. For example, you'd like answers to these kinds of questions:
DEBUG.JS
Example 1 is the listing for the debug.js library file. It consists of one global variable and one function.
var timestamp = 0 function debug(flag, label, value) { if (flag) { var funcName = debug.caller.toString() funcName = funcName.substring(10, funcName.indexOf(")") + 1) var msg = "In " + funcName + ": " + label + "=" + value var now = new Date() var elapsed = now - timestamp if (elapsed < 10000) { msg += " (" + elapsed + " msec)" } timestamp = now java.lang.System.out.println(msg) } }
When this file loads into a document, the timestamp variable becomes a global variable in the document. This variable is used to store the last time the debug() function is called. The value must persist so that subsequent calls to debug() permit calculations of the time differences between the previous and current invocations.
The debug() function takes three parameters. The first, flag, is a Boolean value. Later in this article, you'll see how flag comes into play with the calls to this function. The second parameter is a string that identifies, in plain language, the value being traced. And the value itself is passed as the third parameter. Virtually any type of value or expression can be passed as the third parameter -- which is precisely what you want in a debugging aid.
Only if the flag parameter is true does the bulk of the debug() function execute. The first task is to extract the name of the function from which debug() was called. By retrieving the rarely used caller property, debug() grabs a string copy of the entire calling function. A quick extraction of a substring from the first line yields the name of the function. The second task is to start building the message text that records this particular trace. The message identifies the calling function followed by a colon; after that comes the label text passed as the second parameter plus an equal sign and the value parameter. Here is the format of the output message:
In funcName: label=valueBy itself, the message contains a fair amount of information. But I take it one step further by calculating the current time from the system clock. By subtracting the value of the timestamp variable from the current time, I get an elapsed time. If that elapsed time is less than ten seconds, I append the elapsed time (in milliseconds) in parentheses to the message. Then I update the timestamp global variable.
The final statement is a LiveConnect call to a native Java class method. Experienced Java programmers will recognize the System.out.println() method as the one that writes a value to the Java Console window. If you haven't done any Java programming, you may not even know that Navigator has a Java Console window available in the Options (or Communicator) menu. Java applet errors are automatically written to this window, even if the window is hidden. Similarly, you can use the LiveConnect direct call to a built-in Java method, java.lang.System.out.println(), to write anything you like to the window. Anything written to that window is appended to the end of whatever text is already inside. Therefore, you can write as many debug messages as you like to that window, and they'll all be there for you to see after the script runs.
LOADING DEBUG( )
To include this debugging library in your document, add the following <SCRIPT> tag set at the beginning of the document's head section:
<SCRIPT LANGUAGE="JavaScript" SRC="debug.js"></SCRIPT>The syntax assumes that you have saved debug.js in the same directory as your HTML document. Also, if you're running the page from a server (instead of a local hard disk), the server must be configured to transmit files with the .js extension as the MIME type "application/x-javascript". Netscape servers are set up to do this by default. If you run the page from a local hard disk, no other preparation or configuration is needed.
SETTING UP YOUR DOCUMENT FOR DEBUG( )
As you build your document and its scripts, you need to decide how granular you'd like debugging to be: global or function-by-function. This decision affects the level at which you place the Boolean "switch" that turns debugging on and off.
You can place one such switch as the first statement in the first script of the page. For example, specify a clearly named variable and assign either false or 0 to it so that its initial setting is "off":
var DEBUG = 0To turn debugging on at a later time, simply change the value assigned to DEBUG from 0 to 1:
var DEBUG = 1Be sure to reload the page each time you edit this global value. Also, while you can use any variable name you like, avoid the reserved keyword debugger (all lowercase). I chose all uppercase DEBUG to help me locate and delete relevant statements once the page is ready to deploy.
Alternatively, you can define a local DEBUG variable in each function for which you intend to employ debugging. One advantage of using function-specific debugging is that the list of items to appear in the Java Console window will be limited to those of immediate interest to you, rather than all debugging calls throughout the document. You can turn each function's debugging facility on and off by editing the values assigned to the local DEBUG variables.
CALLING DEBUG( )
All that's left now is to insert the one-line calls to debug() according to the following syntax:
debug(DEBUG,"label",value)By passing the current value of DEBUG as a parameter, you let the library function handle the decision to accumulate and print the trace. The impact on your running code is kept to a one-line statement that is easy to remember. To demonstrate, Example 2 consists of a pair of related functions that convert a time in milliseconds to the string format "hh:mm". To help me verify that values are being massaged correctly, I insert a few calls to debug().
function timeToString(input) { var DEBUG = 1 debug(DEBUG,"input",input) var rawTime = new Date(eval(input)) debug(DEBUG,"rawTime",rawTime) var hrs = twoDigitString(rawTime.getHours()) var mins = twoDigitString(rawTime.getMinutes()) debug(DEBUG,"result", hrs + ":" + mins) return hrs + ":" + mins }
function twoDigitString(val) { var DEBUG = 1 debug(DEBUG,"val",val) return (val < 10) ? "0" + val : "" + val }
After running the script, I display the Java Console window in Navigator to see the following trace:
In timeToString(input): input=869854500000 In timeToString(input): rawTime=Fri Jul 25 11:15:00 Pacific Daylight Time 1997 (60 msec) In twoDigitString(val): val=11 (0 msec) In twoDigitString(val): val=15 (0 msec) In timeToString(input): result=11:15 (220 msec)Now you try it -- run the script from Example 2. Show the Java Console window by choosing Java Console in the Options menu (Navigator 3) or Communicator menu (Communicator).
Having the name of the function in the trace is helpful in cases in which you might justifiably reuse variable names (for example, i loop counters). The function names also help you see more clearly when one function in your script calls another.
ABOUT THE TIMER
In the trace results of Example 2, you might wonder how some statements appear to execute literally "in no time." You have to take the timing values of this debug scheme with a grain of salt. For one thing, these are not intended to be critical benchmarks. Some of the processing in the debug() function itself occupies CPU cycles. And with rapid-fire execution in the small scripts of Example 2, the timings are not very meaningful. But in a more complex script -- especially one involving numerous calls to subroutines and nested loops -- you can place the debug() function calls in statements that aren't deeply nested, so that the intervals won't be too small to be of value. In any case, regard the values as relative, rather than absolute, values. And always run the script several times to help you see a pattern of performance.
VIEWING IN REAL TIME
I faced another debugging challenge recently while using a computer with a small monitor that prevented me from viewing both the Java Console and browser window at the same time. I wasn't convinced that a nested for loop construction was iterating enough times through a pair of related lengthy arrays. To help me visualize how the loops were working as they ran, I modified the debug() function to write the results not to the Java Console, but to the status bar at the bottom of the browser window.
To do this, I replaced the line in debug() that calls the System.out.println() function with a statement that sets the window.status property:
window.status = msgFor this monitoring, the timing wasn't crucial, so I commented out the lines in debug() that added the elapsed time to the message. I placed the call to debug() in the innermost loop, showing both of the loop-counting variables:
debug(DEBUG,"i,j", i + "," + j)When I ran the script, the status bar whizzed through the values of the for loop counters i and j:
i,j=0,0
i,j=0,1
i,j=0,2
...
i,j=1,0
i,j=1,1
(etc.)
In the status bar, I could examine the rhythm of updating the values to see where some parts of the loop were slower than others based on the array data being manipulated. (Notice, incidentally, how the debug() function evaluated the third parameter to let me join multiple values in a single debugging message.) And when the script stopped, the status bar showed the final values, so I could see how far the loops got.
SERVER-SIDE APPLICATION
After seeing how helpful this debugging system was on the client side, I looked to see if it could be useful on the server side. Although the Netscape Enterprise Server application manager provides a convenient trace function that's very good at detecting and revealing syntax errors, it does not provide a way to view intermediate values of expressions in your scripts. But you can modify the debug.js file to operate as a server-side library file (compiled into the application).
Instead of writing trace results to the Java Console, use the server-side write() method to output the accumulated trace message to the current document. You can use the same DEBUG flags either globally or within each function (the latter greatly recommended in a complex server application). Of course, the finely crafted display of the HTML output for the screen will be littered with your debug messages whenever you have the DEBUG switches turned on. But that's OK, because you should clear up all bugs and turn off (or remove) the DEBUG flags before deploying the application.
STOMP 'EM YOUR
WAY
By standardizing a way of generating debugging
reports, you can greatly simplify the task of debugging your code. For
every HTML document you have under construction, include debug.js as
a library file. As you write each new function definition, automatically
include a debugging switch (var DEBUG = 0) as the first statement.
Then, when you're ready to capture values in trial runs of your script,
insert the three-parameter calls to debug() wherever they'll reveal
intermediate and resulting values to show you what's going on inside your
script while you blink. Over time you may also wish to modify the way the
output is formatted or the kinds of parameters you send to the debug()
function. Be aggressive, and don't let creepy crawly critters get
in your way.