Enhancing the Debugger with Dedicated Views

During a debugging session I came to a complex java.awt.Shape object and was in desperate need to have a look at this shape. But how?

How to Have User-defined Data Views During a Debugging Session

Does Your Debugger Support User-defined Data Views?

Although the debugger which comes with IntelliJ IDEA is very powerful I don’t know of a simple possibility to enhance it with a user-defined data viewer. I know that it somehow can be done in Visual Studio, but never did it myself there, so I have no idea how simple this is.

A Way Which Should Work Everywhere

But what any modern debugger can do is evaluating a bit of ad-hoc code expression which is evaluated using the current context, so any data currently available can be used in the given expression. In IDEA this is called Evaluate Expression.

First Steps

So I decided to use that to start a frame which displays the shape and more information available. First task was obviously to implement the shape viewing component which I christianed ShapeAnalyzer which just extends ‘JPanel’. For the basic implementation I just used an ellipse as example shape. After this worked satisfyingly,
I added a simple static method to my class which just creates a frame which displays the shape given as an argument.

Bite the Bullet

Back in my debugging session it didn’t show up, as I already had feared. The reason is that when stopping in a debugger usually all threads are stopped. As this includes the AWT GUI thread my cool ShapeAnalyzer was not able to do anything. There is a possiblity to stop only one thread and let the others do their work, but this has other caveats. Also Shapes are usually created and used in the GUI thread. So I had to bite the bullet and implement a solution which introduced a bit more complication:

Create a new JVM process where my ShapeAnalyzer is displayed, and somehow transfer the shape of interest over to it.

Luckily I already did the work necessary for the creation of a new JVM when implementing my Restarter class. It allows to run your Java application with more memory without any user interaction by just starting it again with improved memory settings. Read here if that sounds interesting.

So what was left was the transfer of the shape. I first used simple serialization, but the shape I was interested wasn’t serializable. So in the end I had to use a dedicated form of serialization based on java.awt.geom.PathIterator which only transfers primitive values and therefore will always work.

With that I succeeded indeed, and could analyze my shape when stopped in the debugger.

A Bit of Code

The ShapeAnalyzer described here uses various internal classes to make the analysis easier. Therefore its complete code wouldn’t help you much. But if you are interested to do something similar the main pieces are given here:

/**
 * Shape analyzer especially useful during debugging sessions.
 *
 * The problem during debug sessions is that usually all threads are stopped,
 * when you want to have a look at a shape. Therefore calling
 * {@link ShapeAnalyzer#xDisplay(Shape)} with your shape will display the
 * given shape in a completely different virtual machine. This especially
 * means that later changes to the source shape are not reflected.
 *
 * @author <a href="mailto:rammi@caff.de">Rammi</a>
 * @since Dezember 17, 2019
 */
