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)