Listeners behavior discussion

John Hendrikx john.hendrikx at gmail.com
Mon Jul 11 06:09:09 UTC 2022


I've done some preliminary testing on the limits of what's possible.

Disallowing nested changes broke about 50 tests. Changing the order of 
listener events broke 9 tests (I reversed the order, see this diff for 
what I did: 
https://github.com/hjohn/jfx/commit/c7588c49d42257f75627f15ca7ed01147d5e5e3d).

I investigated the tests that broke when the ordering changed. They're 
all Tree/Table/ListView tests. The tests breaking all seem to interact 
with the selection and focus model, so there might be some kind of 
ordering dependency in there (list of the 9 tests that broke added at 
end of this post).

Disallowing nested emissions broke far more, Spinner, ComboBox, 
ColorPicker, Scroll related things, TableCell.  I didn't run systemTests 
or web tests, so there might be even more that breaks.

Further comments inline:

On 10/07/2022 20:11, Nir Lisker wrote:
> There was a previous implementation discussion that touched on some of 
> the points here in https://github.com/openjdk/jfx/pull/108
>
> I think there are a few (semi-)independent points here:
> 1. Order of listeners: as John said, there is no specification of the 
> order of listeners, but the add/removeListener methods imply that 
> there is some sort of order by saying that the "first occurrence" will 
> be removed.
> 2. Dispatch order: the docs don't specify (from what I could find) the 
> order in which listeners receive events. That is, if listener1 and 
> listener2 are registered in that order, I didn't find anything that 
> specifies that they will be notified in this order, although It makes 
> sense that they are. From the API perspective, there is no 
> `getListeners()` method or the like that allows me to see the 
> order, so I have to "remember" that order somehow, and that's an 
> indication that the order is actually not important.
> Furthermore, the order is separate for invalidation listeners and 
> change listeners: first all invalidation listeners are notified, then 
> all change listeners, even if all the change listeners were 
> registered before. This alone can be surprising.
> 3. Concurrent modification of listeners: if a listener is added or 
> removed from another thread (while an event is being sent through the 
> listener list or otherwise), what should happen.
> 4. Nested events (relevant only for change listeners I think): if a 
> value is changed inside a listener, what should happen. The docs in 
> ChangeListener#changed mention that it is "bad practice", but don't 
> prohibit it.

Thanks, good summary there.

> I have some thoughts, but I haven't constructed an opinion yet:
> 1+2. It should be tested how much practical maneuvering room we have 
> here first. If we don't keep an ordered collection of listeners, or 
> don't dispatch the event to them in that order, or even if we dispatch 
> first to all change listeners and then to all invalidation listeners, 
> what breaks? This will give us an idea of our options.
> 3. I agree with Michael that as long as there is no mention of thread 
> safety in any of the relevant classes (observables and listeners), the 
> behavior should be unspecified, exactly as it is in core JDK classes 
> that don't specify anything. We can revisit this when the GC part of 
> the story is on the move, as John said.
> 4. I don't think we can restrict nested changes at this point 
> considering the docs, and I remember some classes in JavaFX doing this 
> (I think it was in the animation package). However, I propose to look 
> at another approach: The event dispatcher will use an iterator, and 
> whatever changes happen during iteration will reflect on it (if they 
> are relevant). It is not guaranteed that all listeners that were 
> registered *at the time of the event* will receive it, but only those 
> that are registered in time for a dispatch. It removes the hurdles of 
> locking or maintaining a queue. Also, if I have a listener1 whose job 
> is to remove a listener2 when an event is received, it's very probable 
> that I don't want listener2 to react to the event (unless it wasn't 
> registered before listener1, in which case "too late", but see also 
> points 1+2).
> If this approach is viable, it should also be tested practically.
>
As for the last point, I don't think we should change a current event 
emission. When an event occurs, I think every one that was registered 
should receive it (which is currently ensured by making a copy). Doing 
it differently (allowing the list to change during emission of a single 
event) would actually introduce ordering effects and would be a much 
harder behavior to document.

As for it removing hurdles, I think a copy free approach is already 
possible *if* we specify that changes are historical (ie. everyone at 
the time of registration receives the exact same change event, 
regardless of nested value changes or nested listener list changes).

If changes are historical, the implementation probably must delay nested 
value changes to adhere to the specification.

If we further specify (or implement in this case) that for a single 
property, no more than a single emission can run concurrently (this 
seems reasonable IMHO, especially given the fact that concurrent 
emissions probably hardly happen currently as it would break too much 
things in unexpected ways), then the implementation can buffer any list 
modifications made.

All the above taken together will allow a copy free use of listener 
list. A lock is still needed to detect a nested emission, when to buffer 
changes and to prevent emissions on another thread.

