On Wed, 8 Jul 2026 23:15:01 GMT, John Hendrikx <[email protected]> wrote:

>> This provides and uses a new implementation of `ExpressionHelper`, called 
>> `ListenerManager` with improved semantics.
>> 
>> See also #837 for a previous attempt which instead of triggering nested 
>> emissions immediately (like this PR and `ExpressionHelper`) would wait until 
>> the current emission finishes and then start a new (non-nested) emission.
>> 
>> # Behavior
>> 
>> |Listener...|ExpressionHelper|ListenerManager|
>> |---|---|---|
>> |Invocation Order|In order they were registered, invalidation listeners 
>> always before change listeners|(unchanged)|
>> |Removal during Notification|All listeners present when notification started 
>> are notified, but excluded for any nested changes|Listeners are removed 
>> immediately regardless of nesting|
>> |Addition during Notification|Only listeners present when notification 
>> started are notified, but included for any nested changes|New listeners are 
>> never called during the current notification regardless of nesting|
>> 
>> ## Nested notifications:
>> 
>> | |ExpressionHelper|ListenerManager|
>> |---|---|---|
>> |Type|Depth first (call stack increases for each nested level)|(same)|
>> |# of Calls|Listeners * Depth (using incorrect old values)|Collapses nested 
>> changes, skipping non-changes|
>> |Vetoing Possible?|No|Yes|
>> |Old Value correctness|Only for listeners called before listeners making 
>> nested changes|Always|
>> 
>> # Performance
>> 
>> |Listener|ExpressionHelper|ListenerManager|
>> |---|---|---|
>> |Addition|Array based, append in empty slot, resize as needed|(same)|
>> |Removal|Array based, shift array, resize as needed|(same)|
>> |Addition during notification|Array is copied, removing collected 
>> WeakListeners in the process|Appended when notification finishes|
>> |Removal during notification|As above|Entry is `null`ed (to avoid moving 
>> elements in array that is being iterated)|
>> |Notification completion with changes|-|Null entries (and collected 
>> WeakListeners) are removed|
>> |Notifying Invalidation Listeners|1 ns each|(same)|
>> |Notifying Change Listeners|1 ns each (*)|2-3 ns each|
>> 
>> (*) a simple for loop is close to optimal, but unfortunately does not 
>> provide correct old values
>> 
>> # Memory Use 
>> 
>> Does not include alignment, and assumes a 32-bit VM or one that is using 
>> compressed oops.
>> 
>> |Listener|ExpressionHelper|ListenerManager|OldValueCaching ListenerManager|
>> |---|---|---|---|
>> |No Listeners|none|none|none|
>> |Single InvalidationListener|16 bytes overhead|none|none|
>> |Single ChangeListener|20 bytes overhead|none|16 bytes overhe...
>
> John Hendrikx has updated the pull request incrementally with one additional 
> commit since the last revision:
> 
>   Fix flakey check on (asynchronous) stderr output

This is one big, far reaching PR that might need a lot of time to review.  Here 
is the first batch of comments, code review only.

modules/javafx.base/src/main/java/com/sun/javafx/binding/ArrayManager.java line 
2:

