<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  </head>
  <body>
    <p>Small sample program that triggers an assert in Parent:</p>
    <div style="background-color:#ffffff;padding:0px 0px 0px 2px;">
      <div
style="color:#000000;background-color:#ffffff;font-family:"Consolas";font-size:11pt;white-space:pre;"><p
      style="margin:0;"><span style="color:#000000;">  </span><span
      style="color:#646464;">@</span><span style="color:#646464;">Override</span></p><p
      style="margin:0;"><span style="color:#000000;">  </span><span
      style="color:#0000a0;font-weight:bold;">public</span><span
      style="color:#000000;"> </span><span
      style="color:#0000a0;font-weight:bold;">void</span><span
      style="color:#000000;"> start(Stage primaryStage) {</span></p><p
      style="margin:0;"><span style="color:#000000;">    HBox hbox = </span><span
      style="color:#0000a0;font-weight:bold;">new</span><span
      style="color:#000000;"> HBox();</span></p><p style="margin:0;">
</p><p style="margin:0;"><span style="color:#000000;">    Scene scene = </span><span
      style="color:#0000a0;font-weight:bold;">new</span><span
      style="color:#000000;"> Scene(hbox);</span></p><p
      style="margin:0;">
</p><p style="margin:0;"><span style="color:#000000;">    Button button1 = </span><span
      style="color:#0000a0;font-weight:bold;">new</span><span
      style="color:#000000;"> Button(</span><span style="color:#2a00ff;">"Hi"</span><span
      style="color:#000000;">);</span></p><p style="margin:0;"><span
      style="color:#000000;">    Button button2 = </span><span
      style="color:#0000a0;font-weight:bold;">new</span><span
      style="color:#000000;"> Button(</span><span style="color:#2a00ff;">"World"</span><span
      style="color:#000000;">);</span></p><p style="margin:0;"><span
      style="color:#000000;">    Button button3 = </span><span
      style="color:#0000a0;font-weight:bold;">new</span><span
      style="color:#000000;"> Button(</span><span style="color:#2a00ff;">"!"</span><span
      style="color:#000000;">);</span></p><p style="margin:0;">
</p><p style="margin:0;"><span style="color:#000000;">    hbox.getChildren().addAll(button1, button2);</span></p><p
      style="margin:0;"><span style="color:#000000;">    hbox.getChildren().addListener(</span><span
      style="color:#0000a0;font-weight:bold;">new</span><span
      style="color:#000000;"> InvalidationListener() {</span></p><p
      style="margin:0;"><span style="color:#000000;">      </span><span
      style="color:#646464;">@Override</span></p><p style="margin:0;"><span
      style="color:#000000;">      </span><span
      style="color:#0000a0;font-weight:bold;">public</span><span
      style="color:#000000;"> </span><span
      style="color:#0000a0;font-weight:bold;">void</span><span
      style="color:#000000;"> invalidated(Observable observable) {</span></p><p
      style="margin:0;"><span style="color:#000000;">        </span><span
      style="color:#0000a0;font-weight:bold;">if</span><span
      style="color:#000000;">(!hbox.getChildren().contains(button3)) {</span></p><p
      style="margin:0;"><span style="color:#000000;">          hbox.getChildren().addAll(button3, button3);</span></p><p
      style="margin:0;"><span style="color:#000000;">        }</span></p><p
      style="margin:0;"><span style="color:#000000;">      }</span></p><p
      style="margin:0;"><span style="color:#000000;">    });</span></p><p
      style="margin:0;">
</p><p style="margin:0;"><span style="color:#000000;">    button1.toFront();</span></p><p
      style="margin:0;">
</p><p style="margin:0;"><span style="color:#000000;">    primaryStage.setScene(scene);</span></p><p
      style="margin:0;"><span style="color:#000000;">    primaryStage.show();</span></p><p
      style="margin:0;"><span style="color:#000000;">  }</span></p></div>
    </div>
    <p></p>
    <p>--John<br>
    </p>
    <div class="moz-cite-prefix">On 22/08/2024 09:37, John Hendrikx
      wrote:<br>
    </div>
    <blockquote type="cite"
      cite="mid:a255c77d-3bac-48c2-ae4e-20b9053812ad@gmail.com">
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
      <p>I think I figured out the reason why this fails.  The root
        cause lies in a misconception I've seen in a lot of FX code.<br>
      </p>
      <p>JavaFX uses a single event thread model, which ensures all
        structures are only ever accessed by a single thread.  This
        frees FX from having to do synchronization on almost every
        modification you make to properties or the scene graph.</p>
      <p>However, in many areas it makes the assumption that such code
        will always run sequentially to completion without interruption,
        and uses instance fields judiciously to communicate things to
        deeper nested code or to code further down the line.  But code
        using instance fields in this way is not safe to re-enter (it is
        not reentrant-safe) without precautions -- sharing instance
        fields in this way safely can easily get as complicated as
        writing multi-threaded code.</p>
      <p>A simple example that I saw in Parent's toFront code:</p>
      <div style="background-color:#ffffff;padding:0px 0px 0px 2px;">
        <div
