On Thu, 26 Feb 2026 01:39:14 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:
>
> Remove unreliable unnecessary extra check
Hi,
From what I can see this type of behavior is inherent for an implementation in
which when event is "fired" it is delivered right in place to listeners like in
below pseudo code:
fireXXX()...
{
for each listener invoke onXXX(....)
}
It is common for all awt and swing listeners and may, in plenty of conditions
when nested firing occurs, lead to change in ordering of events. Yet, as far as
I investigated it, and I did put a lot of work into it more than decade ago, it
is a nature of in-place events firing.
As I do see it, the very crucial assumption behind in-place firing is: _"when
fireXXX returns, all listeners are notified"_. This is a strong indication of
state and data consistency and only in-place firing may do it. Like in this
pseudo code:
my_function()
{
fireXXX()
_here I can assume that all listeners reacted to XXX_
fireYYY()
_here I can assume that all listeners reacted to XXX and YYY_
}
I can assume it regardless if `my_function()` was invoked in response to event
or not.
This assumption is in a strict and conceptual conflict with _"when fireXXX is
invoked the events reach all listeners in the same order as in which events
were fired even if events are fired from inside of listener event handler"_.
Because now if my example function is invoked inside a listener those
assumptions may be false.
Any attempt to achieve the goal of delivering events in their order of
appearance will break the first assumption and it will lead to corruption of
existing programs. In many, many funny ways.
Also any reliance on _"if listener A was registered before listener B, then A
will get called before B"_ is a horribly bad idea. Please, do not even try to
suggest it. Simply do not. There is no way to ensure that ordering in any
system of some major complexity.
Any removing of "not necessary changes" in notification process is begging for
troubles. It did change, and it did changed again. Preventing that first change
from reaching the listeners is horrible idea, as listener is by definition
getting every change, not just "some".
In my practice, if I am worried about nested events and ordering problem, I do
consciously decide to "decouple" them. Instead of firing an event in place I
do, consciously, wrap them and delegate to the event pump (in case of awt -
with invokeLater). Then I do know that they will reach the destination in order
of firing, but I also do now that at any time the assumption that fireXXX
returning means that event is delivered to listeners is false.
This practice allows me to know, that when I invoke in-place fireXXX, I pay
with reordering of events for the state consistency. And when I delegate it, I
pay with guaranteed delay and wrapping for the benefit strict ordering. It is a
nice zero-one situation when I know what I can assume and what not, regardless
of what a listeners I notified decided to do.
My recommendation is: if You really plan to change in-place firing to some more
sophisticated algorithm **do not do it**. Not in a stable production code.
Recommend, inform, educate about possible side effect, alternatively add like
"orderedFireXXX" methods or something, but do not change such an essential
algorithm. As you can see above delegation is a simple method which allows
programmers using the library to decide what is important from them and what is
not. If they see problems with ordering delegation solves it. If however you
will build it in, they will have no way to ensure that if they have problems
with:
- lost "unnecessary" event, or;
- fireXXX returning without actually delivering information to listeners;
then they will have **no way** to work around it.
-------------
PR Comment: https://git.openjdk.org/jfx/pull/1081#issuecomment-4032502540