JDK-8344697 : Heavy Performance Cost For Obsolete Aqua Button Repaints
  • Type: Bug
  • Component: client-libs
  • Sub-Component: javax.swing
  • Affected Version: 8,11,17,23,24
  • Priority: P4
  • Status: Closed
  • Resolution: Duplicate
  • OS: os_x
  • CPU: generic
  • Submitted: 2024-11-20
  • Updated: 2025-04-21
  • Resolved: 2025-01-30
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other
tbdResolved
Related Reports
Duplicate :  
Relates :  
Description
ADDITIONAL SYSTEM INFORMATION :
Observed using Java 24 on Mac 15.1

A DESCRIPTION OF THE PROBLEM :
The Aqua L&F goes to a lot of effort to continually repaint the root pane's default button. Sometimes this effort causes performance problems, and this pulsing repaint feature is now obsolete.

According to https://en.wikipedia.org/wiki/Aqua_(user_interface)#OS_X_Yosemite,_El_Capitan,_macOS_Sierra,_High_Sierra,_Mojave,_and_Catalina :
> A blue button is the default action, and in OS releases prior to Yosemite, would appear to pulse to prompt the user to carry out that action.

Native aqua dialogs no longer have this effect, but swing's AquaLookAndFeel still tries to implement it.

Attached is a demo that demonstrates a performance problem related to this feature. The AquaButtonUI uses an AncestorListener to help update the default button. If we include the AncestorListener: then the test took 1.7s on my machine. If we suppress it, then the test took .1s.

This test features 1,000 checkboxes. If you increase that to 10,000 checkboxes: then the attached demo take around 20s on my computer. (And it took .2s without the AncestorListener.)

Apple stopped supporting Yosemite's predecessor (Mavericks, Mac OS 10.9) around 2016.

I suggest the best resolution is simply to remove the logic that repaints the default button continually. This includes the AncestorListener this demo focuses on, and other logic.