style="color:#000000;background-color:#ffffff;font-family:"Consolas";font-size:11pt;white-space:pre;"><p
        style="margin:0;"><span style="color:#000000;">            </span><span
        style="color:#0000c0;">childrenTriggerPermutation</span><span
        style="color:#000000;"> = </span><span
        style="color:#0000a0;font-weight:bold;">true</span><span
        style="color:#000000;">;</span></p><p style="margin:0;"><span
        style="color:#000000;">            </span><span
        style="color:#0000a0;font-weight:bold;">try</span><span
        style="color:#000000;"> {</span></p><p style="margin:0;"><span
        style="color:#000000;">                </span><span
        style="color:#0000c0;">children</span><span
        style="color:#000000;">.remove(node);</span></p><p
        style="margin:0;"><span style="color:#000000;">                </span><span
        style="color:#0000c0;">children</span><span
        style="color:#000000;">.add(node);</span></p><p
        style="margin:0;"><span style="color:#000000;">            } </span><span
        style="color:#0000a0;font-weight:bold;">finally</span><span
        style="color:#000000;"> {</span></p><p style="margin:0;"><span
        style="color:#000000;">                </span><span
        style="color:#0000c0;">childrenTriggerPermutation</span><span
        style="color:#000000;"> = </span><span
        style="color:#0000a0;font-weight:bold;">false</span><span
        style="color:#000000;">;</span></p><p style="margin:0;"><span
        style="color:#000000;">            }</span></p></div>
      </div>
      <p>The above code uses an instance field
        "childrenTriggerPermutation" to activate an optimization. The
        optimization will assume that the children are only re-arranged,
        and no new ones were added or removed.  However, "children" is
        an ObservableList, which means the user can register listeners
        on it, which do who knows what.  If such a listener modifies the
        children list in another way then the code is entered again, but
        the "childrenTriggerPermutation" optimization will still be
        enabled causing it to not notice the change the user did.</p>
      <p>This problem is similar to the ChangeListener old value bug. 
        When within a change listener you do another change (and so the
        same code is called **deeper** in the same stack), downstream
        change listeners will not receive the correct old values because
        the code is insufficiently reentrant-safe.  ExpressionHelper
        **tries** to mitigate some of these issues (for cases where
        listeners are added/removed reentrantly) by making copies of the
        listener list, but it does not handle this case.<br>
      </p>
      <p>Similarly, the bug I encountered in my original post is also
        such an issue.  While processing the children list changes,
        several **properties** are being manipulated.  Being properties,
        these can have listeners of their own that could trigger further
        modifications and, in complex enough programs, they may even
        re-enter the same class's code that is sharing instance fields
        in an unsafe way.  And that's exactly what is happening:</p>
      <p>1. The children list change processing is registering the
        offset of the first changed child in the children list (called
        "startIdx") as an instance field -- this field is used as an
        optimization for updatePeer (so it doesn't have to check/copy
        all children).  It assumes the processing always finishes
        completely and it will get to the point where it sets "startIdx"
        but...<br>
      </p>
      <p>2. Before it sets "startIdx" but after the children list is
        already modified, it modifies several properties.  Being
        properties, these can have listeners, and as such this can
        trigger a cascade of further calls in complicated applications.</p>
      <p>3. In this case, the cascade of calls included an
        "enterNestedEventLoop".  Pulses (and things like
        Platform#runLater) can be handled on such a nested loop, and FX
        decides that now is as good a time as any to handle a new pulse.</p>
      <p>4. The pulse triggers updatePeer calls, among which is the
        Parent that is still (higher in the stack) midway its children
        list processing code.</p>
      <p>5. The updatePeer code looks at "startIdx", the shared instance
        field that Parent uses for its optimizations.  This field is NOT
        modified yet.  The field indicates the first child that was
        modified, and the field is normally set to "children.size()"
        when there are no changes.  That's also the case in this case
        still, and so updatePeer updates nothing at all.  An assertion
        later in this code then checks if children.size() ==
        peer.children.size() which fails... a stack trace is thrown, and
        synchronizeSceneNodes() blows up with infinite NPE's.</p>
      <p>I'm not entirely sure yet how to resolve this, and if it should
        be.</p>
      <p>Perhaps the safest way would be to undo some of the
        optimizations/assumptions, and perhaps reoptimize them if
        there's a pressing need.</p>
      <p>Another option would be to somehow delay listener callbacks
        until the code in Parent is in a safe state.</p>
      <p>The option I like the least is to introduce yet another
        instance flag ("processingListChange") and throwing an early
        exception if other code is entered that doesn't expect it...</p>
      <p>--John<br>
      </p>
      <p><br>
      </p>
      On 22/08/2024 05:24, John Hendrikx wrote:<br>
      <blockquote type="cite"
        cite="mid:efc29c6d-9b12-408b-9328-af181c42ba5d@gmail.com">
        <meta http-equiv="content-type"
          content="text/html; charset=UTF-8">
        <p>Hi List,</p>
        <p>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.<br>
        </p>
        <p>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).</p>
        <p>In Parent#validatePG it prints:</p>
        <p>    *** pgnodes.size validatePG() [1] != children.size() [2]</p>
        <p>And then throws an AssertionError with a long stacktrace.<br>
        </p>
        <p>    java.lang.AssertionError: validation of PGGroup children
          failed<br>
                  (stack trace omitted)<br>
        </p>
        <p>Immediately after this, the NPE in synchronizeNodes starts to
          get spammed, and the application never recovers.<br>
        </p>
        <p>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).</p>
        <p>The nested event loop is entered from a ChangeListener, which
          may be a bit unusual.  The documentation of
          Platform#enterNestedEventLoop says:</p>
        <div style="background-color:#ffffff;padding:0px 0px 0px 2px;">
          <div
