18-649 Simulator Debugging Tips

Last modified 2010-05-09

This page describes some approaches to debugging the elevator simulation and some debugging tools that are available in the simulation framework.  These are certainly not the only ways (or even necessarily the best ways) to debug the system.  You can use any approach you prefer that allows you to identify the source of bugs in the elevator.  Just remember that the code you submit with your portfolio must be compatible with the officially released version of the simulator from the course webpage.

Use the -seed option to repeatably exercise bugs

One of the features of the elevator simulator is that events that occur simultaneously (at the same simulation time) are executed in a pseudorandom order.  This simulates jitter in real systems.  The -seed option allows you to specify the random seed used to generate the simulation.  If you do not specify a random seed, the current timestamp is used.  The seed used in each test (the one you specified or a the default value) is printed in the .stats file for every test run.  If you run the simulator with the same inputs and the same random seed, you should observe the same pseudorandom event ordering, and thus, be able to exactly repeat any tests that result in an error.  Being able to repeatably run (and debug) the exact test that exercised the bug can be very helpful.

It is very common for subtle bugs (such as race conditions) not to be exercised every time a given the test is run.  So a best practice is to omit the -seed argument during normal testing.  That way, you will run the test with a variety of random seeds and you are more likely to exercise subtle bugs.  Then use the -seed argument to repeat a failed test for debugging, or if you are required to do so by a project handin requirement.

Use breakpoints for bugs in long simulation runs

One of the most difficult debugging tasks is a bug that occurs near the end of a long simulation.  The simulator has a breakpoint feature built in.  Using the -break option (see the commandline reference for full details), you can specify a list of simulation times where the simulator will automatically pause the execution of the simulator at the time or times specified.  Then you can start the simulation at a slow speed to observer the GUI output and any messages printed to stdout.  If you are using an interactive debugger (like netbeans), you can also activate breakpoints in the code before unpausing the simulator.  Note that you must use the -gui option in conjunction with the -break option because the gui has the buttons to pause the simulation and control the rate.  If you are setting breakpoints, you can also use the -rate INFINITE option to automatically make the simulation run at top speed (otherwise the default rate with the gui enabled is 1.0 x realtime). Don't use the -gui flag when the cluster machines are congested with many users at the same time and you want to complete the simulation quickly.

Verbose options and simulator performance considerations

For unit and integration tests, you should always enable verbose output on your controllers.  This is the "best practice" and is necessary in some cases where the testing framework cannot capture rapid changes in system state.  See the discussion document in the soda machine portfolio example for more details on using the debug output in unit and integration tests.

For acceptance tests, the simulation framework provides a set of options controlling which system modules print their status to the command line (look for various "verbose" flags in the commandline reference, like -pv, -mv, -cv, etc.). 

If you run an acceptance test with no verbose flags, the only output you will see is a progress message printed every 60 seconds of simulation time.  Once you start invoking the various various verbose outputs, you will see the performance of the simulator decrease.  This is because the performance bottleneck is how fast the system can handle the i/o operations to stdout.  There are a couple of things you can do to improve the performance of the simulator:

Logging framework and simulator performance considerations

A common approach to debugging is to print information about the system state to the command line.  For example, you might write an entry every time the controller runs to indicate its current state variable.  You could also print the current values of inputs and outputs, and print a message every time the controller makes a transition.  You can see example of this in the Testlight example code and in the code given in the soda machine example portfolio.  The rest of this section discusses best practices for getting good performance out of the simulator while printing the necessary debug messages.

Most of the system objects already have log messages built into them, but feel free to add your own for debugging purposes.  However, the place you will likely do most of your debug logging is in your controller implementations.  The simulator.framework.Controller parent class provides a log() method.  Messages created using the log() method will be printed whenever the verbose constructor parameter is true.  You should use this method for logging instead of directly writing to System.err or System.out.  Not only does this automatically tag the log entry with the name of the module and the current system time, it gives you an easy way to enable and disable log messages without having to comment and uncomment lines of code.  The proper way to use the log() method is:

   log("Current controller state", stateVariable, "; current output: ", mHallCall.getValue());  //THIS IS THE BEST WAY

the improper way to use the log method is:

   log("Current controller state" + stateVariable + "; current output: " + mHallCall);  //THIS IS NOT THE BEST WAY -- BAD

The difference between these two lines is that the proper way sends each variable as an argument, while the improper way uses the '+' operator to concatenate the parts of the message into a single string argument.  Although it may not seem like an important difference, the first way will allow the simulator to run faster, which means less of your valuable time spent waiting for simulations to complete!  If you are interested in the technical reasons why this provides a performance boost in the simulator, see note 1 below.  Otherwise, just take our word for it!

Note 1: 
If you are used to C/C++ programming, a standard practice is to enclose debug print statements in a conditional compilation block.  If the conditional compilation variable is not set, then the code inside the conditional block is not even written into the executable source.  However, java does not support conditional compilation.  This is an important difference because it means that the log messages calls are always executed, even if the verbose flag results in them not being written to stdout.

If you look at the definition of the log() method, you will notice that its   protected void log(Object... msg).  If the verbose flag is true, then whatever argument list you provide is passed to Harness.log(String name, Object... msg) for logging to the command line.  Inside Harness.log(), each Object in the msg array is converted to a String (using an implicit call to the object's toString method) and written to System.out. 

Calling the log() method with the message parts as an argument list (the 'proper' method described above) allows the objects in the message to be evaluated lazily.  That is, if the verbose flag is false, then the toString() and concatenation operations never take place.  If you use the improper method, then the concatenate and toString() operations take place before the call to log(), so the toString() operations are performed regardless of whether or not the message is ultimately written.  Some of the more complex system objects have very complex toString() methods, so avoiding unnecessary toString() calls provides a significant performance improvement.

*Please submit all project-related questions to {email} -- thanks!

Back to the codebase