On Fri, 8 Jul 2022 21:00:39 GMT, John Hendrikx <jhendr...@openjdk.org> wrote:
>>> I've done some experimenting using a `Cleaner` suggested by @nlisker; >>> [...] >>> From what I can see, we'd need to change all locations where `bind` is >>> implemented using a weak listener, and add a `Cleaner`. >> >> Yes, this must be implemented for all `*PropertyBase` classes. We can save a >> few bytes for non-`ConcurrentListenerAccess` observables with an >> implementation like this: >> >> 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; >> } >> } >> >> >> >>> Those same classes need their add/remove listener methods synchronized >>> (putting synchronized on the static helper methods in ExpressionListener >>> doesn't look like a good idea as it would block listener changes to >>> unrelated properties). >> >> 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 `LazyObjectBinding` class needs to be thread-safe. >> >> Note that it's not enough for the `addListener` and `removeListener` methods >> to be `synchronized`. _All_ reads and writes must be protected, which (in >> the case of `LazyObjectBinding`) includes the `invalidate` and `isObserved` >> methods. >> >> But if we do that, the lock tax must be paid on every single access to the >> `ExpressionHelper` field (because `ExpressionHelper` is not thread-safe). >> Locking any and all usages of `ExpressionHelper` can have performance >> implications that we need to evaluate carefully. >> >> However, I think we might get away with an implementation that only requires >> locking for the `addListener`/`removeListener` methods, but (crucially) not >> for `fireValueChangedEvent`: >> >> 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](https://gist.github.com/mstr2/1efc9e866f81622253711a963bd272fc). >> When a listener is added, a new `ConcurrentExpressionHelper` instance is >> created, which doesn't interfere with an existing instance that is currently >> in use by `ConcurrentExpressionHelper.fireValueChangedEvent`. >> The`ConcurrentExpressionHelper.Generic` specialization is mutable, but uses >> the lock-free `ConcurrentLinkedQueue` to store its listeners. >> >> `ConcurrentExpressionHelper` avoids locking the (probably most frequently >> invoked) `fireValueChangedEvent` method, but sacrifices cache locality when >> a large number of listeners is added. We should probably benchmark all of >> these proposals before deciding on a solution. > >> 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 `LazyObjectBinding` class needs to be thread-safe. > > 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 `ExpressionHelper` > resizes its backing list). > >> Note that it's not enough for the `addListener` and `removeListener` methods >> to be `synchronized`. _All_ reads and writes must be protected, which (in >> the case of `LazyObjectBinding`) includes the `invalidate` and `isObserved` >> methods. > > Yes, true, I only fixed synchronization issues in my experiment and didn't > look much further than that yet. > >> But if we do that, the lock tax must be paid on every single access to the >> `ExpressionHelper` field (because `ExpressionHelper` is not thread-safe). >> Locking any and all usages of `ExpressionHelper` can have performance >> implications that we need to evaluate carefully. > > 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. > >> However, I think we might get away with an implementation that only requires >> locking for the `addListener`/`removeListener` methods, but (crucially) not >> for `fireValueChangedEvent`: >> >> This implementation works by creating immutable specializations of >> [ConcurrentExpressionHelper](https://gist.github.com/mstr2/1efc9e866f81622253711a963bd272fc). >> When a listener is added, a new `ConcurrentExpressionHelper` instance is >> created, which doesn't interfere with an existing instance that is currently >> in use by `ConcurrentExpressionHelper.fireValueChangedEvent`. >> The`ConcurrentExpressionHelper.Generic` specialization is mutable, but uses >> the lock-free `ConcurrentLinkedQueue` to store its listeners. > > I see you have been playing with improving `ExpressionHelper` as well :) I > noticed there is a bug in the current one, where a copy of the list is made > each time when `locked` is `true`, even when the list was already copied for > the "current" lock. I ran into this problem right after I fixed the > performance issue (it wasn't obvious before as it was already very slow, but > basically an extra copy is happening in some circumstances on top of the > array copying that is done to remove an entry). > > The situation where `locked` is true doesn't happen that often for normal > code, but it happens specifically in the case when a call to `invalidated` is > cleaning up dead stubs... > >> `ConcurrentExpressionHelper` avoids locking the (probably most frequently >> invoked) `fireValueChangedEvent` method, but sacrifices cache locality when >> a large number of listeners is added. We should probably benchmark all of >> these proposals before deciding on a solution. > > Yes, I think that's a good idea, I considered using `ConcurrentLinkedQueue` > as well since we don't need a structure with index based access, but I > couldn't find one with good O performance for `remove(T)` that wouldn't > subtly change the current semantics. FWIW, here's the quick `Collection` > implementation I whipped up: > https://gist.github.com/hjohn/8fee1e5d1a9eacbbb3e021f8a37f582b > > And the changes in `ExpressionHelper` (including different locking): > https://gist.github.com/hjohn/f88362ea78adef96f3a54d97e2405076 @hjohn Sorry to append message here, but I don't know other places where we could talk about this topic Is it a good idea if Binding class provide method like this? Inspired by this PR  ------------- PR: https://git.openjdk.org/jfx/pull/675