Feedback requested for infrastructure for properties that wish to delay registering listeners

John Hendrikx john.hendrikx at gmail.com
Mon Feb 6 06:39:32 UTC 2023


On 06/02/2023 06:00, Michael Strauß wrote:
> I like your proposal in general, since it may result in a performance
> improvement in some cases. If we can avoid to eagerly registering
> listeners that aren't needed, that's great.
Thanks. It would probably simplify some cases that now require custom 
property implementations.
> However, it is not an alternative to using weak listeners. Consider
> the following test, which is taken from Florian's PR
> (https://github.com/openjdk/jfx/pull/689):
>
>      @Test
>      public void testListPropertyLeak2() {
>         JMemoryBuddy.memoryTest(checker -> {
>             ObservableList<Object> list = FXCollections.observableArrayList();
>             ListProperty<Object> listProperty = new SimpleListProperty<>(list);
>
>             checker.setAsReferenced(list);
>             checker.assertCollectable(listProperty);
>         });
>      }
>
> This test only passes with your PR because `listProperty` doesn't
> register a listener on `list` (that's what you are proposing, after
> all).

Yes, when creating a new instance of something (in this case 
`SimpleListProperty`) there shouldn't be any weird side effects. 
Registering a listener to something else in the constructor is passing 
on a reference to the instance (SimpleListProperty) before it is fully 
constructed, which is a bad practice.  Because it is bad practice, it is 
very rare to see an object creation have side effects, like it being 
registered somewhere, that would block its immediate garbage collection.

A call to `new` without an assignment to a variable, will in almost all 
cases result in that object being immediately eligible for GC. This is 
what users expect, the only one that has a reference to the object 
resulting from a `new` operator is the caller.  The cases where this 
doesn't happen are likely bugs or not idiomatic Java.  This escapes are 
problematic for various reasons (see recent discussions in amber-dev and 
the new this-escape warning).  In the strictly threaded environment of 
JavaFX you can get away with it usually, but not always.  It is still a 
huge surprise for users that expect the `new` operator to be side effect 
free.

This is why I applaud the effort which we're doing for Skins at the 
moment, moving all listener registration to the `install` method, 
preventing the instance from escaping before it is fully constructed.  
It means that just constructing the Skin does not tie it to a control 
already.  Similarly, just constructing a ListProperty should not tie it 
to an ObservableList; it doesn't matter if that tie is strong or weak.

> As soon as you do that, for example by adding the following line, the
> test fails:
>
>      listProperty.addListener((ListChangeListener<? super Object>) change -> {});
>
> But the test shouldn't ever fail, because the ObservableList that is
> wrapped by ListProperty should never keep a strong reference to the
> ListProperty, even if the ListProperty is itself observed.
Why should it work that way?  The alternative is that my listener is 
discarded without my knowledge.  There is no warning in my IDE, in my 
logs, or anywhere.  It just seems to work, and then breaks at some point 
in the future. The fact that it seems to work, but then doesn't is a 
huge problem for debugging, testing and reproducing user problems. That 
last one is nasty, because it may work on my machine; when weak 
references get cleared can depend on JVM used, JVM parameters, memory 
available, hardware platform, etc.. -- they're fine for low level work 
like caching, but using them in a high level mechanism like a UI is 
making JavaFX far more complex than it should be.
> That's a bug in the current implementation, and hiding it by making it
> contingent upon the presence of observers doesn't fix the underlying
> issue.

I don't think it's a bug.  I took an action to register a listener.  It 
is on me to take action to unregister it.  And until that time it should 
keep working.

I mean, what if Java were to decide that a `Thread` which I started is 
not doing relevant work because it determined that the objects the 
thread is using are not referenced by any other threads?  Should it be 
allowed to kill this `Thread`? And if that's allowed, should it do this 
silently without warning?

What if I added an important listener like this:

permissionsListProperty.addListener(AuditingService::auditLogPermissionChanges);

Do you think JavaFX should decide that this is not relevant because only 
the property still has a reference to this listener, but nothing else has?

Also, do you think it should keep working until a garbage collection 
occurs, but then stop working after one occurs?  When will that be?

I feel there is fundamental misunderstanding of how listeners are 
normally working in Java. JavaFX is the only system that decides that a 
listener is irrelevant based on how it is being referenced.  Nothing 
else does this.  Beans don't do this (PropertyChangeListener), other UI 
frameworks don't do this (Swing, AWT, SWT), event frameworks I worked 
with don't -- in fact, I can't think of a single example that does 
listener management the way JavaFX does.

I'm trying to get JavaFX to a state where it is more predictable, and 
for this certain tools are needed.  ObservableValue#when was one of 
those, it allows users to do their listener management upfront (no need 
to save a reference to the listener for later removal).

Subscriptions are another useful tool so you don't have to save the 
lambda or method reference for later removal:

        Subscription combinedSubscription =
               propertyA.subscribe(xyz)
                   .and(propertyB.subscribe(abc));

        // dispose of all listeneres with one line:
        combinedSubscription.unsubscribe();

Method references are not unique (or don't have to be) so doing 
"property.removeListener(this::myMethod)" will not do anything (and 
removeListener will silently ignore listeners it doesn't know about 
making this problem worse).  Subscriptions solve this problem.

These (and other patterns) are currently hand rolled by many -- many 
however simply get it wrong, from which many "memory leak", 
"unpredictable" or "bug" complaints arise. ObservableValue#when 
addresses many of these already, and exposing Subscriptions is useful as 
they're a bit lighter (I think it would be useful for Skins, although 
you could use `when` if you're careful).

Note that weak listeners don't solve the problem adequately because the 
pattern breaks when the involved property has a different life cycle 
than its listener.  Weak listeners promise (or seem to promise) that 
manual removal of listeners is never needed, but nothing is further from 
the truth.  Since manual removal is still required, even with weak 
listeners, investing in API and patterns that make this burden easier 
and less error prone is a good thing.

With this proposal I'm trying to help property implementators to not 
hold onto resources when they're not needed.  The idea here being that 
you can override observed/unobserved to register/unregister listeners 
when there's an actual need.  It's also constructed in such a way that 
it is easy to do this for sub properties.

For example, if you have the subproperties "size" and "empty", then you 
can register listeners by having all of these sub properties forward to 
a general "observed" method:

       private void subPropertyWasObserved() {
              if (!isAnySubPropertyObserved()) {
                   // register listeners
              }
       }

       // reusable helper to determine observation state:
       private boolean isAnySubPropertyObserved() {
              return size.isObserved() || empty.isObserved();
       }

And it's counter part:

       private void subPropertyWasUnobserved() {
              if (!isAnySubPropertyObserved()) {
                   // unregister listeners
              }
       }

The observed/unobserved calls happen just before the first registration 
and just after the last unregistration, making this a very simple 
pattern to use when multiple properties are involved.

--John


>
>
> On Sun, Feb 5, 2023 at 10:46 AM John Hendrikx <john.hendrikx at gmail.com> wrote:
>> Any comments on this?
>>
>> I think having API to make it easier for both JavaFX and users to create
>> listeners that can delay registration to their own dependencies. This
>> would be an alternative to using weak listeners for these cases.
>>
>> --John


More information about the openjfx-dev mailing list