If some developers feel that removing the feature is unwise:
We can probably improve the existing logic to maintain support for the pulsing effect. (For example: a JCheckBox will never pulse as the default button, so it shouldn't require listeners to maintain that effect.)

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Run the attached application on Mac.
2. Click the "Simulate Swipe" for a consistent test, or just swipe 2 fingers downward over the scrollpane.
3. Look in the console for a message resembling: "Swipe simulation took X ms".

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The simulation should take less than .5s.
ACTUAL -
The simulation took 1.7s on my MacBook.

---------- BEGIN SOURCE ----------
import javax.swing.*;
import javax.swing.event.AncestorListener;
import java.awt.*;
import java.awt.event.*;


/**
 * Test instructions:
 * Click the "Simulate Swipe" button. Look in the console for "Swipe simulation took X ms".
 * If x is under 500ms, then this test passes. If it is over 500s, then this test fails.
 * <p>
 * On my MacBook Pro this took 1,762ms. If I deselect "Include AncestorListeners", then the
 * simulation takes 103ms. I expect it to always be closer to 100ms than 1.7s
 */
public class ScrollingButtonPanelTest extends JPanel {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame f = new JFrame();
            f.getContentPane().add(new ScrollingButtonPanelTest());
            f.pack();
            f.setVisible(true);
        });
    }

    static final int NUMBER_OF_CHECKBOXES = 1000;

    private static final String PROPERTY_ORIGINAL_ANCESTOR_LISTENERS = "originalAncestorListeners";

    JScrollPane scrollPane;
    JCheckBox includeAncestorListenerButton = new JCheckBox("Include AncestorListeners", true);
    JPanel scrollPaneContent = new JPanel(new GridLayout(NUMBER_OF_CHECKBOXES, 1));

    record ScrollMovement(int wheelRotation, double preciseWheelRotation, long when) {};

    /**
     * These describe the MouseWheelEvents I observed when I swiped down over the scrollpane using
     * my MacBook's touchpad.
     */
    ScrollMovement[] swipeMovements = new ScrollMovement[] {
            new ScrollMovement(0, 0.4, -1),
            new ScrollMovement(1, 0.5, 0),
            new ScrollMovement(6, 6.5, 15),
            new ScrollMovement(0, 0.1, 15),
            new ScrollMovement(12, 11.600000000000001, 30),
            new ScrollMovement(12, 12.3, 49),
            new ScrollMovement(13, 12.3, 63),
            new ScrollMovement(11, 11.8, 82),
            new ScrollMovement(12, 11.3, 97),
            new ScrollMovement(10, 10.700000000000001, 113),
            new ScrollMovement(11, 10.3, 130),
            new ScrollMovement(9, 9.700000000000001, 147),
            new ScrollMovement(10, 9.3, 163),
            new ScrollMovement(9, 8.9, 182),
            new ScrollMovement(8, 8.4, 197),
            new ScrollMovement(8, 7.9, 214),
            new ScrollMovement(7, 7.4, 233),
            new ScrollMovement(7, 7.0, 249),
            new ScrollMovement(7, 6.4, 265),
            new ScrollMovement(6, 6.0, 281),
            new ScrollMovement(5, 5.7, 298),
            new ScrollMovement(6, 5.300000000000001, 315),
            new ScrollMovement(5, 4.9, 331),
            new ScrollMovement(4, 4.5, 348),
            new ScrollMovement(4, 4.1000000000000005, 365),
            new ScrollMovement(4, 3.9000000000000004, 382),
            new ScrollMovement(4, 3.5, 399),
            new ScrollMovement(3, 3.3000000000000003, 415),
            new ScrollMovement(3, 3.0, 431),
            new ScrollMovement(3, 2.8000000000000003, 449),
            new ScrollMovement(2, 2.6, 465),
            new ScrollMovement(3, 2.2, 481),
            new ScrollMovement(2, 2.1, 498),
            new ScrollMovement(2, 1.9000000000000001, 515),
            new ScrollMovement(1, 1.7000000000000002, 532),
            new ScrollMovement(2, 1.6, 548),
            new ScrollMovement(1, 1.4000000000000001, 565),
            new ScrollMovement(2, 1.3, 582),
            new ScrollMovement(1, 1.2000000000000002, 599),
            new ScrollMovement(1, 1.1, 615),
            new ScrollMovement(1, 1.0, 632),
            new ScrollMovement(1, 0.9, 649),
            new ScrollMovement(1, 0.9, 666),
            new ScrollMovement(0, 0.8, 682),
            new ScrollMovement(1, 0.7000000000000001, 699),
            new ScrollMovement(1, 0.7000000000000001, 716),
            new ScrollMovement(0, 0.6000000000000001, 733),
            new ScrollMovement(1, 0.6000000000000001, 749),
            new ScrollMovement(1, 0.5, 766),
            new ScrollMovement(0, 0.5, 783),
            new ScrollMovement(1, 0.5, 800),
            new ScrollMovement(0, 0.4, 817),
            new ScrollMovement(0, 0.4, 834),
            new ScrollMovement(1, 0.4, 849),
            new ScrollMovement(0, 0.30000000000000004, 865),
            new ScrollMovement(0, 0.30000000000000004, 883),
            new ScrollMovement(1, 0.30000000000000004, 899),
            new ScrollMovement(0, 0.30000000000000004, 916),
            new ScrollMovement(0, 0.2, 933),
            new ScrollMovement(0, 0.2, 949),
            new ScrollMovement(1, 0.2, 966),
            new ScrollMovement(0, 0.2, 983),
            new ScrollMovement(0, 0.1, 998),
            new ScrollMovement(0, 0.1, 1017),
            new ScrollMovement(0, 0.1, 1033),
            new ScrollMovement(0, 0.1, 1049),
            new ScrollMovement(0, 0.1, 1067),
            new ScrollMovement(0, 0.1, 1100),
            new ScrollMovement(0, 0.1, 1117),
            new ScrollMovement(1, 0.1, 1134)
    };

    public ScrollingButtonPanelTest() {
        JButton simulateSwipeButton = new JButton("Simulate Swipe");
        simulateSwipeButton.setToolTipText("Simulate scrolling as if swiping two fingers across a touchpad.");
        simulateSwipeButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                long startTime = System.currentTimeMillis();
                for (ScrollMovement m : swipeMovements) {
                    MouseWheelEvent event = new MouseWheelEvent(
                            scrollPane,
                            MouseWheelEvent.MOUSE_WHEEL,
                            m.when + startTime, 0,
                            50, 50, 50, 50, 0, false, MouseWheelEvent.WHEEL_UNIT_SCROLL, 1,
                            m.wheelRotation,
                            m.preciseWheelRotation);
                    Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(event);
                }

                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        SwingUtilities.invokeLater(new Runnable() {
                            @Override
                            public void run() {
                                System.out.println("Swipe simulation took " + (System.currentTimeMillis() - startTime) + " ms");
                            }
                        });
                    }
                });
            }
        });

        scrollPane = new JScrollPane(scrollPaneContent);
        for (int a = 1; a <= NUMBER_OF_CHECKBOXES; a++) {
            JCheckBox checkbox = new JCheckBox("Checkbox " + a);
            scrollPaneContent.add(checkbox);
            checkbox.putClientProperty(PROPERTY_ORIGINAL_ANCESTOR_LISTENERS, checkbox.getAncestorListeners());
        }
        scrollPane.setPreferredSize(new Dimension(800, 400));

        setLayout(new BorderLayout());
        add(scrollPane, BorderLayout.CENTER);
        add(simulateSwipeButton, BorderLayout.NORTH);
        add(includeAncestorListenerButton, BorderLayout.SOUTH);

        includeAncestorListenerButton.setToolTipText("Toggling off AncestorListeners resolves the performance complaint demonstrated here.");
        includeAncestorListenerButton.addActionListener(e -> {
            for (Component c : scrollPaneContent.getComponents()) {
                JComponent jc = (JComponent) c;
                AncestorListener[] listeners = (AncestorListener[]) jc.getClientProperty(PROPERTY_ORIGINAL_ANCESTOR_LISTENERS);
                for (AncestorListener listener : listeners) {
                    if (includeAncestorListenerButton.isSelected()) {
                        jc.addAncestorListener(listener);
                    } else {
                        jc.removeAncestorListener(listener);
                    }
                }
            }
        });
    }
}

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

