-
Bug
-
Resolution: Unresolved
-
P4
-
8, 11, 17, 23, 24
-
generic
-
generic
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
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