style="color:#000000;background-color:#ffffff;font-family:"Consolas";font-size:11pt;white-space:pre;"><p
          style="margin:0;"><span style="color:#3f5fbf;">     * This method must either be called from an input event handler or</span></p><p
          style="margin:0;"><span style="color:#3f5fbf;">     * from the run method of a Runnable passed to</span></p><p
          style="margin:0;"><span style="color:#3f5fbf;">     * </span><span
          style="color:#3f3fbf;">{@link javafx.application.Platform#runLater Platform.runLater}</span><span
          style="color:#3f5fbf;">.</span></p><p style="margin:0;"><span
          style="color:#3f5fbf;">     * It must not be called during animation or layout processing.</span></p></div>
        </div>
        <p>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:</p>
        <p>    at
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)<br>
              at
          com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)<br>
              at
          com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)<br>
              at javafx.event.Event.fireEvent(Event.java:198)<br>
              at javafx.scene.Scene$KeyHandler.process(Scene.java:4113)<br>
              at javafx.scene.Scene.processKeyEvent(Scene.java:2159)<br>
              at
          javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:2627)<br>
              at
com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:218)<br>
          <br>
          So IMHO technically I am in an input event handler...?</p>
        <p>Now the code does a LOT of stuff, which is going to make this
          tough to analyze.  In short:</p>
        <p>- I'm displaying a Scene<br>
          - The user indicates (with a keypress) they want to go to a
          different Pane<br>
          - 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<br>
          - 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:<br>
          <br>
              at javafx.scene.Node.invalidatedScenes(Node.java:1075)<br>
              at javafx.scene.Node.setScenes(Node.java:1142)           
            <-- the new Pane will get a Scene assigned to it<br>
              at javafx.scene.Parent$2.onChanged(Parent.java:372)     
          <-- the new Pane has its Parent changed to the
          TransitionPane<br>
          <br>
          - 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)<br>
          - 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</p>
        <p>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.</p>
        <p>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.</p>
        <p>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.</p>
        <p>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?</p>
        <p>I've included an annotated stack trace below.</p>
        <p>As it is quite reproducable, I can debug this further by
          adding breakpoints/prints -- I'm just unsure where to start
          looking.<br>
        </p>
        <p>--John</p>
        <p>java.lang.AssertionError: validation of PGGroup children
          failed<br>
            at javafx.scene.Parent.validatePG(Parent.java:243)<br>
            at javafx.scene.Parent.doUpdatePeer(Parent.java:201)<br>
            at javafx.scene.Parent$1.doUpdatePeer(Parent.java:109)<br>
            at
          com.sun.javafx.scene.ParentHelper.updatePeerImpl(ParentHelper.java:78)<br>
            at
com.sun.javafx.scene.layout.RegionHelper.updatePeerImpl(RegionHelper.java:72)<br>
            at
          com.sun.javafx.scene.NodeHelper.updatePeer(NodeHelper.java:104)<br>
            at javafx.scene.Node.syncPeer(Node.java:721)<br>
            at
javafx.scene.Scene$ScenePulseListener.synchronizeSceneNodes(Scene.java:2396)<br>
            at
          javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2542)<br>
            at
          com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:407)<br>
            at
java.base/java.security.AccessController.doPrivileged(AccessController.java:400)<br>
            at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:406)<br>
            at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:436)<br>
            at
          com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:575)<br>
            at
          com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:555)<br>
            at
com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:548)<br>
            at
com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$11(QuantumToolkit.java:352)<br>
            at
com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)<br>
            at
          com.sun.glass.ui.win.WinApplication._enterNestedEventLoopImpl(Native
          Method)<br>
            at
com.sun.glass.ui.win.WinApplication._enterNestedEventLoop(WinApplication.java:211)<br>
            at
          com.sun.glass.ui.Application.enterNestedEventLoop(Application.java:515)<br>
            at com.sun.glass.ui.EventLoop.enter(EventLoop.java:107)<br>
            at
com.sun.javafx.tk.quantum.QuantumToolkit.enterNestedEventLoop(QuantumToolkit.java:631)<br>
            at
          javafx.application.Platform.enterNestedEventLoop(Platform.java:301)<br>
            at
hs.mediasystem.util.javafx.SceneUtil.enterNestedEventLoop(SceneUtil.java:75)  
          <-- just my wrapper to trace slow calls, it delegates to
          Platform<br>
            at
          hs.mediasystem.runner.dialog.DialogPane.showDialog(DialogPane.java:78)<br>
            at
hs.mediasystem.runner.dialog.Dialogs.showProgressDialog(Dialogs.java:163)<br>
            at
          hs.mediasystem.runner.dialog.Dialogs.runNested(Dialogs.java:103)<br>
            at
hs.mediasystem.plugin.home.HomeScreenNodeFactory.lambda$1(HomeScreenNodeFactory.java:123) <br>
        </p>
        <p>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).<br>
        </p>
        <p>  at
com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:360)<br>
            at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
            at
javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:80)<br>
            at
javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)<br>
            at
javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:118)<br>
            at
javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:172)<br>
            at
javafx.scene.control.SelectionModel.setSelectedItem(SelectionModel.java:105)<br>
            at
javafx.scene.control.MultipleSelectionModelBase.lambda$new$0(MultipleSelectionModelBase.java:67)<br>
            at
com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:348)<br>
            at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
            at
javafx.beans.property.ReadOnlyIntegerPropertyBase.fireValueChangedEvent(ReadOnlyIntegerPropertyBase.java:78)<br>
            at
javafx.beans.property.ReadOnlyIntegerWrapper.fireValueChangedEvent(ReadOnlyIntegerWrapper.java:102)<br>
            at
javafx.beans.property.IntegerPropertyBase.markInvalid(IntegerPropertyBase.java:114)<br>
            at
javafx.beans.property.IntegerPropertyBase.set(IntegerPropertyBase.java:148)<br>
            at
javafx.scene.control.SelectionModel.setSelectedIndex(SelectionModel.java:69)<br>
            at
javafx.scene.control.MultipleSelectionModelBase.select(MultipleSelectionModelBase.java:424)<br>
            at
javafx.scene.control.MultipleSelectionModelBase.select(MultipleSelectionModelBase.java:456)<br>
            at
