Difference between revisions of "Debugging Exercises"

(Use a template for expanding boxes)
(Add exercise 4)
Line 65: Line 65:
  
 
To investigate further, try to complete the following debugging steps:
 
To investigate further, try to complete the following debugging steps:
# Set a breakpoint in the <code>main</code> method, before <code>makeAThing</code> is called.
+
# Set a breakpoint in the <code>main</code> method, before <code>makeAThing</code> is called
 
# ''Debug'' the file as a Java application
 
# ''Debug'' the file as a Java application
# When the breakpoint is encountered, ''step in'' to the <code>makeAThing</code> method.
+
# When the breakpoint is encountered, ''step in'' to the <code>makeAThing</code> method
# ''step out'' of the <code>makeAThing</code> method.
+
# ''step over'' the line constructing a new <code>Object</code>
# In the ''Variables'' window, look at the value of the Object variable.
+
# ''step out'' of the <code>makeAThing</code> method
 +
# In the ''Variables'' window, look at the value of the Object variable
 
# 'resume' execution until the program completes
 
# 'resume' execution until the program completes
 +
  
 
{{ExpandingBox
 
{{ExpandingBox
Line 76: Line 78:
 
| > Although <code>makeAThing</code> does create an new Object, that Object isn't actually returned by the method.
 
| > Although <code>makeAThing</code> does create an new Object, that Object isn't actually returned by the method.
 
}}
 
}}
 
  
 
==Exercise 2: Expressions==
 
==Exercise 2: Expressions==
Line 186: Line 187:
 
}}
 
}}
  
 +
==Exercise 4: ImageJ plugins==
 +
 +
'''Goals'''
 +
 +
* Start a debugging session in Eclipse that connects to a running ImageJ instance
 +
 +
 +