public class ShapeAnalyzer
        extends JPanel
{
  private static final String SERIALIZED_NORMALLY = "-Xser";
  private static final String SERIALIZED_WITH_PIT = "-Xpit";
  
  // [...] some 1000 lines of displaying and analyzing shapes


  /**
   * Display the given shape.
   * As this class uses standard Swing components nothing will happen
   * if the AWT GUI thread is stopped. Use {@link #xDisplay(Shape)} in
   * this case which will create a new process for displaying the shape.
   * @param shape shape to display
   */
  public static void display(@NotNull Shape shape)
  {
    final JFrame frame = new JFrame("Shape Analyzer");
    frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);

    frame.getContentPane().add(new ShapeAnalyzer(shape));

    frame.setSize(1200, 800);
    frame.setVisible(true);
  }

  /**
   * Display the given shape in an external VM.
   * This takes care of creating a process with this class as main class
   * and transferring the shape to this process.
   * The shape will be displayed in the given process by this class.
   * @param shape shape to display
   * @throws IOException when starting an external VM is failing
   */
  public static void xDisplay(@NotNull Shape shape)
          throws IOException
  {
    final String javaCmd = Restarter.getJavaCommand();
    if (javaCmd == null) {
      throw new IOException("Cannot determine java command!");
    }
    String sendType = SERIALIZED_NORMALLY;
    final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
      oos.writeObject(shape);
    } catch (NotSerializableException x) {
      sendType = SERIALIZED_WITH_PIT;
      bos.reset();
      try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
        final double[] coords = new double[6];
        for (PathIterator pit = shape.getPathIterator(null); !pit.isDone(); pit.next()) {
          oos.writeBoolean(true);
          final int segment = pit.currentSegment(coords);
          oos.writeInt(segment);
          int size = 0;
          switch (segment) {
          case PathIterator.SEG_MOVETO:
          case PathIterator.SEG_LINETO:
            size = 2;
            break;

          case PathIterator.SEG_QUADTO:
            size = 4;
            break;

          case PathIterator.SEG_CUBICTO:
            size = 6;
            break;
          }
          for (int c = 0;  c < size;  ++c) {
            oos.writeDouble(coords[c]);
          }
        }
        oos.writeBoolean(false);
      }
    }

    final java.util.List<String> commandLine = new LinkedList<>();
    commandLine.add(javaCmd);
    commandLine.add("-classpath");
    commandLine.add(System.getProperty(Restarter.PROPERTY_JAVA_CLASSPATH));
    Restarter.addSystemProperties(commandLine);
    commandLine.add(ShapeAnalyzer.class.getCanonicalName());
    commandLine.add(sendType);
    Debug.message("Command line: %0", Types.join(" ", commandLine));
    final ProcessBuilder processBuilder = new ProcessBuilder(commandLine);
    final Process process = processBuilder.start();
    process.getOutputStream().write(bos.toByteArray());
  }

  /**
   * Read a shape in serialized path iterator format.
   * The serialization uses only primitive values. Format is:
   * <ol>
   *   <li>
   *     a boolean value: if {@code true} read the following segment, otherwise the
   *     shape is finished
   *   </li>
   *   <li>
   *     an integer value which defines the path iterator segment type
   *   </li>
   *   <li>
   *     depending on segment type: 0, 2, 4, or 6 double values which define
   *     the coordinates of the segment.
   *   </li>
   *   <li>
   *     Start over with point 1.
   *   </li>
   * </ol>
   * @param is input stream to read from
   * @return the deserialized shape
   * @throws IOException on read or deserialization errors
   */
  @NotNull
  private static Shape readPitShape(@NotNull InputStream is) throws IOException
  {
    try (ObjectInputStream ois = new ObjectInputStream(is))  {
      final GeneralShape shape = GeometryTool.createGeneralShape();
      while (ois.readBoolean()) {
        final int segment = ois.readInt();
        switch (segment) {
        case PathIterator.SEG_MOVETO:
          shape.moveTo(ois.readDouble(), ois.readDouble());
          break;
        case PathIterator.SEG_LINETO:
          shape.lineTo(ois.readDouble(), ois.readDouble());
          break;
        case PathIterator.SEG_QUADTO:
          shape.quadTo(ois.readDouble(), ois.readDouble(),
                       ois.readDouble(), ois.readDouble());
          break;
        case PathIterator.SEG_CUBICTO:
          shape.curveTo(ois.readDouble(), ois.readDouble(),
                        ois.readDouble(), ois.readDouble(),
                        ois.readDouble(), ois.readDouble());
          break;
        case PathIterator.SEG_CLOSE:
          shape.closePath();
          break;
        }
      }
      return shape;
    }
  }

  /**
   * Start the shape analyzer.
   * Depending on the arguments this behaves differently.
   * With no argument it just displays a default shape.
   * This can be used to debug or enhance this classs.
   * Otherwise exactly one argument is expected:
   * <ul>
   *   <li>
   *     With {@code -Xser} a serialized shape is read from stdin.
   *   </li>
   *   <li>
   *     With {@code -Xpit} a shape's specially serialized path iterator is read
   *     from stdin.
   *   </li>
   * </ul>
   *
   * Calling the {@link #xDisplay(Shape)} method will take care of creating a
   * Java process with this as main class with the correct argument
   * and the given shape in the correct format.
   * @param args as described above
   * @throws IOException on pipe read errors
   * @throws ClassNotFoundException on shape read errors when reading a serialized shape
   */
  public static void main(String[] args) throws IOException, ClassNotFoundException
  {
    if (args.length == 0) {
      display(new Ellipse2D.Double(0, 0, 400, 300));
    }
    if (args.length == 1) {
      switch (args[0]) {
      case SERIALIZED_NORMALLY:
        try (ObjectInputStream ois = new ObjectInputStream(System.in)) {
          Shape shape = (Shape)ois.readObject();
          display(shape);
        }
        break;
      case SERIALIZED_WITH_PIT:
        display(readPitShape(System.in));
        break;
      }
    }
  }
}

As said before the helper class Restarter used threefold in method xDisplay() above is discussed here and part of the Caff Commons where it is included in module caff-common. All Caff Commons sources are provided on the linked page.

Annotations @NotNull/@Nullable may be removed (or add Caff Commons module caff-generics which contains them). Calls to Debug can be also be removed as all they are doing is just logging, although the Debug class is already contained in the same module as Restarter.

Screenshot of ShapeAnalyzer