-
Notifications
You must be signed in to change notification settings - Fork 552
8274771: Map, FlatMap and OrElse fluent bindings for ObservableValue #675
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
Conversation
|
👋 Welcome back jhendrikx! A progress list of the required criteria for merging this PR into |
Webrevs
|
|
This will need an API review followed by an implementation review. /csr |
|
@kevinrushforth has indicated that a compatibility and specification (CSR) request is needed for this pull request. |
|
@kevinrushforth |
|
@kevinrushforth I've created the CSR here: https://bugs.openjdk.java.net/browse/JDK-8277456 -- it is my first CSR, if you any feedback on it I'd appreciate it. |
c5d704b to
d9bfefe
Compare
|
I've rebased this PR on the current master to pull in the JUnit 5 upgrade, and I've updated the tests in this PR to use JUnit 5. |
|
I will start my review this week. |
nlisker
left a comment
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 the review of the API. I suggested also adding the examples in the POC or similar to the relevant methods.
I have already commented on the implementation while we were discussing it in the mailing list, so that review will be minimal.
I will review the tests soon, but after a quick look they seem fine.
Don't worry about the CSR for now. When all reviewers agree on the API you can copy the final result to the CSR.
Unrelated to the review, will it makes sense in the future to make all bindings lazily register listeners like LazyObjectBinding, perhaps when we introduce Subscription?
modules/javafx.base/src/main/java/javafx/beans/binding/ObjectBinding.java
Outdated
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/binding/ObjectBinding.java
Outdated
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/value/LazyObjectBinding.java
Outdated
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
That would need to be very well tested. There are some noticeable differences in behavior vs the standard bindings:
These behave very different. The first listener keeps working as you'd expect, while the second one can stop working as soon the GC runs. This is because This keeps working and will not be GC'd by accident.
In effect, Lazy bindings behave the same as standard bindings when they're observed but their behavior changes when not observed: they never become valid and they stop caching values This has pros and cons: Pro: Lazy bindings can be garbage collected when not referenced and not actively being used without the use of weak listeners. See the first example where the binding keeps working when used by Pro: Lazy bindings don't register unnecessary listeners to be aware of changed or invalidated values that are not used by anyone. A good example is the problems we saw about a year ago where Con: lazy bindings never become valid when not observed. This means that In summary: I think lazy bindings are far superior in the experience that they offer, but it does come at a cost that values may need to be recomputed every time when the bindings are unobserved. However, doing substantial work in |
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
| * @return an {@link ObservableValue} which provides a mapping of the value | ||
| * held by this {@code ObservableValue}, and provides {@code null} when | ||
| * this {@code ObservableValue} holds {@code null}, never 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.
Good idea to mention that it can hold null.
I slightly prefer to say that the returned ObservableValue holds the result of the mapping rather than holds the mapping. I don't really mind it, but it's the phrasing used in the method description "holds the result of applying a mapping". "The mapping" itself could be mistaken for the mapping Function in my opinion. If you think it's clear, you can change it to that phrasing, it's also fine.
nlisker
left a comment
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.
The changes look good. I added some minor grammar comments. I think that the API is in a good spot.
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
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.
The API looks good to me. After you get the other reviewers to look at it you can update the CSR.
As for the fluent binding tests:
- The
privatefields inObservableValueFluentBindingsTestcan befinal. - Most tests that I have seen in JavaFX use the assert overloads that include a message that explains what the value should be or what it means if the assertion failed. I don't know how much of a requirement it is. I can help write these if you want.
- There is a utility class
JMemoryBuddythat was added to JavaFX to test for memory leaks. This would have been helpful in the GC tests, but it seems that the class is not suited too well for this case (it requires you tonullyou own reference etc.). I think the way you did it is fine, but maybe that class should be updated (in a different patch) to be more welcoming for these checks.
I would also add tests of chaining the observables in common use cases. I wrote some myself to test it for map:
ObservableValue<String> observableValue1 = property.map(v -> v + "Z");
ObservableValue<String> observableValue2 = observableValue1.map(v -> v + "X");
assertEquals("AZ", observableValue1.getValue());
assertEquals("AZX", observableValue2.getValue());
property.set("B");
assertEquals("BZ", observableValue1.getValue());
assertEquals("BZX", observableValue2.getValue());
for orElse:
ObservableValue<String> observableValue1 = property.map(v -> v + "Z");
ObservableValue<String> observableValue2 = observableValue1.orElse("K");
assertEquals("AZ", observableValue1.getValue());
assertEquals("AZ", observableValue2.getValue());
property.set("B");
assertEquals("BZ", observableValue1.getValue());
assertEquals("BZ", observableValue2.getValue());
property.set(null);
assertNull(observableValue1.getValue());
assertEquals("K", observableValue2.getValue());
You can clean these up and use them or write your own. flatMap should also have one. I can clean mine up and post it if it helps (I did some dirty testing there).
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
...les/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java
Outdated
Show resolved
Hide resolved
nlisker
left a comment
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.
The LazyObjectBindingTest tests look good. I was thinking about adding a test that checks the compyteValue calls when a listener is attached (should increase) and when not attached (should not increase), but I think this behavior is already tested in ObjectBinding itself, so I'll leave it to you.
modules/javafx.base/src/main/java/javafx/beans/value/LazyObjectBinding.java
Show resolved
Hide resolved
modules/javafx.base/src/test/java/test/javafx/beans/value/LazyObjectBindingTest.java
Show resolved
Hide resolved
Rewrote and extended ObservableFluentBindingsTest for more clarity Use JMemoryBuddy in ReferenceAsserts helper
I'm a bit at a loss there, many of the asserts are think are self explanatory already, and a text describing the events that led to this assert would basically describe the same as the path of all
I've however cleaned up the entire test using different values that more clearly show what is happening (like Please let me know if that helps.
I've applied it in the helper class I was using.
I added additional nested classes for Map which adds another nesting that applies another Map or an OrElse. Same for FlatMap. I see the fluent binding test mostly as a test to ensure the basic functionality and to ensure things are getting garbage collected when they should and not when they shouldn't. If we want to test more specific cases without having to worry about GC, perhaps another test class is better suited.
That's good, I hope you didn't find anything surprising :) |
nlisker
left a comment
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.
The tests look good. I'm happy with the coverage and added one comment about a missing case. My own sanity checks also work as I expect.
Will approve when these are addressed as I have already reviewed the API and implementation.
...les/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java
Outdated
Show resolved
Hide resolved
...les/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java
Show resolved
Hide resolved
...les/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java
Outdated
Show resolved
Hide resolved
...les/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java
Show resolved
Hide resolved
...les/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java
Outdated
Show resolved
Hide resolved
...les/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java
Show resolved
Hide resolved
...les/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java
Outdated
Show resolved
Hide resolved
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.
Looks very good. I left a few minor optional comments after doing a quick re-review of everything. You can wait until the other reviews with these if you want.
...les/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java
Show resolved
Hide resolved
...les/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java
Show resolved
Hide resolved
...les/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java
Show resolved
Hide resolved
...les/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java
Show resolved
Hide resolved
...les/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java
Outdated
Show resolved
Hide resolved
...les/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java
Show resolved
Hide resolved
modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
Outdated
Show resolved
Hide resolved
You mean by I think this is a very nice idea to potentially tackle this problem. |
From @mstr2 analysis, it is slightly more than just the stub in the case of the fluent bindings. See illustration below, assuming It might be good to fix this right away. |
I think a model with the following properties would be more consistent and easier to understand than that we currently have:
The second property ensures that it will be sufficient to keep a reference to the last It is true that manually added listeners may suffer from unexpected collection, but something has to give. It seems like a reasonable trade-off to make listeners more complicated than binding expressions (which also hopefully discourages the use of manually added listeners). I would also argue that it is easier to explain that it's not the listener that's different in properties v. expressions, it's the reachability of the
Thanks for this clarification, I wasn't aware that the leaked listener is actually removed when the source property is changed. That makes the issue a little bit less concerning, but I'd still prefer a solution that doesn't require the source property to change. |
We already have |
|
I didn't get into the fine details of the GC discussion yet, but I have a few points I would like to share:
|
I've done some experimenting using a Doing 100.000 throwaway binds on a single property, this mechanism manages to keep up quite well using standard GC settings. Within seconds, all 100.000 stubs have been cleaned up (note: 100.000 is already an insane amount). If I go higher, the cleaning mechanism can't quite keep up, but that's primarily because the operation becomes quite expensive due to the large listener list involved (cleaning doesn't start immediately, and if the list of listeners managed by
From what I can see, we'd need to change all locations where If there is agreement on this approach, I could create a separate MR for this (since it could be implemented and merged before this one) or integrate the changes into this one. |
I need to read through the recent GC discussion one more time, but I like the approach of using a Cleaner to get rid of stale bindings that would otherwise be released when the base property changes. I think the cleanup could be done as a follow-on fix, in which case it wouldn't block getting this in. The cleanup should also go into JavaFX 19, but that could go in after RDP1 (whereas this feature cannot). |
|
I was concerned that the current implementation might leave behind a stale listener that is never collected. If that was the case, I would not be in favor of integrating this PR. But as it turns out, the stale listener is cleared when the source property changes, which doesn't make it as serious an issue as I originally thought. I see no problem with integrating this PR now, and fix the cleanup issue later. |
|
Remember this program? :)
I modified Performance looks now like this: The price for this is increased memory use (you can see that having 10.000.000 binding stubs took around 400 MB before, and now it takes about twice that). The extra memory use is only paid when there is more than 1 listener (as per how |
|
@hjohn If there are no more questions or concerns raised in the next few hours, you can integrate this PR. Please file the follow-up JBS bug to address the cleanup. |
|
Additionally, can you file a docs bug to clarify the GC behavior of bindings and mappings as suggested by @nlisker in point 3 of this comment? |
@hjohn If the followup work on this issue is too much and you want to focus on the implementation, you can assign it to me. |
Yes, this must be implemented for all private static class Listener implements InvalidationListener, WeakListener {
private final WeakReference<StringPropertyBase> wref;
private final Cleaner.Cleanable cleanable;
public Listener(StringPropertyBase ref, Observable observable) {
this.wref = new WeakReference<>(ref);
if (observable instanceof ConcurrentListenerAccess) {
cleanable = CLEANER.register(ref, this::removeListener);
} else {
cleanable = null;
}
}
// Note: dispose() must be called in StringPropertyBase.unbind()
public void dispose() {
if (cleanable != null) {
cleanable.clean();
} else {
removeListener();
}
}
private void removeListener() {
StringPropertyBase ref = wref.get();
if (ref != null) {
ref.observable.removeListener(this);
}
}
@Override
public void invalidated(Observable observable) {
StringPropertyBase ref = wref.get();
if (ref == null) {
dispose();
} else {
ref.markInvalid();
}
}
@Override
public boolean wasGarbageCollected() {
return wref.get() == null;
}
}
Not sure I'm following here. Do you want to implement this pattern for all property implementations? If we just want to implement it for mapped bindings, only the Note that it's not enough for the But if we do that, the lock tax must be paid on every single access to the However, I think we might get away with an implementation that only requires locking for the private volatile ConcurrentExpressionHelper<T> helper = null;
@Override
public synchronized void addListener(InvalidationListener listener) {
helper = ConcurrentExpressionHelper.addListener(helper, this, listener);
}
@Override
public synchronized void removeListener(InvalidationListener listener) {
helper = ConcurrentExpressionHelper.removeListener(helper, listener);
}
@Override
public synchronized void addListener(ChangeListener<? super T> listener) {
helper = ConcurrentExpressionHelper.addListener(helper, this, listener);
}
@Override
public synchronized void removeListener(ChangeListener<? super T> listener) {
helper = ConcurrentExpressionHelper.removeListener(helper, listener);
}
protected void fireValueChangedEvent() {
ConcurrentExpressionHelper.fireValueChangedEvent(helper);
}This implementation works by creating immutable specializations of ConcurrentExpressionHelper. When a listener is added, a new
|
|
/integrate |
|
/sponsor |
|
Going to push as commit 60c75b8.
Your commit was automatically rebased without conflicts. |
|
@kevinrushforth @hjohn Pushed as commit 60c75b8. 💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored. |
Yes, true, only making it thread-safe for that class should remove a chain of fluent bindings. I think however that it would be good to implement this for as many classes as we can as the stub cleaning is normally only triggered on invalidation/changes (and as I recently discovered, when
Yes, true, I only fixed synchronization issues in my experiment and didn't look much further than that yet.
Yeah, we shouldn't do that, it synchronizes all accesses to all property lists everywhere, it would be an easy "fix" as it's in one place, but it doesn't feel right.
I see you have been playing with improving The situation where
Yes, I think that's a good idea, I considered using And the changes in |
|
@hjohn |
@chengenzhao The only way to do this right now using the fluent bindings is like this: It is not very pretty. |
|
@hjohn textProperty.bind(Bindings.reduce(widthProperty, heightProperty, (w, h) -> "Area is "+w.doubleValue()*h.doubleValue()));my implementation @FunctionalInterface
public interface Callable2<T0, T1, T> {
T call(T0 t0, T1 t1);
}
public static <T0, T1, T> ObjectBinding<T> reduce(ObservableValue<T0> t0, ObservableValue<T1> t1, Callable2<T0, T1, T> callable) {
return javafx.beans.binding.Bindings.createObjectBinding(() -> callable.call(t0.getValue(), t1.getValue()), t0, t1);
} |
While openjdk/jfx#675 improved things quite a lot, there's still a gap in the JavaFX's binding system. As far as I know, there is no easy way (or no way at all) to make a bidirectional binding between properties of different types. So, just like my old custom bindings system, this utility fills that gap, but it's much more easier and convenient to use. Signed-off-by: palexdev <alessandro.parisi406@gmail.com>




This is an implementation of the proposal in https://bugs.openjdk.java.net/browse/JDK-8274771 that me and Nir Lisker (@nlisker) have been working on. It's a complete implementation including good test coverage.
This was based on #434 but with a smaller API footprint. Compared to the PoC this is lacking public API for subscriptions, and does not include
orElseGetor theconditionOnconditional mapping.Flexible Mappings
Map the contents of a property any way you like with
map, or map nested properties withflatMap.Lazy
The bindings created are lazy, which means they are always invalid when not themselves observed. This allows for easier garbage collection (once the last observer is removed, a chain of bindings will stop observing their parents) and less listener management when dealing with nested properties. Furthermore, this allows inclusion of such bindings in classes such as
Nodewithout listeners being created when the binding itself is not used (this would allow for the inclusion of atreeShowingPropertyinNodewithout creating excessive listeners, see this fix I did in an earlier PR: #185)Null Safe
The
mapandflatMapmethods are skipped, similar tojava.util.Optionalwhen the value they would be mapping isnull. This makes mapping nested properties withflatMaptrivial as thenullcase does not need to be taken into account in a chain like this:node.sceneProperty().flatMap(Scene::windowProperty).flatMap(Window::showingProperty). Instead a default can be provided withorElse.Some examples:
Note that this is based on ideas in ReactFX and my own experiments in https://github.com/hjohn/hs.jfx.eventstream. I've come to the conclusion that this is much better directly integrated into JavaFX, and I'm hoping this proof of concept will be able to move such an effort forward.
Progress
Issues
Reviewers
Reviewing
Using
gitCheckout this PR locally:
$ git fetch https://git.openjdk.org/jfx pull/675/head:pull/675$ git checkout pull/675Update a local copy of the PR:
$ git checkout pull/675$ git pull https://git.openjdk.org/jfx pull/675/headUsing Skara CLI tools
Checkout this PR locally:
$ git pr checkout 675View PR using the GUI difftool:
$ git pr show -t 675Using diff file
Download this PR as a diff file:
https://git.openjdk.org/jfx/pull/675.diff