> 1: /*
> 2:  * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.

2026 here and elsewhere (hopefully, it won't need to be changed to 2027)

modules/javafx.base/src/main/java/com/sun/javafx/binding/ArrayManager.java line 
48:

> 46:     /**
> 47:      * The minimum array size. (3 is a good number considering it would 
> use exactly 24 bytes
> 48:      * with compressed oops without wasted space due to alignment issues).

oops?

modules/javafx.base/src/main/java/com/sun/javafx/binding/ArrayManager.java line 
85:

> 83: 
> 84:     /**
> 85:      * Gets the occupied slots of the array under management.

it's an internal class, but "occupied slots" is confusing - the number of, or 
some kind of bitmap telling which slots are occupied and which are not?

modules/javafx.base/src/main/java/com/sun/javafx/binding/ArrayManager.java line 
260:

> 258:      * that satisfy the given predicate. If the predicate throws errors 
> or
> 259:      * exceptions, these are relayed to the caller, and the state of the 
> array
> 260:      * will be undefined.

interesting.  should it remain in some safe configuration instead?

modules/javafx.base/src/main/java/com/sun/javafx/binding/ArrayManager.java line 
281:

> 279:             if (filter.test(array[i])) {
> 280:                 shift++;
> 281:             }

} else if {

modules/javafx.base/src/main/java/com/sun/javafx/binding/ArrayManager.java line 
284:

> 282:             else if (shift > 0) {
> 283:                 array[i - shift] = array[i];
> 284:                 array[i] = null;

this nulls unused elements only when `shift>0`.  what happens if the unused 
element are at the end?  I think they will remain strongly referenced via 
`array[]`.

modules/javafx.base/src/main/java/com/sun/javafx/binding/ArrayManager.java line 
315:

> 313:      * completes.<p>
> 314:      *
> 315:      * Note: it is not allowed to change the array fields during this 
> call;

minor: "array fields" is a bit confusing.  array elements?

modules/javafx.base/src/main/java/com/sun/javafx/binding/ArrayManager.java line 
347:

> 345:          * When the minimum size of the array is 3 (MINIMUM_SIZE),
> 346:          * then the resizing algorithm used here always uses specific
> 347:          * sizes: 3, 7, 13, 22, 36, etc...

question: wouldn't it use more memory than necessary if the underlying memory 
allocation uses power of two blocks?  wouldn't it make more sense to use power 
of two sized blocks?

modules/javafx.base/src/main/java/com/sun/javafx/binding/ListenerList.java line 
178:

> 176:                  */
> 177: 
> 178:                 Logging.getLogger().warning(

rant: it's amazing that we reinvent the wheel all the time instead of making a 
good logging facade a part of java.

in this case, can the log output be disabled?  I suspect even when disabled, 
the `formatted()` will be called.

related: is `MessageFormat` a better choice for logging?

modules/javafx.base/src/main/java/com/sun/javafx/binding/ListenerListBase.java 
line 284:

> 282:             CHANGE_LISTENERS.add(this, listener);  // even invalidation 
> listeners go into this list when locked!
> 283:         }
> 284:         else {

} else {

modules/javafx.base/src/main/java/com/sun/javafx/binding/ListenerListBase.java 
line 308:

> 306:                 nulledListenerCount++;
> 307:             }
> 308:             else {

} else {

modules/javafx.base/src/main/java/com/sun/javafx/binding/ListenerListBase.java 
line 319:

> 317:                     CHANGE_LISTENERS.remove(this, index);  // not 
> locked, or was added during lock, so can just remove directly
> 318:                 }
> 319:                 else {

} else {

modules/javafx.base/src/main/java/com/sun/javafx/binding/ListenerListBase.java 
line 350:

> 348:             Object listener = CHANGE_LISTENERS.get(this, i);
> 349: 
> 350:             if (listener instanceof InvalidationListener il) {

a listener might implement both `InvalidationListener` and `ChangeListener`, 
the logic here will break.

modules/javafx.base/src/main/java/com/sun/javafx/binding/ListenerListBase.java 
line 372:

> 370:              * - The notification ends
> 371:              * - While unlocking, a weak listener is determined to be 
> expired
> 372:              *   - Removing this weak listener would not go through 
> proper channels and thus

weird indent

modules/javafx.base/src/main/java/com/sun/javafx/binding/ListenerListBase.java 
line 423:

> 421: 
> 422:     private int totalSize() {
> 423:         return isLocked() ? lockedSize : invalidationListenersCount + 
> changeListenersCount;

what happens when an invalidation listener removes all the change listeners 
while it's locked?  the totalSize() will report incorrect value, breaking 
downstream logic, right?

modules/javafx.base/src/main/java/com/sun/javafx/binding/ListenerManager.java 
line 73:

> 71:     }
> 72: 
> 73:     private void addListenerInternal(I instance, Object listener) {

what will happen when I add a listener object that implements both 
`InvalidationListener` and `ChangeListener`?

what will happen when I remove it?

modules/javafx.base/src/main/java/com/sun/javafx/binding/ListenerManager.java 
line 135:

> 133:             callMultipleListeners(instance, list, oldValue);
> 134:         }
> 135:         else if (listenerData instanceof InvalidationListener il) {

} else if {

modules/javafx.base/src/main/java/com/sun/javafx/binding/ListenerManager.java 
line 138:

> 136:             ListenerListBase.callInvalidationListener(instance, il);
> 137:         }
> 138:         else if (listenerData instanceof ChangeListener) {

} else if {

modules/javafx.base/src/main/java/com/sun/javafx/binding/ListenerManager.java 
line 165:

> 163:                 setData(instance, list.getInvalidationListener(0));
> 164:             }
> 165:             else if (changeListenersSize == 1) {

... it must be your style, but it throws my pattern recognition.  if no one 
else objects, you can ignore my pain.

modules/javafx.base/src/main/java/com/sun/javafx/binding/ListenerManager.java 
line 173:

> 171:         }
> 172:     }
> 173: }

missing newline

-------------

PR Review: https://git.openjdk.org/jfx/pull/1081#pullrequestreview-4665176415
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3553775930
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3553779563
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3553787329
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3553910368
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3553473800
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3553496240
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3553915605
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3553929567
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3554280779
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3554304809
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3554305757
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3554306768
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3554313509
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3554356428
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3553724159
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3553459079
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3554379406
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3554380854
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3554395189
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r3554383337

Reply via email to