CUSTOMER SUBMITTED WORKAROUND :
There is a checkbox at the bottom of the demo that removes AncestorListeners; this speeds up the demo so it performs satisfactorily.

FREQUENCY : always



Comments
Even though it is no longer an issue, is the code still doing something un-needed ? In which case a fix would still be merited, even if the impact is now small. If the other fix JDK-8342782 is actually how this SHOULD be fixed, then this would be a dup. instead of "not reproducible". So either way I don't think "not reproducible" is the right resolution.
29-01-2025

Submitter mentioned: I wrote a test to demonstrate how the code responsible for pulsing the button used to be very expensive (see test). It scrolls a large panel with 5000 JCheckBoxes, and performance varies wildly based on whether Aqua’s default button logic is used. When I first wrote it the output included: > The time it took by default was: 35923 > The time it took when suppressing AncestorListeners was: 112 However now having merged the recent changes for 8342782, this performance gap appears satisfactorily fixed: > The time it took by default was: 287 > The time it took when suppressing AncestorListeners was: 215
21-01-2025

Tested on M3 Tests: 23.0.1 - Fail 21.0.5 - Fail 17.0.13 - Fail 11.0.25 - Fail 8u431 - Fail 8u381 - Fail All tests were over 1500ms When "Include AncestorListeners" is deselected the tests were around 100ms
21-11-2024