Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8342782

AWTEventMulticaster throws StackOverflowError using AquaButtonUI

XMLWordPrintable

    • b03
    • generic
    • generic

      ADDITIONAL SYSTEM INFORMATION :
      Tested using OpenJDK 23 on Mac 15.

      A DESCRIPTION OF THE PROBLEM :
      The AWTEventMulticaster is a tree with 2 nodes, and by default it is an unbalanced tree that basically acts like a LinkedList. It dispatches events recursively. So if it tries to dispatch an event to 8,000 listeners: the stack will grow to (at least) 8,000 lines. This can easily result in a StackOverflowError.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Run the attached source code. Click any one of the checkboxes.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      There should not be a StackOverflowError.

      I recommend one of the following:

      A. The AWTEventMulticaster could be adapted so it does not dispatch events recursively most of the time. (It will always be possible to create AWTEventMulticasters where *both* nodes are an AWTEventMulticaster, but in practice that is probably not common given how the static helper methods are set up.) An AWTEventMulticaster can (with minor adjustments) usually be iterated over as a list instead.

      B. The AquaButtonUI could be adapted so only one entity is responsible for setting up the default button. Currently if you have n-many buttons then you'll have n-many listeners constantly calling `aquaRootPaneUI.updateDefaultButton(..)`

      C. The AquaButtonCheckBoxUI (and others) could be adapted so they do NOT trigger this line in AquaButtonUI:
      `b.addAncestorListener(listener);`. This AncestorListener is useless to JCheckBoxes. The first line of `updateDefaultButton()` immediately aborts for JCheckBoxes:
      `if (!(b instanceof JButton)) return;`

      So in this case: we're adding thousands of AncestorListeners and creating a large AWTEventMulticaster tree that never actually does anything. (Once the listener realizes it's dealing with a JCheckBox: it will immediately abort. The only logic there has to do with updating the root pane's default button, and a JCheckBox isn't interested in being a default button.)


      ACTUAL -

      There is a StackOverflowError resembling:

      Exception in thread "AWT-EventQueue-0" java.lang.StackOverflowError
      at java.desktop/java.awt.AWTEventMulticaster.removeInternal(AWTEventMulticaster.java:983)
      at java.desktop/java.awt.AWTEventMulticaster.remove(AWTEventMulticaster.java:153)
      at java.desktop/java.awt.AWTEventMulticaster.removeInternal(AWTEventMulticaster.java:983)
      at java.desktop/java.awt.AWTEventMulticaster.remove(AWTEventMulticaster.java:153)
      at java.desktop/java.awt.AWTEventMulticaster.removeInternal(AWTEventMulticaster.java:983)
      at java.desktop/java.awt.AWTEventMulticaster.remove(AWTEventMulticaster.java:153)
      at java.desktop/java.awt.AWTEventMulticaster.removeInternal(AWTEventMulticaster.java:983)
      at java.desktop/java.awt.AWTEventMulticaster.remove(AWTEventMulticaster.java:153)
      at java.desktop/java.awt.AWTEventMulticaster.removeInternal(AWTEventMulticaster.java:983)
      at java.desktop/java.awt.AWTEventMulticaster.remove(AWTEventMulticaster.java:153)
      at java.desktop/java.awt.AWTEventMulticaster.removeInternal(AWTEventMulticaster.java:983)
      at java.desktop/java.awt.AWTEventMulticaster.remove(AWTEventMulticaster.java:153)
      at java.desktop/java.awt.AWTEventMulticaster.removeInternal(AWTEventMulticaster.java:983)
      at java.desktop/java.awt.AWTEventMulticaster.remove(AWTEventMulticaster.java:153)
      at java.desktop/java.awt.AWTEventMulticaster.removeInternal(AWTEventMulticaster.java:983)
      at java.desktop/java.awt.AWTEventMulticaster.remove(AWTEventMulticaster.java:153)
      at java.desktop/java.awt.AWTEventMulticaster.removeInternal(AWTEventMulticaster.java:983)


      ---------- BEGIN SOURCE ----------
      import javax.swing.*;
      import javax.swing.border.EmptyBorder;
      import javax.swing.event.AncestorEvent;
      import javax.swing.event.AncestorListener;
      import java.awt.*;
      import java.util.Random;
      import java.util.TimerTask;

      public class AWTEventMulticaster_UITest extends JFrame {
          public static void main(String[] args) {
              setupEventDispatchThreadMonitor();
              SwingUtilities.invokeLater(() -> {
                  AWTEventMulticaster_UITest t = new AWTEventMulticaster_UITest();
                  t.pack();
                  t.setVisible(true);
              });
          }

          /**
           * Setup a java.util.Timer that pings the event dispatch thread every 100ms. Each ping expects to be
           * able to execute a Runnable on the EDT within 100 ms. This logs periods of unresponsiveness to System.out
           */
          private static void setupEventDispatchThreadMonitor() {
              java.util.Timer timer = new java.util.Timer();
              long PING_INTERVAL = 100;
              timer.scheduleAtFixedRate(new TimerTask() {
                  long unresponsiveStartTime = -1;

                  @Override
                  public void run() {
                      final long startTime = System.currentTimeMillis();
                      SwingUtilities.invokeLater(() -> {
                          long t = System.currentTimeMillis();
                          long elapsed = t - startTime;
                          if (elapsed > 100) {
                              if (unresponsiveStartTime == -1)
                                  unresponsiveStartTime = startTime;
                          } else if (unresponsiveStartTime != -1) {
                              // we just recovered; the UI is responsive again

                              long z = t - unresponsiveStartTime - PING_INTERVAL;
                              if (z > 0)
                                  System.out.println("The event dispatch thread was unresponsive for approx " + z + " millis");

                              unresponsiveStartTime = -1;
                          }
                      });
                  }
              }, PING_INTERVAL, PING_INTERVAL);
          }

          Random random = new Random(0);

          /**
           * This is a colored square with one JCheckBox in each corner.
           * Toggling any checkbox will hide and reshow the window's content pane. The
           * end result is not visually noticeable to the user, but this generates a lot
           * of events. In some cases this generates a StackOverflowError
           */
          class Cell extends JPanel {
              Cell() {
                  setBackground(new Color(random.nextInt(0xffffff)));
                  setPreferredSize(new Dimension(80, 80));
                  setLayout(new GridBagLayout());
                  add(createCheckBox(), new GridBagConstraints(0, 0, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0,0,0,0), 0, 0));
                  add(createCheckBox(), new GridBagConstraints(1, 0, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0,0,0,0), 0, 0));
                  add(createCheckBox(), new GridBagConstraints(0, 1, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0,0,0,0), 0, 0));
                  add(createCheckBox(), new GridBagConstraints(1, 1, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0,0,0,0), 0, 0));
              }

              private JCheckBox createCheckBox() {
                  JCheckBox cb = new JCheckBox();
                  // on Mac you get an AncestorListener in the AquaButtonCheckBoxUI, but for the sake of this test
                  // let's make sure you get one no matter which L&F you're using:
                  if (cb.getAncestorListeners().length == 0) {
                      cb.addAncestorListener(new AncestorListener() {
                          @Override
                          public void ancestorAdded(AncestorEvent event) {}

                          @Override
                          public void ancestorRemoved(AncestorEvent event) {}

                          @Override
                          public void ancestorMoved(AncestorEvent event) {}
                      });
                  }
                  cb.addActionListener(e -> {
                      System.out.println("hide and reshow content pane");
                      getContentPane().setVisible(false);
                      getContentPane().setVisible(true);
                  });
                  return cb;
              }
          }

          public AWTEventMulticaster_UITest() {
              int CELL_COUNT = 2_000;
              JEditorPane instructions = new JEditorPane();
              instructions.setText("This window contains " + (CELL_COUNT * 4) + " checkboxes. It demonstrates two problems:\n\n" +
                      "1. When you toggle any checkbox this window's content pane toggles to hidden and shown. This can throw StackOverflowErrors, because each button attached an AncestorListener via the AWTEventMulticaster to an ancestor component. The AWTEventMulticaster defines a two-node tree, and recursing through that tree can be expensive.\n\n" +
                      "2. When you use a mouse wheel to scroll over the scrollpane the event dispatch thread can remain unresponsive for several seconds. (\"Several\" can mean anywhere from 3-30.)");
              instructions.setBorder(new EmptyBorder(5,5,5,5));
              getContentPane().setLayout(new BorderLayout());
              getContentPane().add(instructions, BorderLayout.NORTH);

              JPanel p = new JPanel(new GridBagLayout());
              getContentPane().add(new JScrollPane(p), BorderLayout.CENTER);

              GridBagConstraints c = new GridBagConstraints();
              c.gridx = 0; c.gridy = 0; c.weightx = 1; c.weighty = 1; c.insets = new Insets(3,3,3,3);
              for (int a = 0; a < CELL_COUNT; a++) {
                  p.add(new Cell(), c);
                  c.gridx++;
                  if (c.gridx == 10) {
                      c.gridx = 0;
                      c.gridy++;
                  }
              }
          }
      }

      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      This problem was originally observed using AquaButtonUIs. The test case here has been modified so that should not be necessary; by adding an AncestorListener all platforms should be capable of reproducing this problem.

      But since the original problem had to with AquaButtonUIs: the work-around was just to change the button UI like this:

      myCheckbox.setButtonUI(new BasicCheckBoxUI())



      FREQUENCY : always


            Unassigned Unassigned
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            5 Start watching this issue

              Created:
              Updated:
              Resolved: