<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  </head>
  <body>
    <p>Hey John,<br>
      <br>
      Thanks for taking your time on going into the details here.<br>
      <br>
      About our use case: We are actually not constructing UI in a
      background thread, all nodes are initialized and added to the
      scene on the platform thread. This is done because previously
      instantiating nodes on other threads was unstable for some
      controls. So this issue has nothing to do with nodes at all and is
      purely focused on the observable value listeners. When
      initializing our application, there are some global properties
      like the app language property that a lot of other listeners and
      bindings depend on as a lot of stuff has to be changed when the
      language changes (Not just within nodes). So in practice, we have
      100+ listeners added to these important setting properties. These
      listeners are added from various threads as the loading is
      parallelized. This loading is to some degree done before the
      platform is even started.<br>
      <br>
      About the listener management: Yes I'm aware that this can be
      tricky and I'm doing the best to account for all of that. The GC
      problem is always a bit tricky and has led to some issues in the
      past. But once you are familiar with it, it's manageable. We are
      using a somewhat custom implementation to create nodes, link them
      to properties, and control their lifecycle properly for GC
      handling for that.<br>
      <br>
      About the expression helper being replaced, that is unfortunate
      (and also a bit weird from my unknowing perspective, at least I
      don't see the reason for that). If you are already reworking the
      listeners in general, then it would be nice if there was a good
      way to synchronize the expressionhelper on something that is
      consistent.<br>
      <br>
      You mentioned that there is a case that is hard to fix where a
      callback occurs on another thread while the instances are
      manipulated on the first thread. At least for us, that shouldn't
      be a problem. This issue I reported is only about adding/removing
      listeners in different threads. So I will be following the status
      of your PR for updates.<br>
      <br>
      Best<br>
      Christopher Schnick<br>
    </p>
    <div class="moz-cite-prefix">On 24/04/2025 03:56, John Hendrikx
      wrote:<br>
    </div>
    <blockquote type="cite"
      cite="mid:f0c074e8-176b-4465-a239-2dfb781e361e@gmail.com">
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
      <p>Hi,</p>
      <p>I don't think adding synchronized in ExpressionHelper is going
        to really solve your problem.  It will just move it elsewhere,
        but feel free to let me know your exact scenario.  For now I
        will make some assumptions.<br>
      </p>
      <p>I'm assuming you are constructing UI's in a background thread,
        and this UI requires listening to some global properties, like
        dark/light mode, or any other configuration that must
        dynamically change your UI that's basically global, or some
        global modeled state that can be independently used, even
        without a UI.  It's certainly not an unreasonable scenario in
        larger applications that may have a lot of configuration options
        -- I've been there myself.  I usually call these "global"
        models; they're not part of any specific piece of the user
        interface.  Feel free to let me know your scenario.</p>
      <p>I'm fine with UI's being constructed on background threads;
        anything that could potentially take more than a millisecond
        SHOULD be done on a background thread, as otherwise animations
        will stutter.  However, there are several gotcha's with
        connecting a UI with global models that expose properties that
        you must be aware of:</p>
      <p>## Listener Management</p>
      <p>Any UI component that listens to global properties must either:<br>
        <br>
        a) unregister itself when the UI component is removed or closed
        (this can be very difficult to track as FX has no #dispose
        method that will be called)<br>
        b) use a weak listener (discouraged as this can lead to phantom
        call backs of UI's you thought no longer existed until GC runs)<br>
        c) only register the listener when the UI is visible, and
        immediately unregister when it becomes invisible (this can be
        largely automated with the "when" method of ObservableValue)</p>
      <p>Failing to do so means your UI component (including all its
        children/parents as they refer to those) will never be garbage
        collected as a global property is referring to it.</p>
      <p>I highly recommend using the "when" construct here.  Basically,
        whenever you want to listen to a global property from a UI
        component insert a "when" statement:<br>
        <br>
               globalProperty.when(myComponentIsVisible).subscribe( ...
        ) or addListener( ... )<br>
        <br>
        Or: <br>
        <br>
              
        uiProperty.bind(globalProperty.when(myComponentIsVisible));<br>
        <br>
        This results in listeners being registered on the FX thread just
        before your UI becomes visible to the user.  It also removes the
        listeners on the FX thread as soon as the UI becomes invisible. 
        See the documentation for a good condition to use with when()
        for this.<br>
      </p>
      <p>## Global properties may call listeners at unexpected times!<br>
        <br>
        When you registered on such a property in a background thread,
        realize that as soon as you do, you may get a callback from the
        FX thread.  At that point in time, your presumed single threaded
        code that you are constructing on your isolated thread is being
        run by two threads.  In other words, you can get a callback from
        a global property halfway during construction while your
        components may be in some half constructed state.  As FX
        controls are never safe to use concurrently (and neither will
        your listener code be) this can cause intermittent problems.</p>
      <p>All that said, let's say we do want to proceed to make listener
        management a little bit safer and prevent ExpressionHelper from
        going into a bad state.  <br>
      </p>
      <p>Your proposal to just synchronize the methods in
        ExpressionHelper will be insufficient.  ExpressionHelper
        replaces itself on properties all the time, meaning that having
        a single invalidation listener on a property is a different
        ExpressionHelper instance then when that same property has 2
        invalidation listeners or say just a single change listener. 
        This is done by properties like this (from ObjectPropertyBase):<br>
      </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;">@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;"> addListener(InvalidationListener listener) {</span></p><p
        style="margin:0;"><span style="color:#000000;">        </span><span
        style="color:#0000c0;background-color:#f0d8a8;">helper</span><span
        style="color:#000000;"> = ExpressionHelper.</span><span
        style="color:#000000;font-style:italic;">addListener</span><span
        style="color:#000000;">(</span><span
        style="color:#0000c0;background-color:#d4d4d4;">helper</span><span
        style="color:#000000;">, </span><span
        style="color:#0000a0;font-weight:bold;">this</span><span
        style="color:#000000;">, listener);</span></p><p
        style="margin:0;"><span style="color:#000000;">    }</span></p></div>
      </div>
      <p>As you can see, the actual helper is getting replaced in
        certain cases (it "morphs" from one internal type to another
        depending on what listener types and counts are registered). 
        That means that the first call may be dealing with Helper#1, and
        the second call may also be dealing with Helper#1 (blocking
        inside ExpressionHelper on a synchronized block)... but the
        first call returns a new Helper, including the new listener. 
        When then the second call runs that was blocked, it will replace
        the Helper again but without knowledge of the listener that was
        added by the first call.  This happens when going from a single
        invalidation listener to two invalidation listeners -- it's a
        different helper.<br>
      </p>
      <p>There are two ways around that; you could synchronize at an
        earlier level before calling ExpressionHelper, adding
        synchronized to the above method and similar methods, in all
        property/bindings and read only property classes (about 20
        orso).  Another is to synchronize on the property itself (which
        is passed as "this" in the above snippet).  That still requires
        modifying 20 classes though as the "removeListener" variant does
        not pass "this" currently, so it would need to be explicitly
        passed for those as well to have something to synchronize on.<br>
      </p>
      <p>The PR which replaces ExpressionHelper (<a
          class="moz-txt-link-freetext"
          href="https://github.com/openjdk/jfx/pull/1081"
          moz-do-not-send="true">https://github.com/openjdk/jfx/pull/1081</a>)
        faces similar issues, but in that PR, "this" is passed already
        in all cases, giving it something to synchronize on.  If that PR
        is integrated, then offering thread safe adding/removal of
        listeners for all observable that use the new solution can be
        done in one central location.</p>
      <p>Perhaps it is worth doing; as Kevin mentioned, within FX itself
        we've run into problems with registering listeners that required
        quite some changes in many places.  A central fix may be
        preferable; however it can't and won't be a full fix, as you
        still must deal with potential callbacks coming in from another
        thread shortly after registering -- a scenario that most
        developers will likely not be taking into account while writing
        what they presume to be single threaded code...<br>
        <br>
        --John</p>
      <br>
      <div class="moz-cite-prefix">On 23/04/2025 18:58, Christopher
        Schnick wrote:<br>
      </div>
      <blockquote type="cite"
        cite="mid:f69784b1-6614-47e0-94f1-b5d94f0e46e8@xpipe.io">Hello,
        <br>
        <br>
        I encountered a rare exception where adding listeners to an
        observable value might break when they are added concurrently.
        This is due to ExpressionHelper not being synchronized. I
        thought about how to fix this on my side, but it is very
        difficult to do. As this is not a typical platform thread issue,
        in my opinion it should be possible to add listeners to one
        observable value from any thread without having to think about
        any potential synchronization issues (which I can't solve other
        than just running everything on one thread). <br>
        <br>
        Even worse, due to the size and array being two different
        variables and being incremented unsafely, once such a concurrent
        modification occurs, this invalid state will persist permanently
        and will cause exceptions on any further method call as well.
        The only solution is to restart the application. <br>
        <br>
        This is how a stack trace looks like when this occurs: <br>
        <br>
        21:25:38:840 - error: Index 2 out of bounds for length 2 <br>
        java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds
        for length 2 <br>
            at
com.sun.javafx.binding.ExpressionHelper$Generic.addListener(ExpressionHelper.java:248)<br>
            at
com.sun.javafx.binding.ExpressionHelper$Generic.addListener(ExpressionHelper.java:200)<br>
            at
com.sun.javafx.binding.ExpressionHelper.addListener(ExpressionHelper.java:65)<br>
            at
        javafx.beans.binding.ObjectBinding.addListener(ObjectBinding.java:86)
        <br>
            at
        javafx.beans.binding.StringBinding.bind(StringBinding.java:114)
        <br>
            at
        javafx.beans.binding.Bindings$7.<init>(Bindings.java:428)
        <br>
            at
        javafx.beans.binding.Bindings.createStringBinding(Bindings.java:426)
        <br>
            at
io.xpipe.app.util.StoreStateFormat.shellEnvironment(StoreStateFormat.java:24)<br>
            at
io.xpipe.ext.proc.env.ShellEnvironmentStoreProvider.informationString(ShellEnvironmentStoreProvider.java:155)<br>
            at
io.xpipe.app.comp.store.StoreEntryWrapper.update(StoreEntryWrapper.java:228)<br>
            at
io.xpipe.app.comp.store.StoreViewState.lambda$updateContent$1(StoreViewState.java:147)<br>
            at java.lang.Iterable.forEach(Iterable.java:75) <br>
            at
io.xpipe.app.comp.store.StoreViewState.updateContent(StoreViewState.java:147)<br>
            at
        io.xpipe.app.comp.store.StoreViewState.init(StoreViewState.java:93)
        <br>
            at
        io.xpipe.app.core.mode.BaseMode.lambda$onSwitchTo$1(BaseMode.java:109)
        <br>
            at
        io.xpipe.app.util.ThreadHelper.lambda$load$0(ThreadHelper.java:78)
        <br>
            at java.lang.Thread.run(Thread.java:1447) <br>
        <br>
        21:25:38:847 - error: Index 3 out of bounds for length 2 <br>
        java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds
        for length 2 <br>
            at
com.sun.javafx.binding.ExpressionHelper$Generic.addListener(ExpressionHelper.java:248)<br>
            at
com.sun.javafx.binding.ExpressionHelper$Generic.addListener(ExpressionHelper.java:200)<br>
            at
com.sun.javafx.binding.ExpressionHelper.addListener(ExpressionHelper.java:65)<br>
            at
        javafx.beans.binding.ObjectBinding.addListener(ObjectBinding.java:86)
        <br>
            at
        javafx.beans.binding.StringBinding.bind(StringBinding.java:114)
        <br>
            at
        javafx.beans.binding.Bindings$7.<init>(Bindings.java:428)
        <br>
            at
        javafx.beans.binding.Bindings.createStringBinding(Bindings.java:426)
        <br>
            at
io.xpipe.app.util.StoreStateFormat.shellEnvironment(StoreStateFormat.java:24)<br>
            at
io.xpipe.ext.proc.env.ShellEnvironmentStoreProvider.informationString(ShellEnvironmentStoreProvider.java:155)<br>
            at
io.xpipe.app.comp.store.StoreEntryWrapper.update(StoreEntryWrapper.java:228)<br>
            at
io.xpipe.app.comp.store.StoreEntryWrapper.lambda$setupListeners$3(StoreEntryWrapper.java:143)<br>
            at
io.xpipe.app.util.PlatformThread.lambda$runLaterIfNeeded$0(PlatformThread.java:318)<br>
            at
com.sun.javafx.application.PlatformImpl.lambda$runLater$4(PlatformImpl.java:424)<br>
            at
com.sun.glass.ui.InvokeLaterDispatcher$Future.run$$$capture(InvokeLaterDispatcher.java:95)<br>
            at
com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java)<br>
        <br>
        This full log goes up to index 50 out of bounds due to the
        recurring nature of this exception. <br>
        <br>
        Looking at the implementation of ExpressionHelper, I don't see
        any harm in just synchronizing the methods, at least from my
        perspective. But I guess that is up to the developers to decide.
        The only real solution I have as an application developer is to
        perform all initialization on one thread or just hope that this
        error is rare enough, both of which aren't great options. So I
        hope that a potential synchronization of the ExpressionHelper
        methods can be considered. <br>
        <br>
        Best <br>
        Christopher Schnick <br>
        <br>
      </blockquote>
    </blockquote>
  </body>
</html>