Omniscient Debugger User Manual

Omniscient debugging is the concept of debugging by retaining state changes and "navigating" backwards in time to find the problem. The ODB is a proof-of-concept implementation written in Java. It is neither optimized nor integrated. None-the-less, I believe you will find it to be the most effective debugger you have ever used.

Figure 1

The ODB is based on the idea of collecting "time stamps" at each point of interest in a program and then allowing the programmer to use those time stamps to explore the history of that program run. "Interesting" points include: setting a variable value (local, instance, array element, static), making a method call, throwing/catching an exception, and creating a new object. The debugger inserts code into the class files to collect these stamps. (The instrumentation is done using the wonderful BCEL package which is available from When the program runs, time stamps will be recorded.

The ODB does not require any preprocessing, or any special repository. There are no limitations on the program being debugged. It is not even absolutely necessary to have the source code available. After installing (see below), the programmer runs the debugger by calling:

% debug TargetProgram arg1 arg2 arg3

The debugger will display a little control window and start the program. The main debugger window will pop up when the program calls exit() or when the programmer pushes the "Stop Recording" button in the control window. The programmer will then be able to navigate through the time stamps to find interesting events.

Once the debugger main window is up, all recording will be turned off, even if the program is still running. You may resume recording using the control window.

Data Display Formats

The ODB needs a print string, so it supplies its own format showing the class name and an index number: <MyObject_75>, chopping off the package. You may add an additional string to it: <MyObject_75 square> (see defaults file).

Class objects are displayed as just the class name (e.g., Person ). Boolean values, numbers, and characters are displayed as expected: true, false, 123, 45.678, 'X' (88). Bytes are also displayed like characters with both the ASCII character and the decimal value in parenthesis.

Long names and strings (more than 20 characters) will be elided and printed as: <TestLongName..0> in the trace pane. In the objects pane more room is provided (500 characters). Very long strings and large objects may be printed out to the terminal via the menu item Objects->Print.

Method traces are displayed with the object (or class object for static methods), followed by the method name, the first three arguments, and finally an arrow and the return value. Methods that haven't returned just show ****. Methods that throw (or propagate) an exception show ****<Exception>. They are indented appropriately, hence in figure 1, sort() called average() once, new() twice, then start(), and finally sort() recursively. The recursive call to sort() called average(), etc.

Returns from methods are displayed on separate lines as sort -> void. If the return line would be right after the call line, it will not be displayed. If the program catches an exception from more than one method call deep, the display will appear jagged because there won't be any intervening "return lines".

Constructors will be shown like static methods ( -> <MyObject_1>), as will super methods.

The Display Panes

The ODB attempts to show all the data of interest on the screen at the same time. Popup dialogues are only used for finding source files and selecting values, which are rarely done. The panes are all updated to display the same time stamp.
Threads Pane
Threads are displayed in the format <Thread-3>, where "Thread-3" is whatever is returned by getName(). If the currently selected time is previous to the creation of the thread (before the thread's first time stamp), it is shown as -- <Thread-3> --, and as <Thread-3> Dead if after the thread has exited (after the last time stamp for a thread whose isAlive() method returns false). If the thread is blocked, waiting for a lock or in a call to wait() or join(), it is shown as <Thread-10> <MyObj_2>. Adding the thread to the objects pane will show this as a pseudo-instance variable _blockedOn.
Stack Pane
All methods on the stack are displayed here. Single-clicking on a line will update the Code pane, the arguments, and the local variables for that stack pane. The current time stamp will not be changed. Pushing any button will act on the actual time, not what is displayed in the code pane. (It's not clear what is most "intuitive" here.) The menu command Code->Goto Stack Frame will revert to that time stamp.
Locals Pane
All arguments and local variables will be displayed here. If the program has been compiled without the -g flag, local variable names will displayed as var1, etc.
'this' Pane
This pane displays the current 'this' object (or the class object for static methods).
Trace Pane
For each thread, this shows the trace of all method calls that were made. Only three arguments are shown. Only ten are recorded.
Code Pane
This displays the current line of code. The buttons all move forwards or backwards starting from this line. The two exceptions are: when a higher stack frame has been selected (see Stack Pane) and when a time stamp occurs in a source file that can't be found, in which case the code pane will be empty.
I/O Pane
This shows the all the lines written by System.out.println() and System.err.println(). (Obviously, only calls from instrumented code.) If the line is preceded by "--", that indicates that the current time is previous to the time that line was written. (Eventually this pane will show calls to print(), write(), etc. going to any I/O stream.)
Objects Pane
This shows whatever objects the user has copied here via double-clicking on objects in other windows. Double-clicking on an instance variable will add that object to the pane. Double-clicking on an object in this pane will "close up" that object, hiding all its instance variables. Double-clicking a second time will open it back up. Individual instance variables may also be removed selectively (see Objects->Remove and Objects->Retain Only).

Double-clicking on an instance variable will expand the value of that instance variable in-place (like the array int[20]_0 in figure 1). If the value is a primitive or is an object with no visible instance variables, nothing will happen. Pushing the Previous Value button, etc. will advance to the next value of the instance variable, not the next value of the instance variable's instance variable. For example, the previous value of array in figure 1 is null, so the array will disappear if you go there. If you select an instance variable's instance variable, the commands will work on that, which could cause it to disappear if the instance variable also changed value. (This recursive stuff is a little confusing to explain. Just try it.)

Values which have not yet been set (e.g., instance variables of objects before they have been created) will be displayed as "--". Variables which changed values from the previously selected time stamp will be displayed with a leading "*".

Currently only a single level of recursion is supported. Double-clicking on an instance variable's instance variable will copy that instance variable to the top level of the pane. An instance variable's value may also be copied to the top level via Objects->Add Instance Variable.

The Objects menu allows you to display class object of the selected object (and the static variables thereof) Objects->Add Class.


Navigation has never been a issue in traditional debuggers because there wasn't much to think about. You pushed Continue, the program resumed and ran until it hit the next breakpoint, and that was it. We need to be a bit more formal because there are so many more ways to get from here to there and back again.

The ODB provides a variety of methods for navigating through the program run. They are all aimed at making it as simple as possible for the programmer to get to "interesting" points in the program and see what state it was in. The ODB implements what I believe are the most "obvious" methods, with plenty of room for adding others.

The primary modes of navigation are:

o Selecting a line in the trace window, which will revert the debugger to the time of the first time stamp recorded in that method. If that method was not instrumented, then the debugger will revert to the line it was called on. You may request the debugger to always to the calling line by turning Trace->Go to first line in method off.

o Selecting a line in the code window, which will revert the debugger to a time stamp on that line (see below).

o Pushing one of the buttons, which will revert the debugger to a time stamp depending on the currently selected object or line in that pane. (See Navigation Buttons below.)

o Selecting a line in the Stack pane, which will not change the current time stamp, but will show you where methods in the stack were called from (reverting the code pane) and the values of the local variables. (See Stack Pane.)

o Selecting a thread, which will revert to the nearest time stamp in that thread, earlier than the current time preferred. (All instrumentation methods are synchronized, so time stamps are strictly sequential.)

o Selecting a line in the I/O window, which will revert to the time when the newly selected line was printed.

The choice of which time stamp on a line in the code pane to select may be controlled by a set of options in the Code menu. The normal direction is to go backwards in time if the newly selected line is above the previously selected line, and forwards if not. Going forwards, you will select the first TS (of a contiguous set, e.g., call/return) on a line, going backwards, you will select the last. Normally the time stamp has to be in the current thread. You may also restrict the choice to being in the same method call. (The best behavior here is hard to determine. I've played with lots of ideas.)

Navigation Buttons

The buttons work as uniformly as possible for the different panes. The four arrow buttons revert to the first, previous, next, and last time for selected object in that pane. All of the buttons have tooltip messages.

You may bring up a list of all the values a variable ever had and select one of them. The commands Object->Select IV Value selects a value for an instance variable, and Object->Select Local Value selects a value from a local variable. The ODB will revert to the time when that variable first took that value.

Now for the Details

If you are just starting, skip down to "Installing the Debugger" and spend some time playing with the demo.


Modeled after EMACS, extended commands (such as Evaluate Expression and Search) are displayed here, as are messages to the programmer.
You may do an incremental text search through the trace pane. <Ctrl-S> will start the search and give focus to the minibuffer. Typing characters will extend the search string. Another <Ctrl-S> will advance to the next match. <Ctrl-R> will reverse the search. <Return> will end the search. <Ctrl-G> will abort any command.

The search is case-insensitive and looks for two things: the exact characters on the screen (including the ".." for very long names), and the characters in the individual objects (i.e., the complete name for long names and strings). TABs are typed as themselves (not <Ctrl-I>, not "\t"), newline characters are entered as "\n". In the middle of a search, double-clicking on an object will add that object to the search string.

"Event" searches are provide via the "Fget" commands (<Ctrl-F>). They are executed as incremental searches based on the pattern typed in (another <Ctrl-F> will find the next match and <Ctrl-R> will search in the reverse direction). A typical pattern would be (note the "Prolog style"):
port = call & to = THIS & arg0 = <MyObj_5>
which will search for method calls ("port = call") whose "this" object ("to = THIS") will be assigned to the variable THIS (NB: a sybmol in uppercase means it's a variable) and whose first argument ("arg0") is <MyObj_5>.

Port must be one of: call, return, enter (first line in a method), exit (last line in a method), civ (change instance variable), clv (change local variable), chgArray (change array element).
o All ports define "to" (this object), "toc" (this object class), "mn" (method name), "thr" (thread), and "pN" (parameter 0-9).
o "Calls" also define "argN" (argument 0-9) and "cmn" (call method name).
o "Returns" define "rv" (return value).
o "Changes" define "nv" (new value).

Strings are entered with '"' and class objects are prefixed with a '#'. All objects may be compared with '=' and '!='. Integer values may be compared with '>' and '<'. Hence this pattern:
port = enter & mn = "sort" & toc = #fr.insa.Thing & p0 > 3
will match the first line in any method named "sort" in the class fr.insa.Thing whose first parameter is an int greater than 3.

There are two commands on the Trace menu which will create queries for you, matching either the current trace line (i.e., the current method call) or the current source line. These will be stored as the "previous" FGET query so that typing <Ctrl-F><Ctrl-F> will show them.

Typing <Ctrl-T> in the midst of an FGET query will display the total number of matches in the entire run of the program (starting with the currently selected time).

Marking Points of Interest

The ODB maintains a ring of "marks" for time stamps. You may add a mark to the ring with <Ctrl-SPACE>. You may then revert to that mark with <Ctrl-X>. The ring is circular, and if there is more than one mark, <Ctrl-X> will cycle through the ring.

Locks and Wait Sets

When a thread blocks, waiting for a lock or calls wait() , the object on which it blocks will be displayed next to it in the Threads Pane (e.g., <Thread-7> is blocked on the class object DemoWait below). A pseudo-instance variable _blockedOn will be added to the thread and shown in the object pane if the thread is displayed there. Three pseudo-instance variable ( _waiters, _owner, _sleepers) will be added to the object. If a thread blocks in uninstrumented code, the ODB will not notice.


If you have a large number of trace lines which are not particularly interesting, you may filter them out using the Filter menu options. You may filter out individual methods (hiding them and their internals), filter out method calls deeper than some limit, or filter in one method (hiding everything else). And you may "unfilter" everything. You may save the filters to the .debuggerDefaults file (see options) and the next time you instrument those classes, those methods will neither be instrumented nor recorded.

Probably the most useful filter option is to "filter in" a method, hiding all method calls outside of the selected one.

All panes reflect the current time stamp (except when navigating using the stack). The code lines are those defined by the compiler, so there may be many time stamps on one line of code. Each method call generates two time stamps -- one for the call, one for the return. If it's instrumented, the method then also records the first line executed and the last line. If return is not called directly, the Sun compiler assigns the implicit return instruction to the first line of the method (kinda weird). This line:

t = first(tl.getNext());

generates five time stamps, two calls, two returns, and an assignment. Logic, flow of control, and operators generate no time stamps. This:

if ( ((a + 71) > (b * c)) || (this == that) ) break;

generates no time stamps.

Evaluating Expressions Interactively

Arbitrary method calls may be made at any time. You may revert the debugger to any time you wish and then evaluate a method call using the current values of the recorded objects. So if you revert to time 1234, when <Demo_0>.array elements 16 and 17 are 454 and 123 respectively, calling <Demo_0>.quick(16, 17) will sort those two. If you advance to time 1330, when 16 and 17 are already 123 and 454, then sorting them won't change anything.

In order to maintain some sense of consistancy, the methods will be executed in a different "timeline". Basically, the current state will be copied into a new timeline which is initialized with only one time stamp. The method will then be run (with recording turned on and off automatically), populating this new timeline. You can switch back and forth between the two timelines freely. Further method calls will clear the secondary timeline before running.

In the secondary timeline you may change the value of instance variables before running. The commands are all on the main menus, with keyboard accelerators. <Tab> will do an auto-complete on object and method names. <Return> will execute the command (running the method or setting the instance variable value).

These commands are the least robust part of the debugger. The interactive "Evaluate Expression" only accepts four arguments. The arguments must be object or class print strings (e.g., Demo, <Demo_0>), strings, integers, true, false, or null. You cannot change the value of final variables. One space is required after each "," and nowhere else. <Return> and <Tab> must be typed at the end of the minibuffer. You can only change the values of variables of type Object, int, and boolean. Lock states and wait sets cannot be set, so their displays will be set to "unlocked" and "empty".

Garbage Collection

There is a limit to the number of time stamps that can be recorded. Each simple time stamp takes three words (12 bytes on a 32-bit system) and each method call/return takes about 25 words and another 25 for the JList to display it. And then your program probably needs to use some memory also.

The ODB looks at the value of the MEMORY environment variable (or MaxTimeStamps in .debuggerDefaults) to determine how much memory to devote to time stamps. Currently it allows a maximum of MEMORY/200 time stamps. (You'll notice that the aliases default to 400MB, allowing 2m time stamps.) When that number is exceeded, the ODB's "garbage collector" kicks in and throws away some of the early time stamps. The defaults are set on the command line by the aliases such as this one (for debug):

java -Xms400100100 -Xmx400100100 -DMEMORY=400100100 -cp $HOME/Debugger/debugger.jar:$CLASSPATH $USER_FLAGS com.lambda.Debugger.Debugger

The flag -Xmx sets the maximum memory for the program, -Xms sets the initial memory size. We want to avoid useless early JVM garbage collections, so they are set to the same size. Depending on your program, you may wish to increase these numbers while keeping the value of MEMORY low. This would give your program more room of its own.

Of course these are not really garbage. So the ODB attempts to retain the time stamps which are most likely to be interesting (object changes), while throwing away those that aren't. In the first mode, the ODB throws away all time stamps for local variables and method calls that are older than 50% of the entries. If this is insufficent (less than 10% are collected), then the second mode kicks in and entries for objects are also collected.

On each invocation, the collector will print out a line similar to this:

ODB: Collected 244783 out of 326382 stamps and 47268 TraceLines

Although very clever, it is not clear that the GC is actually useful. If you turn it off, collection will simply stop when storage is filled up. (See the environment variable GC_OFF.)

Objective of the ODB

I originally wrote a paper describing this so that someone else would write a proper debugger/IDE so that I could use it. Then I thought it would be good if I could prove that this was actually useful. So I wrote a little mock-up, which evolved into this full-fledged proof-of-concept. I claim that the ODB is sufficient proof that the idea of "omniscient debugging" works and works very well.

So, there are lots of guesses about what data would be most useful, how it should be displayed, what navigation commands make sense, etc. Most of the surrounding pieces of an IDE have already been done quite well by other folks. So, for compilation, editing, class browsing, etc. I simply assume that the ODB will be properly integrated into those bits which work best. I wrote the ODB in Java because it was easy. Omniscient Debugging could equally well be applied to C, C++, Eifel, etc.

Optimizations and Limitations

Other than any exceptions noted in the "bugs" section below, the ODB shows data which is 100% correct and can be confidently run on any Java 1.3 1.4, or 1.5 program. (ODB1.4 for SDK 1.3 and 1.4, and ODB1.5 for SDK1.5.) Unless there are bugs I don't know of (ha-ha).

If you don't need to get fancy, stop now. You'll probably never use the stuff below.

Although it is possible to record absolutely everything, it is rather expensive and not very useful. For classes which are trusted (such as the JCF classes, library classes, and your own well-tested classes), it is possible not to instrument them. Indeed, the default is to instrument and record only those classes in the package of the main method. The debugged code will run faster and the debugger will not be cluttered with uninteresting data. There are some limitations...

The limitations include: any direct changes to an instance variable (e.g., obj.iv = value;) performed in non-instrumented code will not be recorded (it's bad OOP practice to set instance variables directly anyway! You should be using getters and setters.), calls from non-instrumented code (e.g., from the event loop) back into instrumented code will look funny (the call chain won't be visible. These "unparented" calls are marked in the trace pane with "**" preceding the call.), calls to System.exit() in non-instrumented code will exit the process.

You may clear the history and record new data from a live application (e.g., when debugging a button push), or restart the same. You may select a line in the program and tell the debugger to turn recording on/off there. You may select a line of output for the same purpose.

Objects which were created outside of instrumented code will not be recognized until they are touched in instrumented code. The actual creation time is thus not known and their instance variable values will not be known. They will be marked with a "@" in the Objects Pane.

Some classes are just crying out for special, hand instrumentation. Vectors, for example, really want to be displayed as if they were arrays which just happen to be of the exact right size. This is done by replacing Vector with MyVector during instrumentation.

Obviously, instrumenting classes which the ODB uses (e.g., Swing) will not work. If you are working on such code, you will have to change the package for the code you're working on. For example, to debug the ODB, a complete copy of the source tree is made and the package "com.lambda.Debugger" is replaced by "lambda.Debugger." I do the same thing when debugging Swing itself.

Displaying very large objects is awkward. By default, objects with more than 1000 instance variables (i.e., arrays) will display only the first 1000, and print (Objects>Print) only the first 10000.


The ODB normally inserts instrumentation while loading classes via a subclass of java.lang.ClassLoader. This is done by default for classes in the package of the main method. Thus, if you use Apache's crimson package it won't be instrumented unless you specifically request it (see .debuggerDefaults). It doesn't matter where the files are loaded from. Normally the ODB will bring up the main debugger window as soon as the target program's main thread returns or System.exit() is called from instrumented code. (The exit() method gets replaced by a call to throw a DebuggerExit exception.) Any other threads in the program will be suspended on their next call to instrumented code.

For programs in packages, CLASSPATH must be set correctly (the aliases will pick this up when they are run), so you must start the debugger from the command line. This is also true for code from jar files--you must put the jar file in the CLASSPATH and start the debugger from the command line. (Microsoft details are noted below.)

% setenv CLASSPATH /home/your_directory/your_code.jar
% debug com.your_company.CoolProgram arg1 arg2

All of the options below may be selected from the controller window if the debugger is started with the DONT_START flag (use debugp). Once you figure out what setting you want, it's more convenient to use the appropriate alias.

For programs that spawn new threads which continue to run after main() returns (e.g., any windowing program), you should set the DONT_SHOW flag (run the alias debugn ). Recording will continue until you push the Stop Recording button.

You may set the PAUSED flag to start the program without starting recording. For example, when there's a bug when pushing a button, you can setup the windows without recording.

You may set both flags from the command line or the controller window, allowing you to start, stop, and restart recording. You may see some odd looking traces if the program is not quiescent when you start and stop it.

You may request that certain methods not be instrumented or not be recorded, by placing their class and method names into the .debuggerDefaults file in the current directory.

For example, StringBuffer and StringWriter aren't be recorded by default, thus ridding the trace pane of lines from code like this:
"Teach "+ n +"dogs new tricks!"
(which actually involves a new StringBuffer, two appends, and a valueOf() call). The methods not instrumented by default are: toString() and valueOf(). Those not recorded are: toString(), valueOf(), and <cinit> (Class constructors -- "initialize statics") and all StringBuffer methods.

If you really want to record/instrument everything (hint: you don't!), you may remove them from the defaults file. You may use the Filter menu to add methods to it or edit it by hand (see Defaults File below).

You may instrument specific classes in different directories by hand:

% debugify MyObject1.class MyObject2.class ...

If you want to run the debugger and have it not instrument any files, you may pass the DONT_INSTRUMENT flag. (Presumably you have debugified some files by hand.) This may be done with the alias/bat file:

% debugdi MyProgram arg0 arg1...

If you start the debugger with no arguments or from a file manager, a file chooser will pop up and allow you to select a program and set these options. This method is rather limited. The program in question must be in the default package.
Microsoft Windows
Everything works the same way as above, except that instead of aliases, batch files are used. It is assumed that you will put the debugger in the directory c:\Debugger and that you will unpack the batch files instead of the aliases. Include them in your path and you'll be ready to go! If you choose a different location, edit the batch files appropriately.

% jar xf debugger.jar Microsoft

Defaults and the .debuggerDefaults File

Most of the flags and parameters which the ODB uses are defined in a single file, .debuggerDefaults. This file is located in the current working directory. If no such file is found when the ODB is started, it will create one. After that, the ODB will not change the file unless you specifically request it to. You may edit the file by hand and the ODB will retain your changes. (Thus you may run the debugger once and then edit the file it writes out.)

You may declare a class which you write to be the "special" format for displaying objects of some type (typically classes, but primitives are allowable). Look at the example in the source for These would be listed as:
SpecialFormatter:      com.lambda.Debugger.SpecialTimeStampFormatter
and would be so displayed only in the Objects window when you select the object (or an array of such objects) and execute Objects>Special Format.

Here is a sample. All of the lines may have duplicate entires (e.g., the DontInstrumentMethod lines below), except for the start/stop lines. It is unlikely you will change anything other than "*Instrument/Record*" and "UserSelectedField" by hand.
# ODB Defaults -- You may edit by hand. See Manual for details
MaxTimeStamps:			400000
OnlyInstrument:			"org.apache"
OnlyInstrument:			""
DontInstrument:			""
DontInstrumentMethod:		"*	 toString"
DontInstrumentMethod:		"*	 valueOf"
DontRecordMethod:		"*	 toString"
DontRecordMethod:		"*	 valueOf"
DontRecordMethod:		"java.lang.StringBuffer	 *"
DontRecordMethod:		"java.lang.Object	 new"
UserSelectedField:		"com.lambda.test.MyClass name"
You may request some objects to include a field value in their print string by setting the user selected field as above. The field choosen must be a primitive and must not change.

The ODB will write out a "DidntInstrument/OnlyInstrument" line for every single class loaded. This makes it easier to change the set of classes being instrumented. By default, only the classes in the package of the "main" method are instrumented.

Finding the Source Code

The ODB will look for source code in the same directory it loaded the class files from. If there is no java file there, the ODB will then look in the current directory. If it doesn't find anything there, it will popup a file chooser so you can tell it where the source is. Any directories you add will be written into the defaults file in the current directory and loaded on subsequent runs.

Known Bugs

The "Heisenbug Principal" of software (observing a program changes its behavior) will always apply, but there shouldn't be any particular differences from running under a traditional debugger. The one exception is when a program suffers from cache incoherency. If you fail to properly lock a shared variable, it is possible for CPU 1 to write a value to that variable and for CPU 2 not to see that new value for some time. The ODB will prevent this bug from occuring. There are static lock analyzers. Use one. The substitute collection classes (MyArrayList, Vector, Hashtable) don't record changing entries via enumerators. The clock displayed on the menu bar is of very doubious value.

Java puts a "catch" around all code that uses locks. Normally you don't notice this. It will appear as an "extra" catch in the ODB.

Calls to "super" methods will look like calls to static methods. (It's not clear what kind of display would be the most "intutive".)
<MySubObj_4>.frob() -> void
  MySuperObj.frob() -> void
frob -> void
Some char and boolean values are recorded as int.

A few Swing bugs and error messages appear in JDK 1.3, which you can ignore (pressing "Stop" in the controller again will force redisplay -- useful in JDK 1.3). Arrays only display the first 1,000 elements. The I/O pane only displays calls to println() writing to standard output.

For faster startup on larger projects (100+ files) where most of the files don't get recompiled very often, you may instrument your files by hand.
RMI and Serialization Problems
If you serialize an object, the program which unserializes it expects to have the same class definition as the program which serialized it. However, if one of the two programs is instrumented and the other isn't, then you can get a variety of exceptions, such as below. One solution is to instrument the files directly and simply run one side with collection turned off.
java.lang.LinkageError: duplicate class definition: ClientImpl_Skel
Possible Verifier Bugs
Because the ODB works by instrumenting the class code, it is possible that your javac compiler produces code in a form that the debugifier won't work with. (It works with all the code I've produced, but I have been surprised before.) The only thing you can do is to: (a) put that class in the "don't instrument" list, or (b) debugify all the other files by hand and run the ODB with instrumentation turned off. And report the bug to me.

A typical verifier error (and related linker error) looks like this:
java.lang.VerifyError: (class: BadClass, method: bar signature: ()V) 
  Unable to pop operand off an empty stack

java.lang.LinkageError: Class org/xml/sax/EntityResolver violates loader constraints
Classloader Issues
The default operation of the ODB is to use a classloader I wrote, which is a simple subclass of java.lang.ClassLoader. However, if you use your own classloader for some of your code, then that code won't be instrumented and it might even cause a classloader conflict (classes mine loads won't recognize classes yours loads). Methods that depend on a specific classloader will also fail. The method Class.getPackage() for example, accesses the classloader to determine the package (for some reason). It will return null for my classloader.

You may either instrument by hand (above) or build and install a system classloader:
java com.lambda.Debugger.InstrumentorForCL ~/Debugger
java com.lambda.Debugger.InstrumentorForCL \Debugger

Future Enhancements

One may imagine all sorts of things. High on the list are: The ability to change command line arguments, some more elaborate data display options, saving a debugging session to disk, integration with IDEs, dynamic reloading of recompiled code, hand instrumentation of collections, new navigation commands, a pane to display all I/O, another to display windows, and of course, performance optimizations. Suggestions welcome.


This is an unoptimized proof-of-concept implementation. I know of an obvious order of magnitude improvement in both time and space for the ODB. Other implementations could do even better. So what can you expect? It works fine for me when debugging the debugger on my 110MHz, 128MB SS4. On my SS4 with a 80MB maximum heap size, I can reasonably create and navigate 40,000 "typical" method calls (with 10 time stamps inside). When you exceed the amount of physical memory available, performance will degrade somewhat. The ODB does run fairly well even in very large virtual spaces -- until you have to garbage collect. Your program may not.

It is possible for a program to create far more time stamps than the ODB can record. You can certainly expand the maximum heap up to the 31-bit limit (2GB). I have successfully collected 100 million time stamps and been able to navigate through them, but it's clearly slower than I'd like. It is highly unlikely that collecting so many time stamps will be useful, simply because it is so difficult to find your way through that much data. Selective instrumentation, judicious use of Start and Stop, and intelligent narrowing of the code to the bug will almost certainly be sufficent.

Installing the Debugger

Download the jar file (on: into $HOME/Debugger, then extract the aliases (see above for .BAT files). Define the latter. If you wish to add command line properties (e.g., -DYOUR_PROPERTY) you can set the variable USER_FLAGS. The aliases (debug, debugn, debugp) will now work.

% cd ~/Debugger
% jar xf debugger.jar aliases 
% source aliases
% debug YourProgram
% cd \Debugger
% jar xf debugger.jar Microsoft
Place the files Microsoft/*.BAT where you will find them.
% debug YourProgram

Running the Demo

Start the ODB by double-clicking. Select "Demo" (a multithreaded quicksort) from the file chooser. It will run, the debugger will popup, and you will be able to "explore" the program.

Bil Lewis
29 December 2006