Exercises 1-3 are abstract, self-contained programs. The <code>E4RemoteResearch</code> class, on the other hand, is an actual ImageJ [[Writing_plugins#What_is_a_.22plugin.22.3F|plugin]]. Although plugin developers typically set up tests to run their plugin within ImageJ, it is impossible to test all possible runtime environments: users could have a variety of [[Update Sites|update sites]] enabled, custom plugins installed, etc... and each modification is an opportunity for dependency skew and clashes with your plugin. When problems do arise, it is invaluable to have the ability to debug a running copy of ImageJ itself.
 +
 +
<code>E4RemoteResearch</code> does still have a <code>main</code> method to demonstrate the expected output for this plugin - in this case, simply printing the concrete implementation of the <code>ConsoleService</code>. Run the class in Eclipse and you should see some simple output in the console:
 +
 +
<source>I found our console service! Look: class org.scijava.console.DefaultConsoleService</source>
 +
 +
Next, we want to run this plugin in ImageJ and see what happens:
 +
 +
# On the command line, run <code>mvn clean install</code> from the <code>imagej-troubleshooting</code> directory, to build a <code>.jar</code>
 +
# Copy the produced jar (e.g. <code>target/imagej-troubleshooting-0.1.0-SNAPSHOT.jar</code>) to the <code>ImageJ.app/jars</code> directory of your ImageJ installation
 +
# Start ImageJ
 +
 +
Note that the menu path of the plugin is specified in the class's annotation:
 +
 +
<source lang=java>
 +
@Plugin(type = Command.class,menuPath = "Plugins>Troubleshooting>E4 - Print ConsoleService")
 +
</source>
 +
 +
So, you can now run the <code>E4 - Print ConsoleService</code> command either via the menus or [[Command_Finder|command finder]]. You should get an exception:
 +
 +
[[File:E4StackTrace.png]]
 +
 +
In order to connect Eclipse to ImageJ, we need to close our running instance and restart from the command line, setting the [[Debugging#Attaching_to_ImageJ_instances|debug flag]], e.g.:
 +
 +
<source>
 +
ImageJ.app/ImageJ-linux64 --debugger=8000
 +
</source>
 +
[[File:E4DebugConfig.png|400px|right|thumb|Remote Java Application debug configuration]]
 +
This will start up ImageJ in a mode that's able to communicate with Eclipse. Next we need to connect Eclipse to the running ImageJ instance:
 +
 +
# Right-click the <code>E4RemoteResearch</code> source file in the Package Explorer
 +
# Select <code>Debug As > Debug Configurations...</code>
 +
# Scroll down the list of configurations (or search) until you find <code>Remote Java Application</code>
 +
# Double-click <code>Remote Java Application</code> to create the debug configuration
 +
# Rename the configuration to <code>E4RemoteResearch-remote</code> to distinguish it. If necessary you can update the port to match what was specified when you started ImageJ.
 +
# Click the <code>Debug</code> button to start a remote debugging session
 +
 +
At this point ImageJ and Eclipse should be communicating. It's important to understand that the information flow goes from ImageJ to Eclipse: ImageJ says "I am at line number X in class Y" and if Eclipse looks in the source files it knows about, and stops if it finds a breakpoint at that location.
 +
 +
'''However''', when Eclipse looks at its source files, it's not looking at the ''actual'' classes that were used to launch the remote Application. In fact, the classes Eclipse looks in depend entirely on the classpath of the project ''that was used to start the remote debugging session''. So when you are remote debugging, there are two best practices to follow:
 +
* Launch the remote session from the project you're interested in (if you want to debug a class in <code>scijava-common</code>, don't start the session from the <code>imagej-legacy</code> project)
 +
* Make sure the source in Eclipse matches what's in the remote application (an easy way to guarantee this is to build the <code>.jar</code> and copy it to the application)
 +
 +
Since we already followed these best practices, we can now finally debug our plugin:
 +
# In Eclipse, set a breakpoint on the line where the <code>ConsoleService> is being cast to <code>DefualtConsoleService</code>
 +
# In ImageJ, run the <code>E4 - Print ConsoleService</code> command
 +
# In Eclipse, when the breakpoint is hit, inspect the value of the <code>consoleService</code> field
 +
 +
 +
{{ExpandingBox
 +
| What is the class of <code>consoleService</code>?
 +
| >It's a <code>LegacyConsoleService</code>.
 +
}}
 +
 +
{{ExpandingBox
 +
| Extra credit: why did this plugin work when we ran its <code>main</code> method, but not in ImageJ?
 +
| >The <code>Context</code> in the <code>main</code> method is built with only one <code>ConsoleService</code> implementation available - <code>DefaultConsoleService</code>. In the full <code>Context</code> used in ImageJ, a higher-priority <code>LegacyConsoleService</code> overrides the <code>DefaultConsoleService</code>.
 +
}}
  
 
=What next?=
 
=What next?=

Revision as of 10:04, 24 November 2015

Template:Development



Debugging is the art of determining the cause and/or location of a problem. The purpose of this guide is to provide developers practical, hands-on experience using a variety of debugging techniques to identify problems in code.

Requirements

As ImageJ is built using the SciJava principles of project management, this guide assumes a basic familiarity with these topics and tools, especially:

Additionally, you should:

Finally, you should read the Debugging in Eclipse section, at least, of Lars Vogel's Eclipse tutorial. Note that there is significant overlap between the topics covered between these tutorials. Vogel's guide does a fantastic job of laying out the debugging interface and tools in Eclipse, while this guide focuses on providing hands-on practice and explaining the reasoning for when to use these tools.

If you find yourself confused when this tutorial asks you to do something in Eclipse (e.g. "start debugging", "step into") it's almost certainly covered in Vogel's guide.

What not to do: print statements

For many developers, the first tool in their debugging toolbox is the print statement. Print statements are easy to lean on as a safety crutch: you don't need any special knowledge to use them, and they often work to answer common questions (e.g. "why is this variable null here?", "how many elements are in my array here?").

However, there are critical drawbacks to trying to debug via print statement:

  • They are slow. If you realize you need to move or add a print statement, you need to recompile your code and re-launch your application.
  • They are part of the code. Adding print statements changes line numbers, causes git to pick up modifications to the source code, and can even affect performance and/or behavior.
  • They are limited. Even the most basic breakpoint and expression evaluation in Eclipse debug mode gives you vastly more power and flexibility over print statements.

Learning to use debugging tools is, understandably, a burden: it's "one more thing" to learn as a developer. But if you want to develop ImageJ plugins, you will almost certainly run into cases where debugging is necessary. So you may as well start familiarizing yourself with the tools now, gaining skills and perspectives that will serve you well throughout your career.

Using this guide

Remember the goal of these exercises is not to solve the problems, but to learn how and when to use the myriad of troubleshooting techniques available. Given the inherent complexity that comes with a combined work, all of these techniques have their time and place when working on a rich application like ImageJ.

Because this project is intended to help new developers practice troubleshooting skills, it uses contrived examples, limited in complexity. If you have complete, perfect knowledge of the code then there isn't really a need for troubleshooting, as it is trivial to see why something is behaving incorrectly. Thus the source of these exercises is divided into hidden and visible packages. Users are strongly recommended to only inspect and set breakpoints from the visible classes. From a development point of view, consider the hidden package a 3rd-party library that you may not have control over.

Changing the source code to actually fix the bugs is outside the scope of this guide, but motivated users are of course welcome to do so for practice.

If at any time you need to revert changes to this repository, you can always do so via the command:

git reset --hard origin/master

Exercises

Exercise 1: Breakpoints

Goals

  • Practice setting breakpoints in Eclipse
  • Use the Variables window to inspect variable values
  • Use the navigation commands to traverse code while in Debug mode


The first thing we want to do is open the source file, E1BasicBreakpoints, and run it to get an idea of what's going on. We should see a simple stack trace:


E1StackTrace.png


Stack traces are a common starting point for debugging, as they indicate something has gone wrong in Java that the program was not prepared to handle. In this case, the stack trace tells us we know the what (tried to use a null object) and the where (the topmost line number), but not why the object was null at that point - the actual root cause of the exception.

To investigate further, try to complete the following debugging steps:

  1. Set a breakpoint in the main method, before makeAThing is called
  2. Debug the file as a Java application
  3. When the breakpoint is encountered, step in to the makeAThing method
  4. step over the line constructing a new Object
  5. step out of the makeAThing method
  6. In the Variables window, look at the value of the Object variable
  7. 'resume' execution until the program completes


Now that you've walked through the program, do you know why we got a NullPointerException?
> Although makeAThing does create an new Object, that Object isn't actually returned by the method.


Exercise 2: Expressions

Goals

  • Set breakpoints on exception creation
  • Use the Expressions window to evaluate code at runtime


Start by opening the E2EffectiveExpressions source and running it. Like the previous exercise, we have a stack trace to start from:


E2StackTrace.png


Try setting a breakpoint on the conditional line:

if (index < 0 || index >= list.size()) {


Try debugging now, using Resume any time a breakpoint is encountered. How many times do you hit a breakpoint?
>Three times. This is because we're debugging inside a method that is used multiple times in our program.



Since we are only interested in the processElementAtIndex method when a problem actually occurs, let's try something different:

Setting a breakpoint on an exception
  1. From the Breakpoints window in the Debug perspective, delete the old breakpoint
  2. Now use the Add Java Exception Breakpoint button to add a breakpoint to IllegalArgumentException
  3. Debug the program. When it stops, inspect the Variables window.
Inspecting the variables window

At this point, we know there is a problem accessing the 99999th element of the list, but the variables window doesn't tell us exactly what the problem is. We can manually expand and explore the list variable - but given its size that could be cumbersome.

Instead, let's use expressions. If it's not already visible, open the Window > Show View > Expressions view.

In this view, we can add any number of Java expressions to evaluate. We can call methods from, and on, any variables and classes visible from the current scope.

Warning: if you evaluate expressions that change the state of variables, for example List.add, then those changes will persist. Sometimes you do legitimately need to alter the state of variables while debugging (for example, to force the conditions in which a bug appears). Just be aware that you could also be introducing problematic behavior when evaluating expressions.

To complete this exercise:

  1. Write an expression that calls the size() method of our list.
  2. Write a 2nd expression that evaluates to the value of the index variable


Once you've evaluated these expressions, can you tell what went wrong in the program?
>We asked for a list of size 100,000 but a list of size 99,999 came back. Since we used hard-coded indices, instead of size-relative (e.g. list.size()-1), a method was called wanting to access a non-existent element.


Exercise 3: Conditional breakpoints

Goals

  • Create a breakpoint that triggers after a specified number of hits
  • Create a breakpoint that triggers when a certain condition is true


Start by opening the E3ConditionalCrisis source and running it. This time our console output looks a bit different:

E3StackTrace.png


In addition to the exception stack trace, the program itself appears to have found an invalid object, causing the processing to go unfinished. Although we could set a breakpoint on the exception, as we did in exercise 2, the exception is actually happening after the more interesting part of the program - the loop. As we learned in exercise 2, breakpoints in code that is called repeatedly are annoying, so let's see what we can find by attaching conditions to our breakpoint.

First set a breakpoint on the line after the everythingIsOK assignment:

everythingIsOK = ObjectAnalyzer.processElementAtIndex(myArray, i);
i++;

Then try the following:

Setting a hit count
  1. Open the Breakpoints window
  2. Right-click our breakpoint and select Breakpoint Properties...
  3. Check the Hit Count box and set it to the object number printed in the error message.
  4. Try debugging


Was there a problem with the current object when/if your breakpoint is hit?
>If there was, try it again! In this exercise, the "broken" object index is non-deterministic - so it is unlikely to be exactly the same index two runs in a row.

>If the broken object appears later the second time, your breakpoint will hit but the current object will likely be fine. >If the "broken" object appears earlier the second time, your breakpoint won't be hit at all.



Using count-based conditional breakpoints can be very useful if the error is deterministic. In this case we need to try something different. We know the everythingIsOK flag reflects the integrity of the object at a given index - so what we really want to use here is a breakpoint that stops in the loop when the everythingIsOK flag is set to false:

Setting a conditional expression
  1. Open the Breakpoints window again
  2. Open the properties of our breakpoint
  3. Uncheck the Hit Count box
  4. Check the Conditional box, and "Suspend when 'true'"
  5. Enter the condition we want to check (any Java conditional statement can be used here)
  6. Try debugging again

Were you able to get the breakpoint to stop in the loop only when a problem is encountered?


What was suspicious about the object at that index?
>The object was null.


Exercise 4: ImageJ plugins

Goals

  • Start a debugging session in Eclipse that connects to a running ImageJ instance


Exercises 1-3 are abstract, self-contained programs. The E4RemoteResearch class, on the other hand, is an actual ImageJ plugin. Although plugin developers typically set up tests to run their plugin within ImageJ, it is impossible to test all possible runtime environments: users could have a variety of update sites enabled, custom plugins installed, etc... and each modification is an opportunity for dependency skew and clashes with your plugin. When problems do arise, it is invaluable to have the ability to debug a running copy of ImageJ itself.

E4RemoteResearch does still have a main method to demonstrate the expected output for this plugin - in this case, simply printing the concrete implementation of the ConsoleService. Run the class in Eclipse and you should see some simple output in the console:

I found our console service! Look: class org.scijava.console.DefaultConsoleService

Next, we want to run this plugin in ImageJ and see what happens:

  1. On the command line, run mvn clean install from the imagej-troubleshooting directory, to build a .jar
  2. Copy the produced jar (e.g. target/imagej-troubleshooting-0.1.0-SNAPSHOT.jar) to the ImageJ.app/jars directory of your ImageJ installation
  3. Start ImageJ

Note that the menu path of the plugin is specified in the class's annotation:

@Plugin(type = Command.class,menuPath = "Plugins>Troubleshooting>E4 - Print ConsoleService")

So, you can now run the E4 - Print ConsoleService command either via the menus or command finder. You should get an exception:

E4StackTrace.png

In order to connect Eclipse to ImageJ, we need to close our running instance and restart from the command line, setting the debug flag, e.g.:

ImageJ.app/ImageJ-linux64 --debugger=8000
Remote Java Application debug configuration

This will start up ImageJ in a mode that's able to communicate with Eclipse. Next we need to connect Eclipse to the running ImageJ instance:

  1. Right-click the E4RemoteResearch source file in the Package Explorer
  2. Select Debug As > Debug Configurations...
  3. Scroll down the list of configurations (or search) until you find Remote Java Application
  4. Double-click Remote Java Application to create the debug configuration
  5. Rename the configuration to E4RemoteResearch-remote to distinguish it. If necessary you can update the port to match what was specified when you started ImageJ.
  6. Click the Debug button to start a remote debugging session

At this point ImageJ and Eclipse should be communicating. It's important to understand that the information flow goes from ImageJ to Eclipse: ImageJ says "I am at line number X in class Y" and if Eclipse looks in the source files it knows about, and stops if it finds a breakpoint at that location.

However, when Eclipse looks at its source files, it's not looking at the actual classes that were used to launch the remote Application. In fact, the classes Eclipse looks in depend entirely on the classpath of the project that was used to start the remote debugging session. So when you are remote debugging, there are two best practices to follow:

  • Launch the remote session from the project you're interested in (if you want to debug a class in scijava-common, don't start the session from the imagej-legacy project)
  • Make sure the source in Eclipse matches what's in the remote application (an easy way to guarantee this is to build the .jar and copy it to the application)

Since we already followed these best practices, we can now finally debug our plugin:

  1. In Eclipse, set a breakpoint on the line where the ConsoleService> is being cast to <code>DefualtConsoleService
  2. In ImageJ, run the E4 - Print ConsoleService command
  3. In Eclipse, when the breakpoint is hit, inspect the value of the consoleService field


What is the class of consoleService?
>It's a LegacyConsoleService.


Extra credit: why did this plugin work when we ran its main method, but not in ImageJ?
>The Context in the main method is built with only one ConsoleService implementation available - DefaultConsoleService. In the full Context used in ImageJ, a higher-priority LegacyConsoleService overrides the DefaultConsoleService.


What next?

At the start of this guide we mentioned that the goal of debugging isn't explicitly to fix the problem. So a natural question that follows is - once we've successfully identified the cause of an issue (or at least narrowed things down), what are the next steps to take?

The answer depends on the debugging developer's skill and responsibilities, and whether or not they have identified a fix (a fix is often easy to come up with once the problem is identified).

If a fix is identified:

  • If you are a maintainer of the component you can make the fix directly, or use a topic branch if the fix needs discussion first.
  • If you do not have commit rights to the repository, you can contribute the fix via pull request

If a fix is unclear:

Even if you can't contribute a fix, if you went through the effort of debugging - at the very list you should identify the problem, steps you took to debug, and potential fix(es) via a bug report so that your effort is not lost.

See Also