-
Couldn't load subscription status.
- Fork 542
8367439: Bulk change notifications for ObservableSet and ObservableMap #1885
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
👋 Welcome back mstrauss! A progress list of the required criteria for merging this PR into |
|
❗ This change is not yet ready to be integrated. |
| }); | ||
|
|
||
| observer.assertMultipleCalls(call("one", "1", "2"), call("two", "2", "3")); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the description:
The implementation is fully backwards-compatible for listeners that
are unaware of the new API. If the next() method is not called, then
all subsequent changes are delivered as usual by repeated listener
invocations.
If a listener only fetches some changes of a bulk operation (but stops
halfway through the operation), the remaining changes will also be
delivered with repeated listener invocations.
- should this be included in javadoc?
- could we add a test for this scenario?
|
/reviewers 2 reviewer |
|
@mstr2 has indicated that a compatibility and specification (CSR) request is needed for this pull request. @mstr2 please create a CSR request for issue JDK-8367439 with the correct fix version. This pull request cannot be integrated until the CSR request is approved. |
Webrevs
|
|
@mstr2 This pull request has been inactive for more than 4 weeks and will be automatically closed if another 4 weeks passes without any activity. To avoid this, simply issue a |
|
I'll try to finish the review this week. |
|
@andy-goryachev-oracle The pull request is being re-evaluated and the inactivity timeout has been reset. |
|
Before going into a full review, I'd like t ask this:
|
The bulk operation tests are excercised both for bulk retrieval and individual retrieval. See
Yes, see above. |
| if (!backingMap.containsKey(key)) { | ||
| change.nextAdded(key, newValue); | ||
| } else { | ||
| V oldValue = backingMap.get(key); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
containsKey() followed by a get() - do you think it's possible to optimize this to avoid looking up twice? maybe use get() == null ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is possible if we know that the map doesn't contain null mappings. But since ObservableMapWrapper is a wrapper around an arbitray Map, we need to account for null mappings. As a consequence of that, we don't know if map.get(key) == null means that no mapping is present, or if a mapping is present but its value is null.
| return builder.append(" at key ").append(change.getKey()).toString(); | ||
| } | ||
|
|
||
| private class SimpleChange extends MapChangeListener.Change<K,V> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could this and BulkChange classes made static?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I can do that for the existing Change implementations, but it's not directly related to bulk change notifications. Maybe another PR would be better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since you are already here... saves a pointer maybe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"since you are already here" is not usually sufficient justification for fixing an unrelated bug. So it probably makes sense to do for any new class added, but I'd leave the existing ones alone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it is a new class. But sure, I am ok to keep the code as is.
| if (addedElement != null) { | ||
| callObservers(new SimpleAddChange(addedElement)); | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor suggestion:
if (addedElement != null) {
..
} else if (addedList != null) {
..
| return true; | ||
| } | ||
|
|
||
| if (removedList != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same suggestion with else
modules/javafx.base/src/main/java/com/sun/javafx/collections/ObservableSetWrapper.java
Outdated
Show resolved
Hide resolved
| "c added at key k3", | ||
| "d added at key k4", | ||
| "e added at key k5", | ||
| "f added at key k6"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is confusing (to me): I don't see the boundary created by the next() call (i.e. between the bulk and individual changes)
or am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test demonstrates that the sequence of changes is the same, no matter the number of change listener invocations. For this purpose, we assert that the number of changes is as expected (6), but the number of invocations is only 3. In this sense, not seeing a boundary between the invocations is the point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so if I were to write a test per the public API, two things should be tested: the changes and the bulk aspect. So perhaps the toString() or the test's toString() can be modified to explicitly encode the bulk aspect. In other words, the test might look like this:
expect([k3=c, k4=d],[k5=e],[k6=f])
if the test calls next on k3 and k4, then individual events come for k5, k6.
This way you test both API functions in one test.
| @ParameterizedTest | ||
| @MethodSource("createParameters") | ||
| @SuppressWarnings("unchecked") | ||
| public void testReplaceAll(Callable<ObservableMap<String, String>> mapFactory) throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should ObservableSetTest be modified as well?
| public void testReplaceAll(Callable<ObservableMap<String, String>> mapFactory) throws Exception { | ||
| setUp(mapFactory); | ||
|
|
||
| observableMap.replaceAll((key, value) -> switch (key) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In addition to testing internal details in ObservableXxxWrapperTests, shouldn't we also test the bulk change functionality inside this test (i.e. testing the publicly accessible Set/Map variants)?
I mean, I would rather prioritize testing of the public APIs than some internal implementation.
| default -> value; | ||
| }); | ||
|
|
||
| observer.assertMultipleCalls(call("one", "1", "2"), call("two", "2", "3")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, what I think might still be missing is the tests for all bulk methods (clear, merge, putAll, replaceAll) in 3 flavors:
- the legacy (no
next(), individual events) - partial, one
next()call followed by individual events - real bulk processing (while last)
same for Sets.
|
|
||
| private final List<InvalidationListener> invalidationListeners = new CopyOnWriteArrayList<>(); | ||
| private final List<MapChangeListener<? super String, Object>> mapChangeListeners = new CopyOnWriteArrayList<>(); | ||
| private MapListenerHelper<String, Object> helper; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this change should be undertaken as a separate PR, it's a totally unrelated functionality.
|
oh, could you please merge the latest master branch? |
|
There's one behavioral change with this enhancement that I want to point out. Previously, bulk operations were just repeated invocations of their single-element counterpart, i.e. Now consider this method: public interface SetChangeListener<E> {
abstract class Change<E> {
/**
* An observable set that is associated with the change.
* @return the source set
*/
public ObservableSet<E> getSet() { ... }
}
}Previously, if a listener were to query the source set, it would always correspond to the current state of the set. |
|
|
What about the ordering of bulk changes? Should it be addressed in javadoc for bulk changes (in next())? The ordering might impact tests. |
| E[] removed = (E[])new Object[size]; | ||
| backingSet.toArray(removed); | ||
| backingSet.clear(); | ||
| callObservers(new IterableSetChange.Remove<>(this, Arrays.asList(removed))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion:
ArrayList<E> removed = new ArrayList<>(backingSet);
backingSet.clear();
callObservers(new IterableSetChange.Remove<>(this, removed));
While a
ListChangeListenercan receive notifications for bulk operations (addAll,removeAll,clear, etc.),SetChangeListenerandMapChangeListeneronly receive notifications for individual add/replace/delete operations. For example, when mappings are added to anObservableMapwithputAll(), listeners will be invoked once for each individual mapping.Since there is no way for a
SetChangeListener/MapChangeListenerto know that more changes are coming, reacting to changes becomes difficult and potentially inefficient if an expensive operation (like reconfiguring the UI) is done for each individual change instead of once for a bulk change operation.I think we can improve the situation by adding a new method to
SetChangeListener.ChangeandMapChangeListener.Change:This new method allows listener implementations to fetch all subsequent changes of a bulk operation, which can be implemented as follows:
The implementation is fully backwards-compatible for listeners that are unaware of the new API. If the
next()method is not called, then all subsequent changes are delivered as usual by repeated listener invocations.If a listener only fetches some changes of a bulk operation (but stops halfway through the operation), the remaining changes will also be delivered with repeated listener invocations.
Implementation
Bulk change retrieval is only possible if the collection implementation explicitly supports the new API. We do this for
ObservableSetWrapperandObservableMapWrapper, which accounts for most instances where observable collections are used.Progress
Issue
Reviewing
Using
gitCheckout this PR locally:
$ git fetch https://git.openjdk.org/jfx.git pull/1885/head:pull/1885$ git checkout pull/1885Update a local copy of the PR:
$ git checkout pull/1885$ git pull https://git.openjdk.org/jfx.git pull/1885/headUsing Skara CLI tools
Checkout this PR locally:
$ git pr checkout 1885View PR using the GUI difftool:
$ git pr show -t 1885Using diff file
Download this PR as a diff file:
https://git.openjdk.org/jfx/pull/1885.diff
Using Webrev
Link to Webrev Comment