RFR: 8274771: Map, FlatMap and OrElse fluent bindings for ObservableValue [v3]
John Hendrikx
jhendrikx at openjdk.java.net
Wed Jan 5 13:28:12 UTC 2022
On Tue, 4 Jan 2022 03:28:57 GMT, Nir Lisker <nlisker at openjdk.org> wrote:
> 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`?
That would need to be very well tested. There are some noticeable differences in behavior vs the standard bindings:
1) Standard bindings can more easily be GC'd (accidentally usually, but it could be intentional), take for example:
textProperty.addListener((obs, old, current) -> System.out.println(current));
textProperty.concat("A").addListener((obs, old, current) -> System.out.println(current));
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 `concat` results in an `ObservableValue` that is weakly bound. Compare this to:
textProperty.map(x -> x + "A").addListener((obs, old, current) -> System.out.println(current));
This keeps working and will not be GC'd by accident.
2) Standard bindings will always cache values. This means that when `getValue` is called, it will just return the cached value instead of calling `computeValue`. If `computeValue` is really expensive (unwise since this happens on the FX thread) then this cost is paid each time for Lazy bindings, at least when they're not bound to anything else (unobserved) and you are just using `getValue`. Furthermore, for a chain of Lazy bindings that is unobserved, this would propagate through the entire chain. As soon as they're observed though, they become non-lazy and values are cached.
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 `println` lambda. This is in contrast to traditional bindings which can be garbage collected when unreferenced by user code even if actively used(!!). This is a huge gotcha and one of the biggest reasons to use the lazy model.
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 `Node` had created an `isShowing` property which bounds to its `Scene` and `Window`. This looks harmless, until you realize that a listener is created on these properties for each `Node` in existence. If a `Scene` has tens of thousands of `Node`s then that means that the `Scene#windowProperty` will have a listener list containing tens of thousands of entries. This list is not only expensive to change (when a node is added or removed) but also expensive to act on (when the scene, window or its showing state changes). And for what? Almost nobody was actually using this property, yet listeners had to be added for each and every `Node`. In effect with lazy bindings, it is much cheaper now to create properties that create convenient calculated values which will only register listeners or compute
their values when in actual use.
Con: lazy bindings never become valid when not observed. This means that `getValue` will always have to recompute the value as the value is not cached. However, if you register an invalidation listener the binding becomes observed and it will start behaving like a traditional binding sending invalidation events and caching the current value. A small "pro" here could be that this means that intermediate values in a long binding chain don't take up memory (and are prevented from GC), however that goes away as soon as the binding is observed.
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 `computeValue` is probably unwise anyway as it blocks the FX thread so making lazy binding the default behavior in well behaving code could be of only minor impact.
-------------
PR: https://git.openjdk.java.net/jfx/pull/675
More information about the openjfx-dev
mailing list