hs.mediasystem.plugin.home.HomeScreenNodeFactory.lambda$3(HomeScreenNodeFactory.java:176)   
          <-- triggers the creation of a ListView<br>
            at
          javafx.beans.value.ObservableValue.lambda$0(ObservableValue.java:364)<br>
            at
com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)<br>
            at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
            at
          javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)<br>
            at
com.sun.javafx.binding.ConditionalBinding.conditionChanged(ConditionalBinding.java:53)<br>
            at
          com.sun.javafx.binding.Subscription.lambda$2(Subscription.java:63)<br>
            at
com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)<br>
            at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
            at
          javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)<br>
            at
          com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)<br>
            at
com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)<br>
            at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
            at
          javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)<br>
            at
          com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)<br>
            at
com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)<br>
            at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
            at
          javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)<br>
            at
com.sun.javafx.binding.FlatMappedBinding.invalidateAll(FlatMappedBinding.java:102)<br>
            at
          com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)<br>
            at
com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)<br>
            at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
            at
          javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)<br>
            at
com.sun.javafx.binding.FlatMappedBinding.invalidateAll(FlatMappedBinding.java:102)<br>
            at
          com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)<br>
            at
com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:348)<br>
            at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
            at
javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:80)<br>
            at
javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)<br>
            at
javafx.scene.Node$ReadOnlyObjectWrapperManualFire.fireSuperValueChangedEvent(Node.java:1053)  
          <-- there is a listener on a Scene property<br>
            at javafx.scene.Node.invalidatedScenes(Node.java:1104)<br>
            at javafx.scene.Node.setScenes(Node.java:1142)<br>
            at javafx.scene.Parent.scenesChanged(Parent.java:772)   
          <-- the new Pane has a few nested Panes, so you see
          multiple Parent assignments<br>
            at javafx.scene.Node.invalidatedScenes(Node.java:1075)<br>
            at javafx.scene.Node.setScenes(Node.java:1142)<br>
            at javafx.scene.Parent.scenesChanged(Parent.java:772)<br>
            at javafx.scene.Node.invalidatedScenes(Node.java:1075)<br>
            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<br>
            at javafx.scene.Parent$2.onChanged(Parent.java:372)   
          <-- the new Pane gets its parent assigned (TransitionPane)<br>
            at
com.sun.javafx.collections.TrackableObservableList.lambda$new$0(TrackableObservableList.java:45)<br>
            at
com.sun.javafx.collections.ListListenerHelper$Generic.fireValueChangedEvent(ListListenerHelper.java:329)<br>
            at
com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(ListListenerHelper.java:73)<br>
            at
javafx.collections.ObservableListBase.fireChange(ObservableListBase.java:239)<br>
            at
          javafx.collections.ListChangeBuilder.commit(ListChangeBuilder.java:482)<br>
            at
javafx.collections.ListChangeBuilder.endChange(ListChangeBuilder.java:541)<br>
            at
javafx.collections.ObservableListBase.endChange(ObservableListBase.java:211)<br>
            at
javafx.collections.ModifiableObservableListBase.add(ModifiableObservableListBase.java:162)<br>
            at
com.sun.javafx.collections.VetoableListDecorator.add(VetoableListDecorator.java:319)<br>
            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<br>
            at
hs.mediasystem.util.javafx.ui.transition.TransitionPane.addAtStart(TransitionPane.java:105)<br>
            at
hs.mediasystem.util.javafx.ui.transition.TransitionPane.add(TransitionPane.java:113)<br>
            at
hs.mediasystem.runner.presentation.ViewPort.updateChildNode(ViewPort.java:68)      
          <-- here it installs the new Pane to display<br>
            at
          hs.mediasystem.runner.presentation.ViewPort.lambda$0(ViewPort.java:39)<br>
            at
com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)<br>
            at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
            at
          javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)<br>
            at
          com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)<br>
            at
com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)<br>
            at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
            at
javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:111)<br>
            at
javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:118)<br>
            at
javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:172)<br>
            at
hs.mediasystem.presentation.ParentPresentation.navigateBack(ParentPresentation.java:39)   
          <-- this handles the keyPress (it was a "back" press)<br>
            at
          hs.mediasystem.util.expose.Expose.lambda$3(Expose.java:55)<br>
            at hs.mediasystem.util.expose.Trigger$1.run(Trigger.java:58)<br>
            at
