Accidentally reproduced NPE in synchronizeNodes in combination with enterNestedEventLoop
John Hendrikx
john.hendrikx at gmail.com
Thu Aug 22 03:24:49 UTC 2024
Hi List,
This is a bit of a long post. I'm mainly wondering if I did something
wrong that FX should detect early, or if I'm doing nothing unusual and
FX should handle the case described below correctly.
I encountered the bug where an NPE occurs in
Scene$ScenePulseListener#synchronizeNodes, and it is reproducable at the
moment. I'm not sure it is the same one others sometimes see, but the
version I encountered is prefaced with a failing assert (which may
easily get lost as synchronizeNodes will spam NPE's in your log, as it
doesn't recover).
In Parent#validatePG it prints:
*** pgnodes.size validatePG() [1] != children.size() [2]
And then throws an AssertionError with a long stacktrace.
java.lang.AssertionError: validation of PGGroup children failed
(stack trace omitted)
Immediately after this, the NPE in synchronizeNodes starts to get
spammed, and the application never recovers.
This seems to have something to do with nested event loops, as I
introduced a new one in the code involved. When I remove the nested
event loop, there is no problem (it also initially works fine when the
application is just starting, only in a different situation, when there
is some user interaction via a keypress, does the bug trigger).
The nested event loop is entered from a ChangeListener, which may be a
bit unusual. The documentation of Platform#enterNestedEventLoop says:
* This method must either be called from an input event handler or
* from the run method of a Runnable passed to
* {@link javafx.application.Platform#runLater Platform.runLater}.
* It must not be called during animation or layout processing.
It is also documented to throw an IllegalStateException if used
incorrectly. That is however not happening, so I guess I'm using it
correctly...? On the other hand, I'm not in an input event
handler.... the whole process is triggered by a keypress though, and
deep down in the AssertionError trace you can see:
at
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
at javafx.event.Event.fireEvent(Event.java:198)
at javafx.scene.Scene$KeyHandler.process(Scene.java:4113)
at javafx.scene.Scene.processKeyEvent(Scene.java:2159)
at javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:2627)
at
com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:218)
So IMHO technically I am in an input event handler...?
Now the code does a LOT of stuff, which is going to make this tough to
analyze. In short:
- I'm displaying a Scene
- The user indicates (with a keypress) they want to go to a different Pane
- The previous Pane is created, and added to the scene as a child to a
TransitionPane that will cross-fade between the current Pane and the new
Pane
- As soon as the new Pane becomes part of the Scene, lots of things get
triggered because the Scene property goes from null to the active Scene:
at javafx.scene.Node.invalidatedScenes(Node.java:1075)
at javafx.scene.Node.setScenes(Node.java:1142) <-- the new Pane
will get a Scene assigned to it
at javafx.scene.Parent$2.onChanged(Parent.java:372) <-- the new
Pane has its Parent changed to the TransitionPane
- Several parts of the new Pane listen (directly or indirectly) to a
change in Scene, as they only become "active" when the Node involved is
displayed (this happens with a Scene listener)
- One of those things is some code that will create and populate a
ListView; this code uses a relatively fast query, but I had marked it as
something that should be done in the background as it sometimes does
take a bit too much time on the FX thread
Now, how I normally do things in the background is to display an
automatically closing "Please wait" dialog potentially with a progress
bar on it (this dialog is actually invisible, unless sufficient time
passes, so in 99% of the cases when things respond fast, the user never
sees it, even though it was created and is there). This involves
starting a nested event loop. This works marvelously throughout this
application (and has done so for years), and it is used for almost every
transition from one part of the application to the next. In all cases
so far however I did this directly from an event handler.
So the main difference is that I'm trying to enter a nested event loop
from a ChangeListener (which deep down was triggered by an Event). In
the AssertionError stack trace (which I will include at the end), there
is no layout or animation code **before** entering the nested loop,
although there is some layout code **after** it was entered.
I can live with the fact that I may be using enterNestedEventLoop
incorrectly here, but in that case it should probably also detect this
incorrect use and throw the IllegalStateException.
Technically, all this code is triggered while "adding" a Child to the
TransitionPane, so I suspect that is what the AssertionError is about
(it indicates the child count 1 != 2, which is what TransitionPane has,
one active pane, and just added to cross fade to). Still, is this
really incorrect usage?
I've included an annotated stack trace below.
As it is quite reproducable, I can debug this further by adding
breakpoints/prints -- I'm just unsure where to start looking.
--John
java.lang.AssertionError: validation of PGGroup children failed
at javafx.scene.Parent.validatePG(Parent.java:243)
at javafx.scene.Parent.doUpdatePeer(Parent.java:201)
at javafx.scene.Parent$1.doUpdatePeer(Parent.java:109)
at com.sun.javafx.scene.ParentHelper.updatePeerImpl(ParentHelper.java:78)
at
com.sun.javafx.scene.layout.RegionHelper.updatePeerImpl(RegionHelper.java:72)
at com.sun.javafx.scene.NodeHelper.updatePeer(NodeHelper.java:104)
at javafx.scene.Node.syncPeer(Node.java:721)
at
javafx.scene.Scene$ScenePulseListener.synchronizeSceneNodes(Scene.java:2396)
at javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2542)
at com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:407)
at
java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:406)
at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:436)
at
com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:575)
at
com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:555)
at
com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:548)
at
com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$11(QuantumToolkit.java:352)
at
com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
at
com.sun.glass.ui.win.WinApplication._enterNestedEventLoopImpl(Native Method)
at
com.sun.glass.ui.win.WinApplication._enterNestedEventLoop(WinApplication.java:211)
at
com.sun.glass.ui.Application.enterNestedEventLoop(Application.java:515)
at com.sun.glass.ui.EventLoop.enter(EventLoop.java:107)
at
com.sun.javafx.tk.quantum.QuantumToolkit.enterNestedEventLoop(QuantumToolkit.java:631)
at javafx.application.Platform.enterNestedEventLoop(Platform.java:301)
at
hs.mediasystem.util.javafx.SceneUtil.enterNestedEventLoop(SceneUtil.java:75)
<-- just my wrapper to trace slow calls, it delegates to Platform
at hs.mediasystem.runner.dialog.DialogPane.showDialog(DialogPane.java:78)
at
hs.mediasystem.runner.dialog.Dialogs.showProgressDialog(Dialogs.java:163)
at hs.mediasystem.runner.dialog.Dialogs.runNested(Dialogs.java:103)
at
hs.mediasystem.plugin.home.HomeScreenNodeFactory.lambda$1(HomeScreenNodeFactory.java:123)
The above line is where the nested event loop is entered. It starts to
display a "busy" dialog. A background task (on a new thread) will create
a ListView (this never actually happens, the AssertionError is thrown
immediately even with an empty task that just sleeps 10 seconds).
at
com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:360)
at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
at
javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:80)
at
javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)
at
javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:118)
at
javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:172)
at
javafx.scene.control.SelectionModel.setSelectedItem(SelectionModel.java:105)
at
javafx.scene.control.MultipleSelectionModelBase.lambda$new$0(MultipleSelectionModelBase.java:67)
at
com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:348)
at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
at
javafx.beans.property.ReadOnlyIntegerPropertyBase.fireValueChangedEvent(ReadOnlyIntegerPropertyBase.java:78)
at
javafx.beans.property.ReadOnlyIntegerWrapper.fireValueChangedEvent(ReadOnlyIntegerWrapper.java:102)
at
javafx.beans.property.IntegerPropertyBase.markInvalid(IntegerPropertyBase.java:114)
at
javafx.beans.property.IntegerPropertyBase.set(IntegerPropertyBase.java:148)
at
javafx.scene.control.SelectionModel.setSelectedIndex(SelectionModel.java:69)
at
javafx.scene.control.MultipleSelectionModelBase.select(MultipleSelectionModelBase.java:424)
at
javafx.scene.control.MultipleSelectionModelBase.select(MultipleSelectionModelBase.java:456)
at
hs.mediasystem.plugin.home.HomeScreenNodeFactory.lambda$3(HomeScreenNodeFactory.java:176)
<-- triggers the creation of a ListView
at javafx.beans.value.ObservableValue.lambda$0(ObservableValue.java:364)
at
com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)
at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
at javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)
at
com.sun.javafx.binding.ConditionalBinding.conditionChanged(ConditionalBinding.java:53)
at com.sun.javafx.binding.Subscription.lambda$2(Subscription.java:63)
at
com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)
at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
at javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)
at com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)
at
com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)
at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
at javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)
at com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)
at
com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)
at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
at javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)
at
com.sun.javafx.binding.FlatMappedBinding.invalidateAll(FlatMappedBinding.java:102)
at com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)
at
com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)
at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
at javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)
at
com.sun.javafx.binding.FlatMappedBinding.invalidateAll(FlatMappedBinding.java:102)
at com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)
at
com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:348)
at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
at
javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:80)
at
javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)
at
javafx.scene.Node$ReadOnlyObjectWrapperManualFire.fireSuperValueChangedEvent(Node.java:1053)
<-- there is a listener on a Scene property
at javafx.scene.Node.invalidatedScenes(Node.java:1104)
at javafx.scene.Node.setScenes(Node.java:1142)
at javafx.scene.Parent.scenesChanged(Parent.java:772) <-- the new
Pane has a few nested Panes, so you see multiple Parent assignments
at javafx.scene.Node.invalidatedScenes(Node.java:1075)
at javafx.scene.Node.setScenes(Node.java:1142)
at javafx.scene.Parent.scenesChanged(Parent.java:772)
at javafx.scene.Node.invalidatedScenes(Node.java:1075)
at javafx.scene.Node.setScenes(Node.java:1142) <-- the
Parent is part of a Scene, so this new Pane also gets the same Scene
assigned
at javafx.scene.Parent$2.onChanged(Parent.java:372) <-- the new
Pane gets its parent assigned (TransitionPane)
at
com.sun.javafx.collections.TrackableObservableList.lambda$new$0(TrackableObservableList.java:45)
at
com.sun.javafx.collections.ListListenerHelper$Generic.fireValueChangedEvent(ListListenerHelper.java:329)
at
com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(ListListenerHelper.java:73)
at
javafx.collections.ObservableListBase.fireChange(ObservableListBase.java:239)
at
javafx.collections.ListChangeBuilder.commit(ListChangeBuilder.java:482)
at
javafx.collections.ListChangeBuilder.endChange(ListChangeBuilder.java:541)
at
javafx.collections.ObservableListBase.endChange(ObservableListBase.java:211)
at
javafx.collections.ModifiableObservableListBase.add(ModifiableObservableListBase.java:162)
at
com.sun.javafx.collections.VetoableListDecorator.add(VetoableListDecorator.java:319)
at
hs.mediasystem.util.javafx.ui.transition.TransitionPane.add(TransitionPane.java:94)
<-- new Pane is added to a TransitionPane that handles cross fade
between old and new Pane
at
hs.mediasystem.util.javafx.ui.transition.TransitionPane.addAtStart(TransitionPane.java:105)
at
hs.mediasystem.util.javafx.ui.transition.TransitionPane.add(TransitionPane.java:113)
at
hs.mediasystem.runner.presentation.ViewPort.updateChildNode(ViewPort.java:68)
<-- here it installs the new Pane to display
at hs.mediasystem.runner.presentation.ViewPort.lambda$0(ViewPort.java:39)
at
com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)
at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
at javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)
at com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)
at
com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)
at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
at
javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:111)
at
javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:118)
at
javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:172)
at
hs.mediasystem.presentation.ParentPresentation.navigateBack(ParentPresentation.java:39)
<-- this handles the keyPress (it was a "back" press)
at hs.mediasystem.util.expose.Expose.lambda$3(Expose.java:55)
at hs.mediasystem.util.expose.Trigger$1.run(Trigger.java:58)
at
hs.mediasystem.runner.RootPresentationHandler.tryRunAction(RootPresentationHandler.java:106)
at
hs.mediasystem.runner.RootPresentationHandler.handleActionEvent(RootPresentationHandler.java:81)
at
com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:247)
at
com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
at
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:234)
at
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
at
com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
at
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
at hs.mediasystem.util.javafx.SceneUtil.lambda$1(SceneUtil.java:101)
<-- this is just my Slow Event detection, and just delegates the event
at
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49)
at hs.mediasystem.util.javafx.base.Events.dispatchEvent(Events.java:35)
at
hs.mediasystem.runner.action.InputActionHandler.handleKeyEvent(InputActionHandler.java:153)
<-- just code that handles a keypress that bubbled all the way to the top
at
hs.mediasystem.runner.action.InputActionHandler.onKeyPressed(InputActionHandler.java:138)
at
com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)
at
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:234)
at
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
at
com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
at
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
at hs.mediasystem.util.javafx.SceneUtil.lambda$1(SceneUtil.java:101)
<-- this is just my Slow Event detection, and just delegates the event
at
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
at javafx.event.Event.fireEvent(Event.java:198)
at javafx.scene.Scene$KeyHandler.process(Scene.java:4113)
at javafx.scene.Scene.processKeyEvent(Scene.java:2159)
at javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:2627)
at
com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:218)
at
com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:150)
at
java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
at
com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleKeyEvent$1(GlassViewEventHandler.java:250)
at
com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:424)
at
com.sun.javafx.tk.quantum.GlassViewEventHandler.handleKeyEvent(GlassViewEventHandler.java:249)
at com.sun.glass.ui.View.handleKeyEvent(View.java:542)
at com.sun.glass.ui.View.notifyKey(View.java:966)
at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at
com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:184)
at java.base/java.lang.Thread.run(Thread.java:1583)
And the NPE exception:
java.lang.NullPointerException: Cannot invoke
"javafx.scene.Node.getScene()" because "<local2>" is null
at
javafx.scene.Scene$ScenePulseListener.synchronizeSceneNodes(Scene.java:2395)
at javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2542)
at com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:407)
at
java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:406)
(rest of trace identical to the AssertionError one)
And another variant of the NPE exception:
java.lang.NullPointerException: Cannot invoke
"javafx.scene.Node.getScene()" because "<local2>" is null
at
javafx.scene.Scene$ScenePulseListener.synchronizeSceneNodes(Scene.java:2395)
at javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2542)
at com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:407)
at
java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:406)
at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:436)
at
com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:575)
at
com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:555)
at
com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:548)
at
com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$11(QuantumToolkit.java:352)
at
com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at
com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:184)
at java.base/java.lang.Thread.run(Thread.java:1583)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/openjfx-dev/attachments/20240822/0b2d3a8b/attachment-0001.htm>
More information about the openjfx-dev
mailing list