IndexOutOfBoundsException in Parent::updateCachedBounds when visibility changes

Christopher Schnick crschnick at xpipe.io
Mon Mar 24 15:21:57 UTC 2025


Hello,

We encountered an issue after updating our application implementation to 
frequently change the visibility of nodes. We are essentially now 
running an implementation that very frequently changes the visibility of 
various children nodes based on when they are needed and shown. When the 
user performs a lot of actions, the visibility of many nodes will be 
changed rapidly.

For that, there are many listeners in place that listen for bounds 
changes of nodes to recheck whether they need to be made visible or not. 
All the visibility changes are queued up, so they are not immediately 
done in the listener after any bounds changes of parents. They are all 
properly done on the platform thread with runLater. When this 
implementation is running on many client systems, we sometimes receive 
an error report with an exception that looks something like this:

java.lang.IndexOutOfBoundsException: Index -1 out of bounds for length 2
     at 
java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:100)
     at 
java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:106)
     at 
java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:302)
     at java.base/java.util.Objects.checkIndex(Objects.java:365)
     at java.base/java.util.ArrayList.get(ArrayList.java:428)
     at 
javafx.base at 25-ea/com.sun.javafx.collections.ObservableListWrapper.get(ObservableListWrapper.java:88)
     at 
javafx.base at 25-ea/com.sun.javafx.collections.VetoableListDecorator.get(VetoableListDecorator.java:326)
     at 
javafx.graphics at 25-ea/javafx.scene.Parent.updateCachedBounds(Parent.java:1769)
     at 
javafx.graphics at 25-ea/javafx.scene.Parent.recomputeBounds(Parent.java:1713)
     at 
javafx.graphics at 25-ea/javafx.scene.Parent.doComputeGeomBounds(Parent.java:1566)
     at 
javafx.graphics at 25-ea/javafx.scene.Parent$1.doComputeGeomBounds(Parent.java:116)
     at 
javafx.graphics at 25-ea/com.sun.javafx.scene.ParentHelper.computeGeomBoundsImpl(ParentHelper.java:84)
     at 
javafx.graphics at 25-ea/com.sun.javafx.scene.layout.RegionHelper.superComputeGeomBoundsImpl(RegionHelper.java:78)
     at 
javafx.graphics at 25-ea/com.sun.javafx.scene.layout.RegionHelper.superComputeGeomBounds(RegionHelper.java:62)
     at 
javafx.graphics at 25-ea/javafx.scene.layout.Region.doComputeGeomBounds(Region.java:3301)
     at 
javafx.graphics at 25-ea/javafx.scene.layout.Region$1.doComputeGeomBounds(Region.java:166)
     at 
javafx.graphics at 25-ea/com.sun.javafx.scene.layout.RegionHelper.computeGeomBoundsImpl(RegionHelper.java:89)
     at 
javafx.graphics at 25-ea/com.sun.javafx.scene.NodeHelper.computeGeomBounds(NodeHelper.java:101)
     at 
javafx.graphics at 25-ea/javafx.scene.Node.updateGeomBounds(Node.java:3908)
     at 
javafx.graphics at 25-ea/javafx.scene.Node.getGeomBounds(Node.java:3870)
     at 
javafx.graphics at 25-ea/javafx.scene.Node.getLocalBounds(Node.java:3818)
     at 
javafx.graphics at 25-ea/javafx.scene.Node.updateTxBounds(Node.java:3972)
     at 
javafx.graphics at 25-ea/javafx.scene.Node.getTransformedBounds(Node.java:3764)
     at javafx.graphics at 25-ea/javafx.scene.Node.updateBounds(Node.java:828)
     at 
javafx.graphics at 25-ea/javafx.scene.Parent.updateBounds(Parent.java:1900)
     at 
javafx.graphics at 25-ea/javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2670)
     at 
javafx.graphics at 25-ea/com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:380)
     at 
javafx.graphics at 25-ea/com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:401)
     at 
javafx.graphics at 25-ea/com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:592)
     at 
javafx.graphics at 25-ea/com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:572)
     at 
javafx.graphics at 25-ea/com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:565)
     at 
javafx.graphics at 25-ea/com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$6(QuantumToolkit.java:346)
     at 
javafx.graphics at 25-ea/com.sun.glass.ui.InvokeLaterDispatcher$Future.run$$$capture(InvokeLaterDispatcher.java:95)
     at 
javafx.graphics at 25-ea/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java)

The index out of bounds is not always the same, there are various 
variations of this. It happens on all operating systems. It seems like 
there is a very specific scenario where an index can be out of bounds. 
This happens very rarely, like only a few times out of some hundred 
application runs, so I tried my best at forcing it to reproduce.

The following reproducer works most of the time, but it might have to be 
run multiple times. I am aware that it eventually results in a 
StackOverflow, but that was the best way to force it reliably, by just 
continuously spamming visibility changes to eventually encounter this 
rare issue. But I want to emphasize that the same error also occurs 
naturally when not being forced like this, but it is just a lot more 
rare. So the StackOverflow in the reproducer has nothing to do with this 
issue, it also happens later on.

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.io.IOException;

public class ParentBoundsBugextends Application {

     @Override public void start(Stage stage)throws IOException {
         Scene scene =new Scene(createContent(),640,480);
         stage.setScene(scene);
         stage.show();
         stage.centerOnScreen();
     }

     private RegioncreateContent() {
         var b1 =new Button("Click me!");
         var b2 =new Button("Click me!");
         var vbox =new VBox(b1, b2);
         b1.boundsInParentProperty().addListener((observable, oldValue, newValue) -> {
             vbox.setVisible(!vbox.isVisible());
         });
         b2.boundsInParentProperty().addListener((observable, oldValue, newValue) -> {
             vbox.setVisible(!vbox.isVisible());
         });
         vbox.boundsInParentProperty().addListener((observable, oldValue, newValue) -> {
             vbox.setVisible(!vbox.isVisible());
         });

         var stack =new StackPane(vbox,new StackPane());
         stack.boundsInParentProperty().addListener((observable, oldValue, newValue) -> {
             vbox.setVisible(!vbox.isVisible());
         });
         return stack;
     }

     public static void main(String[] args) {
         launch();
     }
}


It doesn't necessarily have something to do with running the visibility 
change directly in the listener, our application does a runLater to 
change the visibility state, still with the same results. To properly 
debug this, you will have to launch the reproducer with a bigger stack 
size like -Xss8m to increase the chance that it occurs. Then, you can 
just set a breakpoint at jdk.internal.util.Preconditions:302, and wait 
for it to trigger the OOB eventually.

This problem is currently the biggest JavaFX issue for us as it breaks 
the layout and usually requires a restart to fix.

Looking at the bounds calculation code, the list index bounds check is 
very optimistic in that it doesn't check any indices and relies on 
multiple assumtions to hold. So if it is very difficult to find the 
cause, a simple index bounds check for the list access would also work fine.

Best
Christopher Schnick
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/openjfx-dev/attachments/20250324/22887cd5/attachment-0001.htm>


More information about the openjfx-dev mailing list