hs.mediasystem.runner.RootPresentationHandler.tryRunAction(RootPresentationHandler.java:106)<br>
            at
hs.mediasystem.runner.RootPresentationHandler.handleActionEvent(RootPresentationHandler.java:81)<br>
            at
com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:247)<br>
            at
com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)<br>
            at
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:234)<br>
            at
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)<br>
            at
com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)<br>
            at
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)<br>
            at
          hs.mediasystem.util.javafx.SceneUtil.lambda$1(SceneUtil.java:101)     
          <-- this is just my Slow Event detection, and just
          delegates the event<br>
            at
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)<br>
            at
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)<br>
            at
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)<br>
            at
          com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)<br>
            at
          com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49)<br>
            at
          hs.mediasystem.util.javafx.base.Events.dispatchEvent(Events.java:35)<br>
            at
hs.mediasystem.runner.action.InputActionHandler.handleKeyEvent(InputActionHandler.java:153)    
          <-- just code that handles a keypress that bubbled all the
          way to the top<br>
            at
hs.mediasystem.runner.action.InputActionHandler.onKeyPressed(InputActionHandler.java:138)<br>
            at
com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)<br>
            at
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:234)<br>
            at
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)<br>
            at
com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)<br>
            at
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)<br>
            at
          hs.mediasystem.util.javafx.SceneUtil.lambda$1(SceneUtil.java:101)     
          <-- this is just my Slow Event detection, and just
          delegates the event<br>
            at
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)<br>
            at
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)<br>
            at
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)<br>
            at
          com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)<br>
            at
          com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)<br>
            at javafx.event.Event.fireEvent(Event.java:198)<br>
            at javafx.scene.Scene$KeyHandler.process(Scene.java:4113)<br>
            at javafx.scene.Scene.processKeyEvent(Scene.java:2159)<br>
            at
          javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:2627)<br>
            at
com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:218)<br>
            at
com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:150)<br>
            at
java.base/java.security.AccessController.doPrivileged(AccessController.java:400)<br>
            at
com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleKeyEvent$1(GlassViewEventHandler.java:250)<br>
            at
com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:424)<br>
            at
com.sun.javafx.tk.quantum.GlassViewEventHandler.handleKeyEvent(GlassViewEventHandler.java:249)<br>
            at com.sun.glass.ui.View.handleKeyEvent(View.java:542)<br>
            at com.sun.glass.ui.View.notifyKey(View.java:966)<br>
            at com.sun.glass.ui.win.WinApplication._runLoop(Native
          Method)<br>
            at
com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:184)<br>
            at java.base/java.lang.Thread.run(Thread.java:1583)<br>
        </p>
        <p>And the NPE exception:</p>
        <p>java.lang.NullPointerException: Cannot invoke
          "javafx.scene.Node.getScene()" because "<local2>" is
          null<br>
              at
javafx.scene.Scene$ScenePulseListener.synchronizeSceneNodes(Scene.java:2395)<br>
              at
          javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2542)<br>
              at
          com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:407)<br>
              at
java.base/java.security.AccessController.doPrivileged(AccessController.java:400)<br>
              at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:406)<br>
              (rest of trace identical to the AssertionError one)</p>
        <p>And another variant of the NPE exception:<br>
          <br>
          java.lang.NullPointerException: Cannot invoke
          "javafx.scene.Node.getScene()" because "<local2>" is
          null<br>
            at
javafx.scene.Scene$ScenePulseListener.synchronizeSceneNodes(Scene.java:2395)<br>
            at
          javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2542)<br>
            at
          com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:407)<br>
            at
java.base/java.security.AccessController.doPrivileged(AccessController.java:400)<br>
            at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:406)<br>
            at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:436)<br>
            at
          com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:575)<br>
            at
          com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:555)<br>
            at
com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:548)<br>
            at
com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$11(QuantumToolkit.java:352)<br>
            at
com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)<br>
            at com.sun.glass.ui.win.WinApplication._runLoop(Native
          Method)<br>
            at
com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:184)<br>
            at java.base/java.lang.Thread.run(Thread.java:1583)<br>
        </p>
        <p><br>
        </p>
        <p><br>
        </p>
        <p><br>
        </p>
        <p><br>
        </p>
      </blockquote>
    </blockquote>
  </body>
</html>