--John


ListViewTest > ensureSelectionIsCorrectWhenItemsChange FAILED
     java.lang.AssertionError: expected:<0> but was:<-1>
         at org.junit.Assert.fail(Assert.java:89)
         at org.junit.Assert.failNotEquals(Assert.java:835)
         at org.junit.Assert.assertEquals(Assert.java:647)
         at org.junit.Assert.assertEquals(Assert.java:633)
         at 
test.javafx.scene.control.ListViewTest.ensureSelectionIsCorrectWhenItemsChange(ListViewTest.java:337)

TableViewTest > ensureSelectionIsCorrectWhenItemsChange FAILED
     java.lang.AssertionError: expected:<0> but was:<-1>
         at org.junit.Assert.fail(Assert.java:89)
         at org.junit.Assert.failNotEquals(Assert.java:835)
         at org.junit.Assert.assertEquals(Assert.java:647)
         at org.junit.Assert.assertEquals(Assert.java:633)
         at 
test.javafx.scene.control.TableViewTest.ensureSelectionIsCorrectWhenItemsChange(TableViewTest.java:351)

TreeTableViewTest > 
test_rt27180_expandBranch_laterSiblingSelected_singleSelection FAILED
     java.lang.AssertionError
         at org.junit.Assert.fail(Assert.java:87)
         at org.junit.Assert.assertTrue(Assert.java:42)
         at org.junit.Assert.assertTrue(Assert.java:53)
         at 
test.javafx.scene.control.TreeTableViewTest.test_rt27180_expandBranch_laterSiblingSelected_singleSelection(TreeTableViewTest.java:2315)

TreeTableViewTest > 
test_rt27180_expandBranch_laterSiblingAndChildrenSelected FAILED
     java.lang.AssertionError
         at org.junit.Assert.fail(Assert.java:87)
         at org.junit.Assert.assertTrue(Assert.java:42)
         at org.junit.Assert.assertTrue(Assert.java:53)
         at 
test.javafx.scene.control.TreeTableViewTest.test_rt27180_expandBranch_laterSiblingAndChildrenSelected(TreeTableViewTest.java:2340)

TreeTableViewTest > test_rt27185 FAILED
     java.lang.AssertionError: expected:<TreeItem [ value: Mike Graham 
]> but was:<null>
         at org.junit.Assert.fail(Assert.java:89)
         at org.junit.Assert.failNotEquals(Assert.java:835)
         at org.junit.Assert.assertEquals(Assert.java:120)
         at org.junit.Assert.assertEquals(Assert.java:146)
         at 
test.javafx.scene.control.TreeTableViewTest.test_rt27185(TreeTableViewTest.java:1569)

TreeViewTest > 
test_rt27180_expandBranch_laterSiblingSelected_singleSelection FAILED
     java.lang.AssertionError
         at org.junit.Assert.fail(Assert.java:87)
         at org.junit.Assert.assertTrue(Assert.java:42)
         at org.junit.Assert.assertTrue(Assert.java:53)
         at 
test.javafx.scene.control.TreeViewTest.test_rt27180_expandBranch_laterSiblingSelected_singleSelection(TreeViewTest.java:1018)

TreeViewTest > 
test_rt27180_collapseBranch_laterSiblingAndChildrenSelected FAILED
     java.lang.AssertionError
         at org.junit.Assert.fail(Assert.java:87)
         at org.junit.Assert.assertTrue(Assert.java:42)
         at org.junit.Assert.assertTrue(Assert.java:53)
         at 
test.javafx.scene.control.TreeViewTest.test_rt27180_collapseBranch_laterSiblingAndChildrenSelected(TreeViewTest.java:999)

TreeViewTest > test_rt27180_expandBranch_laterSiblingAndChildrenSelected 
FAILED
     java.lang.AssertionError
         at org.junit.Assert.fail(Assert.java:87)
         at org.junit.Assert.assertTrue(Assert.java:42)
         at org.junit.Assert.assertTrue(Assert.java:53)
         at 
test.javafx.scene.control.TreeViewTest.test_rt27180_expandBranch_laterSiblingAndChildrenSelected(TreeViewTest.java:1043)

TreeViewTest > test_rt27185 FAILED
     java.lang.AssertionError: expected:<TreeItem [ value: Mike Graham 
]> but was:<null>
         at org.junit.Assert.fail(Assert.java:89)
         at org.junit.Assert.failNotEquals(Assert.java:835)
         at org.junit.Assert.assertEquals(Assert.java:120)
         at org.junit.Assert.assertEquals(Assert.java:146)
         at 
test.javafx.scene.control.TreeViewTest.test_rt27185(TreeViewTest.java:630)



More information about the openjfx-dev mailing list