IndexOutOfBoundsException in Parent::updateCachedBounds when visibility changes

Dean Wookey wookey.dean at gmail.com
Wed Jun 25 12:50:22 UTC 2025


Hi Christopher,

Thanks. I had to adapt your code to reuse listeners because I ran out of
memory. So far I haven't found anything, but will keep looking. At least
it's possible to give a version with this to select users which isn't
possible with the conditional breakpoint approach.

Dean

On Wed, Jun 25, 2025 at 2:37 PM Christopher Schnick <crschnick at xpipe.io>
wrote:

> (Resending this mail since it somehow didn't make it to the mailing list
> the first time)
>
> This issue vanished for us after reworking the application, including
> implementing more fixes for non-platform thread access. I can't say
> definitively whether this caused it, but there were some rare instances for
> us where some properties were changed from the wrong thread.
>
> As an easy solution to check the platform thread access for everything
> without having to implement explicit asserts everywhere, was a
> listener-based approach I implemented here:
> https://mail.openjdk.org/pipermail/openjfx-dev/2025-April/053212.html .
>
> It might still be possible that you can encounter this issue just using
> the platform thread as you explained. A better error handling of this
> situation in JavaFX would make this issue already less severe. A proper fix
> to prevent this from happening would be even better, but I have no idea how
> feasible this is.
>
>
> On 25/06/2025 11:06, Dean Wookey wrote:
>
>
>    Hi Everyone,
>
>    We've also been experiencing this problem over the years. It seems to
>    be related to JDK-8198577.
>
>    Once it goes wrong, each pulse hits the issue repeated meaning it can
>    never escape. It's rare, but extremely disruptive when it does occur
>    because the user loses what they've been working on and has to restart the
>    app.
>
>    I've tried really hard to figure out the conditions this happens in. I
>    don't think it's a multiple thread issue (although for some people it
>    almost certainly could be triggered that way) because we've put conditional
>    breakpoints that trigger whenever anything that could affect dirty children
>    is done off the app thread. We've got assert
>    Platform.isFXApplicationThread() all over our app to make sure the
>    threading is happening properly.
>
>    What I think is happening is that getChildTransformedBounds which is
>    being called inside the updateCachedBounds loop, can in some rare cases,
>    end up triggering a call to updateCachedBounds on the same node. Basically
>    updateCachedBounds can call itself recursively. This is a snipped from
>    Parent.java in updateCachedBounds.
>
>    // this checks the newly added nodes first, so if dirtyNodes is the
>    // whole children list, we can end early
>    for (int i = dirtyNodes.size() - 1; remainingDirtyNodes > 0; --i) {
>       final Node node = dirtyNodes.get(i);
>       if (node.boundsChanged) {
>           // assert node.isVisible();
>           node.boundsChanged = false;
>           --remainingDirtyNodes;
>           tmp = getChildTransformedBounds(node,
>    BaseTransform.IDENTITY_TRANSFORM, tmp);
>
>    In the code above, if this gets called recursively through
>    getChildTransformedBounds, then node.boundsChanged will change to false for
>    all the nodes which stops remainingDirtyNodes from being updated and i
>    eventually goes negative.
>
>    We tried to fix the scene graph when this happens by catching the
>    exception in the Thread.setDefaultUncaughtExceptionHandler but it didn't
>    work. Maybe Christopher's suggested fix would work, but as Kevin says "It
>    needs to be tested to ensure that when we get the AIOOBE that we can
>    recover. It wouldn't solve anything if we catch and log that exception only
>    to have it fail shortly after because the scene graph isn't in a good state
>    (I don't know whether that would be the case, but it's something that needs
>    to be checked)."
>
>    Here's how we tried to fix the scene graph when we caught the error.
>    The "Fixing IOB Issue" log gets hit all the time, but it doesn't find any
>    problems, and in the next pulse it hits the problem again with various
>    different stack traces until it settles on one. In our latest example of
>    the error, it first occurred during a Platform.runLater and not during the
>    pulse, but then all subsequent issues happen during the pulse.
>
>        protected static void checkSpecialException(Throwable t) {
>            if (t instanceof IndexOutOfBoundsException) {
>                fixIndexOutOfBounds(t);
>            }
>        }
>
>        public static void fixIndexOutOfBounds(Throwable throwable) {
>            FXUtilities.log(EmbraceDesktop.class,
>    org.slf4j.event.Level.INFO, "Fixing IOB Issue");
>            try {
>                Field dirtyChildrenCountField =
>    Parent.class.getDeclaredField("dirtyChildrenCount");
>                dirtyChildrenCountField.setAccessible(true);
>                Field dirtyChildrenField =
>    Parent.class.getDeclaredField("dirtyChildren");
>                dirtyChildrenField.setAccessible(true);
>                Set<Scene> apps = applicationManager.getApplications();
>                ArrayList<Node> brokenStack = new ArrayList<>();
>                for (Scene s: apps) {
>                    fixTreeRecursive(dirtyChildrenCountField,
>    dirtyChildrenField, s.getRoot(), brokenStack);
>                }
>                if (brokenStack.size() > 0) {
>                    StringBuilder errorStack = new StringBuilder();
>                    for (Node n: brokenStack) {
>                        errorStack.append(n.getClass().getSimpleName() + "
>    " + String.join( ",", n.getStyleClass())).append("\n");
>                    }
>                    EmbraceAnalytics.logCrash("Index out of bounds
>    crash",errorStack.toString(), throwable);
>                }
>
>
>            }
>            catch (Throwable t2) {
>                FXUtilities.log(EmbraceDesktop.class,
>    org.slf4j.event.Level.ERROR, "Exception while fixing tree", t2);
>            }
>        }
>
>        protected static boolean fixTreeRecursive(Field
>    dirtyChildrenCountField, Field dirtyChildrenField, Parent parent,
>    ArrayList<Node> brokenStack) throws IllegalAccessException {
>            List<?> dirtyChildren = (List<?>)
>    dirtyChildrenField.get(parent);
>            int dirtyChildrenCount = (int)
>    dirtyChildrenCountField.get(parent);
>            if (dirtyChildren != null) {
>                if (dirtyChildrenCount > dirtyChildren.size()) {
>                    FXUtilities.log(EmbraceDesktop.class,
>    org.slf4j.event.Level.ERROR, "Offending node1 was " +
>    parent.getClass().getSimpleName());
>                    dirtyChildrenCountField.set(parent,
>    dirtyChildren.size());
>                    brokenStack.add(parent);
>                    return true;
>                }
>            }
>            else {
>                if (parent.getChildrenUnmodifiable().size() <
>    dirtyChildrenCount) {
>                    FXUtilities.log(EmbraceDesktop.class,
>    org.slf4j.event.Level.ERROR, "Offending node2 was " +
>    parent.getClass().getSimpleName());
>                    dirtyChildrenCountField.set(parent,
>    parent.getChildrenUnmodifiable().size());
>                    brokenStack.add(parent);
>                    return true;
>                }
>            }
>            for (Node n: parent.getChildrenUnmodifiable()) {
>                if (n instanceof Parent) {
>                    boolean error =
>    fixTreeRecursive(dirtyChildrenCountField, dirtyChildrenField, (Parent)n,
>    brokenStack);
>                    if (error) {
>                        brokenStack.add(parent);
>                        FXUtilities.log(EmbraceDesktop.class,
>    org.slf4j.event.Level.ERROR, "Parent was " +
>    parent.getClass().getSimpleName());
>                    }
>                    return error;
>                }
>            }
>            return false;
>        }
>
>    I think we should we should put the index check potential fix in and
>    log when it happens. As far as we can tell, if this issue gets hit, it's
>    catastrophic 100% of the time. The fix might resolve the issue. It can't
>    really make it any worse. Another thing we should do is add a check for
>    recursive entry to that method and log when that occurs. That's (I think)
>    the real issue, and without a stack trace of that, it's hard to find the
>    root cause.
>
>    I don't know if anyone else has experienced this issue and has
>    insights/workarounds?
>
>    Dean
>
>
> On Mon, Mar 24, 2025 at 5:22 PM Christopher Schnick <crschnick at xpipe.io>
> wrote:
>
>> 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 ParentBoundsBug extends Application {
>>
>>     @Override    public void start(Stage stage) throws IOException {
>>         Scene scene = new Scene(createContent(), 640, 480);
>>         stage.setScene(scene);
>>         stage.show();
>>         stage.centerOnScreen();
>>     }
>>
>>     private Region createContent() {
>>         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/20250625/1b9c99ec/attachment-0001.htm>


More information about the openjfx-dev mailing list