<div dir="ltr"><ol style="margin:0px;list-style:none;padding:0px;color:rgb(23,43,77);font-family:"DejaVu Sans",sans-serif;font-size:14px">Hi Everyone,<br><br>We've also been experiencing this problem over the years. It seems to be related to JDK-8198577.<br><br>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.<br><br>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.<br><br>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.<br><br>// this checks the newly added nodes first, so if dirtyNodes is the<br>// whole children list, we can end early<br>for (int i = dirtyNodes.size() - 1; remainingDirtyNodes > 0; --i) {<br> final Node node = dirtyNodes.get(i);<br> if (node.boundsChanged) {<br> // assert node.isVisible();<br> node.boundsChanged = false;<br> --remainingDirtyNodes;<br> tmp = getChildTransformedBounds(node, BaseTransform.IDENTITY_TRANSFORM, tmp);<br><br>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.<br><br>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)."<br><br>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.<br><br> protected static void checkSpecialException(Throwable t) {<br> if (t instanceof IndexOutOfBoundsException) {<br> fixIndexOutOfBounds(t);<br> }<br> }<br><br> public static void fixIndexOutOfBounds(Throwable throwable) {<br> FXUtilities.log(EmbraceDesktop.class, <a href="http://org.slf4j.event.Level.INFO">org.slf4j.event.Level.INFO</a>, "Fixing IOB Issue");<br> try {<br> Field dirtyChildrenCountField = Parent.class.getDeclaredField("dirtyChildrenCount");<br> dirtyChildrenCountField.setAccessible(true);<br> Field dirtyChildrenField = Parent.class.getDeclaredField("dirtyChildren");<br> dirtyChildrenField.setAccessible(true);<br> Set<Scene> apps = applicationManager.getApplications();<br> ArrayList<Node> brokenStack = new ArrayList<>();<br> for (Scene s: apps) {<br> fixTreeRecursive(dirtyChildrenCountField, dirtyChildrenField, s.getRoot(), brokenStack);<br> }<br> if (brokenStack.size() > 0) {<br> StringBuilder errorStack = new StringBuilder();<br> for (Node n: brokenStack) {<br> errorStack.append(n.getClass().getSimpleName() + " " + String.join( ",", n.getStyleClass())).append("\n");<br> }<br> EmbraceAnalytics.logCrash("Index out of bounds crash",errorStack.toString(), throwable);<br> }<br><br><br> }<br> catch (Throwable t2) {<br> FXUtilities.log(EmbraceDesktop.class, org.slf4j.event.Level.ERROR, "Exception while fixing tree", t2);<br> }<br> }<br><br> protected static boolean fixTreeRecursive(Field dirtyChildrenCountField, Field dirtyChildrenField, Parent parent, ArrayList<Node> brokenStack) throws IllegalAccessException {<br> List<?> dirtyChildren = (List<?>) dirtyChildrenField.get(parent);<br> int dirtyChildrenCount = (int) dirtyChildrenCountField.get(parent);<br> if (dirtyChildren != null) {<br> if (dirtyChildrenCount > dirtyChildren.size()) {<br> FXUtilities.log(EmbraceDesktop.class, org.slf4j.event.Level.ERROR, "Offending node1 was " + parent.getClass().getSimpleName());<br> dirtyChildrenCountField.set(parent, dirtyChildren.size());<br> brokenStack.add(parent);<br> return true;<br> }<br> }<br> else {<br> if (parent.getChildrenUnmodifiable().size() < dirtyChildrenCount) {<br> FXUtilities.log(EmbraceDesktop.class, org.slf4j.event.Level.ERROR, "Offending node2 was " + parent.getClass().getSimpleName());<br> dirtyChildrenCountField.set(parent, parent.getChildrenUnmodifiable().size());<br> brokenStack.add(parent);<br> return true;<br> }<br> }<br> for (Node n: parent.getChildrenUnmodifiable()) {<br> if (n instanceof Parent) {<br> boolean error = fixTreeRecursive(dirtyChildrenCountField, dirtyChildrenField, (Parent)n, brokenStack);<br> if (error) {<br> brokenStack.add(parent);<br> FXUtilities.log(EmbraceDesktop.class, org.slf4j.event.Level.ERROR, "Parent was " + parent.getClass().getSimpleName());<br> }<br> return error;<br> }<br> }<br> return false;<br> }<br><br>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.<br><br>I don't know if anyone else has experienced this issue and has insights/workarounds?<br><br>Dean<br></ol></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Mon, Mar 24, 2025 at 5:22 PM Christopher Schnick <<a href="mailto:crschnick@xpipe.io" target="_blank">crschnick@xpipe.io</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><u></u>
<div>
<p>Hello,<br>
<br>
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. <br>
<br>
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:<br>
<br>
java.lang.IndexOutOfBoundsException: Index -1 out of bounds for
length 2<br>
at
java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:100)<br>
at
java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:106)<br>
at
java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:302)<br>
at java.base/java.util.Objects.checkIndex(Objects.java:365)<br>
at java.base/java.util.ArrayList.get(ArrayList.java:428)<br>
at
<a href="mailto:javafx.base@25-ea/com.sun.javafx.collections.ObservableListWrapper.get" target="_blank">javafx.base@25-ea/com.sun.javafx.collections.ObservableListWrapper.get</a>(ObservableListWrapper.java:88)<br>
at
<a href="mailto:javafx.base@25-ea/com.sun.javafx.collections.VetoableListDecorator.get" target="_blank">javafx.base@25-ea/com.sun.javafx.collections.VetoableListDecorator.get</a>(VetoableListDecorator.java:326)<br>
at
<a href="mailto:javafx.graphics@25-ea/javafx.scene.Parent.updateCachedBounds" target="_blank">javafx.graphics@25-ea/javafx.scene.Parent.updateCachedBounds</a>(Parent.java:1769)<br>
at
<a href="mailto:javafx.graphics@25-ea/javafx.scene.Parent.recomputeBounds" target="_blank">javafx.graphics@25-ea/javafx.scene.Parent.recomputeBounds</a>(Parent.java:1713)<br>
at
<a href="mailto:javafx.graphics@25-ea/javafx.scene.Parent.doComputeGeomBounds" target="_blank">javafx.graphics@25-ea/javafx.scene.Parent.doComputeGeomBounds</a>(Parent.java:1566)<br>
at
<a href="mailto:javafx.graphics@25-ea/javafx.scene.Parent$1.doComputeGeomBounds" target="_blank">javafx.graphics@25-ea/javafx.scene.Parent$1.doComputeGeomBounds</a>(Parent.java:116)<br>
at
<a href="mailto:javafx.graphics@25-ea/com.sun.javafx.scene.ParentHelper.computeGeomBoundsImpl" target="_blank">javafx.graphics@25-ea/com.sun.javafx.scene.ParentHelper.computeGeomBoundsImpl</a>(ParentHelper.java:84)<br>
at
<a href="mailto:javafx.graphics@25-ea/com.sun.javafx.scene.layout.RegionHelper.superComputeGeomBoundsImpl" target="_blank">javafx.graphics@25-ea/com.sun.javafx.scene.layout.RegionHelper.superComputeGeomBoundsImpl</a>(RegionHelper.java:78)<br>
at
<a href="mailto:javafx.graphics@25-ea/com.sun.javafx.scene.layout.RegionHelper.superComputeGeomBounds" target="_blank">javafx.graphics@25-ea/com.sun.javafx.scene.layout.RegionHelper.superComputeGeomBounds</a>(RegionHelper.java:62)<br>
at
<a href="mailto:javafx.graphics@25-ea/javafx.scene.layout.Region.doComputeGeomBounds" target="_blank">javafx.graphics@25-ea/javafx.scene.layout.Region.doComputeGeomBounds</a>(Region.java:3301)<br>
at
<a href="mailto:javafx.graphics@25-ea/javafx.scene.layout.Region$1.doComputeGeomBounds" target="_blank">javafx.graphics@25-ea/javafx.scene.layout.Region$1.doComputeGeomBounds</a>(Region.java:166)<br>
at
<a href="mailto:javafx.graphics@25-ea/com.sun.javafx.scene.layout.RegionHelper.computeGeomBoundsImpl" target="_blank">javafx.graphics@25-ea/com.sun.javafx.scene.layout.RegionHelper.computeGeomBoundsImpl</a>(RegionHelper.java:89)<br>
at
<a href="mailto:javafx.graphics@25-ea/com.sun.javafx.scene.NodeHelper.computeGeomBounds" target="_blank">javafx.graphics@25-ea/com.sun.javafx.scene.NodeHelper.computeGeomBounds</a>(NodeHelper.java:101)<br>
at
<a href="mailto:javafx.graphics@25-ea/javafx.scene.Node.updateGeomBounds" target="_blank">javafx.graphics@25-ea/javafx.scene.Node.updateGeomBounds</a>(Node.java:3908)<br>
at
<a href="mailto:javafx.graphics@25-ea/javafx.scene.Node.getGeomBounds" target="_blank">javafx.graphics@25-ea/javafx.scene.Node.getGeomBounds</a>(Node.java:3870)<br>
at
<a href="mailto:javafx.graphics@25-ea/javafx.scene.Node.getLocalBounds" target="_blank">javafx.graphics@25-ea/javafx.scene.Node.getLocalBounds</a>(Node.java:3818)<br>
at
<a href="mailto:javafx.graphics@25-ea/javafx.scene.Node.updateTxBounds" target="_blank">javafx.graphics@25-ea/javafx.scene.Node.updateTxBounds</a>(Node.java:3972)<br>
at
<a href="mailto:javafx.graphics@25-ea/javafx.scene.Node.getTransformedBounds" target="_blank">javafx.graphics@25-ea/javafx.scene.Node.getTransformedBounds</a>(Node.java:3764)<br>
at
<a href="mailto:javafx.graphics@25-ea/javafx.scene.Node.updateBounds" target="_blank">javafx.graphics@25-ea/javafx.scene.Node.updateBounds</a>(Node.java:828)<br>
at
<a href="mailto:javafx.graphics@25-ea/javafx.scene.Parent.updateBounds" target="_blank">javafx.graphics@25-ea/javafx.scene.Parent.updateBounds</a>(Parent.java:1900)<br>
at
<a href="mailto:javafx.graphics@25-ea/javafx.scene.Scene$ScenePulseListener.pulse" target="_blank">javafx.graphics@25-ea/javafx.scene.Scene$ScenePulseListener.pulse</a>(Scene.java:2670)<br>
at
<a href="mailto:javafx.graphics@25-ea/com.sun.javafx.tk.Toolkit.runPulse" target="_blank">javafx.graphics@25-ea/com.sun.javafx.tk.Toolkit.runPulse</a>(Toolkit.java:380)<br>
at
<a href="mailto:javafx.graphics@25-ea/com.sun.javafx.tk.Toolkit.firePulse" target="_blank">javafx.graphics@25-ea/com.sun.javafx.tk.Toolkit.firePulse</a>(Toolkit.java:401)<br>
at
<a href="mailto:javafx.graphics@25-ea/com.sun.javafx.tk.quantum.QuantumToolkit.pulse" target="_blank">javafx.graphics@25-ea/com.sun.javafx.tk.quantum.QuantumToolkit.pulse</a>(QuantumToolkit.java:592)<br>
at
<a href="mailto:javafx.graphics@25-ea/com.sun.javafx.tk.quantum.QuantumToolkit.pulse" target="_blank">javafx.graphics@25-ea/com.sun.javafx.tk.quantum.QuantumToolkit.pulse</a>(QuantumToolkit.java:572)<br>
at
<a href="mailto:javafx.graphics@25-ea/com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue" target="_blank">javafx.graphics@25-ea/com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue</a>(QuantumToolkit.java:565)<br>
at
<a href="mailto:javafx.graphics@25-ea/com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$6" target="_blank">javafx.graphics@25-ea/com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$6</a>(QuantumToolkit.java:346)<br>
at
<a href="mailto:javafx.graphics@25-ea/com.sun.glass.ui.InvokeLaterDispatcher$Future.run$$$capture" target="_blank">javafx.graphics@25-ea/com.sun.glass.ui.InvokeLaterDispatcher$Future.run$$$capture</a>(InvokeLaterDispatcher.java:95)<br>
at
<a href="mailto:javafx.graphics@25-ea/com.sun.glass.ui.InvokeLaterDispatcher$Future.run" target="_blank">javafx.graphics@25-ea/com.sun.glass.ui.InvokeLaterDispatcher$Future.run</a>(InvokeLaterDispatcher.java)<br>
<br>
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.<br>
<br>
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.</p>
<div style="background-color:rgb(30,31,34);color:rgb(188,190,196)">
<pre style="font-family:"JetBrains Mono",monospace"><span style="color:rgb(207,142,109)">import </span>javafx.application.Application;
<span style="color:rgb(207,142,109)">import </span>javafx.scene.Scene;
<span style="color:rgb(207,142,109)">import </span>javafx.scene.control.Button;
<span style="color:rgb(207,142,109)">import </span>javafx.scene.layout.Region;
<span style="color:rgb(207,142,109)">import </span>javafx.scene.layout.StackPane;
<span style="color:rgb(207,142,109)">import </span>javafx.scene.layout.VBox;
<span style="color:rgb(207,142,109)">import </span>javafx.stage.Stage;
<span style="color:rgb(207,142,109)">import </span>java.io.IOException;
<span style="color:rgb(207,142,109)">public class </span>ParentBoundsBug <span style="color:rgb(207,142,109)">extends </span>Application {
<span style="color:rgb(179,174,96)">@Override
</span><span style="color:rgb(179,174,96)"> </span><span style="color:rgb(207,142,109)">public void </span><span style="color:rgb(86,168,245)">start</span>(Stage stage) <span style="color:rgb(207,142,109)">throws </span>IOException {
Scene scene = <span style="color:rgb(207,142,109)">new </span>Scene(createContent(), <span style="color:rgb(42,172,184)">640</span>, <span style="color:rgb(42,172,184)">480</span>);
stage.setScene(scene);
stage.show();
stage.centerOnScreen();
}
<span style="color:rgb(207,142,109)">private </span>Region <span style="color:rgb(86,168,245)">createContent</span>() {
<span style="color:rgb(207,142,109)">var </span>b1 = <span style="color:rgb(207,142,109)">new </span>Button(<span style="color:rgb(106,171,115)">"Click me!"</span>);
<span style="color:rgb(207,142,109)">var </span>b2 = <span style="color:rgb(207,142,109)">new </span>Button(<span style="color:rgb(106,171,115)">"Click me!"</span>);
<span style="color:rgb(207,142,109)">var </span>vbox = <span style="color:rgb(207,142,109)">new </span>VBox(b1, b2);
b1.boundsInParentProperty().addListener((observable, oldValue, newValue) -> {
<span style="color:rgb(199,125,187)">vbox</span>.setVisible(!<span style="color:rgb(199,125,187)">vbox</span>.isVisible());
});
b2.boundsInParentProperty().addListener((observable, oldValue, newValue) -> {
<span style="color:rgb(199,125,187)">vbox</span>.setVisible(!<span style="color:rgb(199,125,187)">vbox</span>.isVisible());
});
vbox.boundsInParentProperty().addListener((observable, oldValue, newValue) -> {
<span style="color:rgb(199,125,187)">vbox</span>.setVisible(!<span style="color:rgb(199,125,187)">vbox</span>.isVisible());
});
<span style="color:rgb(207,142,109)">var </span>stack = <span style="color:rgb(207,142,109)">new </span>StackPane(vbox, <span style="color:rgb(207,142,109)">new </span>StackPane());
stack.boundsInParentProperty().addListener((observable, oldValue, newValue) -> {
<span style="color:rgb(199,125,187)">vbox</span>.setVisible(!<span style="color:rgb(199,125,187)">vbox</span>.isVisible());
});
<span style="color:rgb(207,142,109)">return </span>stack;
}
<span style="color:rgb(207,142,109)">public static void </span><span style="color:rgb(86,168,245)">main</span>(String[] args) {
<span style="font-style:italic">launch</span>();
}
}</pre>
</div>
<p></p>
<p><br>
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.<br>
<br>
This problem is currently the biggest JavaFX issue for us as it
breaks the layout and usually requires a restart to fix. <br>
<br>
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.</p>
<p>Best<br>
Christopher Schnick<br>
</p>
</div>
</blockquote></div>