-
Notifications
You must be signed in to change notification settings - Fork 9
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
Fix #125 Refreshable Performance #130
Conversation
Generate changelog in
|
6936fea
to
f18c6b4
Compare
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.
Just some comments. I do wonder what are you aiming for here, what's acceptable in terms of performance?
hoping to resolve the problem represented by slowMap benchmark, where slow code in hot paths becomes exceedingly slow. This change reduces avg time from 5 seconds to 2.5 milliseconds. Raw map calls are much more consistent as well. |
Ah I never linked this PR to the #125 issue! That issue has some context based on a few internal P0s. |
e05d406
to
5e11545
Compare
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.
Releasing comments so far
This results in better performance when the `map` function is executed in parallel because new `map` calls aren't blocked on the potentially expensive transformation function completing. There's some risk of spaghetti refreshable graphs causing issues if a subscriber attempts to update itself because lock upgrades are not supported, only downgrades.
The tests will need some awaitility since the cleaner runs on another thread.
0fdd03d
to
4a68e29
Compare
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 have spent way too much time on this, but I like reviewing and understanding code like this. My suggestions are on this PR #144, I don't think any of them are blocking (except for renames where you changed responsibilities of classes), and I haven't been able to find fault in any of the code here and I spent about 1.5h 🕐 just grokking all the different responsibilities and flows this class has.
|
||
@GuardedBy("readLock") | ||
private Disposable subscribeToSelf(Consumer<? super T> subscriber) { |
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.
For documentation purposes and error prevention, would you consider doing:
private Disposable subscribeToSelf(SideEffectSubscriber<? super T> subscriber) {
return subscribeToSelfImpl(subscriber);
}
private Disposable subscribeToSelf(SelfRemovingMapSubscriber subscriber) {
return subscribeToSelf(subscriber);
}
private Disposable subscribeToSelfImpl(Consumer<? super T> subscriber) {
...
}
We never want unsafe subscriber to be passed in here.
@@ -190,43 +239,20 @@ public void accept(T value) { | |||
private final WeakReference<DefaultRefreshable<R>> childRef; | |||
private final Function<T, R> function; | |||
|
|||
@Nullable | |||
private Disposable cleanUpSubscription = null; | |||
|
|||
private SelfRemovingMapSubscriber(Function<T, R> function, DefaultRefreshable<R> child) { |
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 class name is no longer accurate: it is not responsible for removing itself at all!
R initialChildValue = function.apply(current); | ||
DefaultRefreshable<R> child = createChild(initialChildValue); | ||
|
||
SelfRemovingMapSubscriber<? super T, R> mapSubscriber = new SelfRemovingMapSubscriber<>(function, child); |
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.
Would you consider refactoring a tiny method to make the it abundantly clear that we abide by the cleaner contract for register which states that the clearning Runnable should never reference the object it is supposed to clean up after? (code written in browser so might not be 100% correct, but I hope you get what I mean).
private Disposable subscribeMapper(WeakReference<SettableRefreshable<R>> child, Function<? super T, R> function) {
SelfRemovingMapSubscriber<? super T, R> mapSubscriber = new SelfRemovingMapSubscriber<>(function, child);
return subscribeToSelf(mapSubscriber);
}
Refusing to release a major rev - please do this manually at https://autorelease.bots/palantir/refreshable |
Before this PR
coarse locking, java8, no cleaners
Before benchmarks:
After this PR
Minimum java 11 (for the new Cleaner API), Read/Write locks
After benchmarks (from #129):
Read/Write locking
Readers do not need an exclusive lock, this results in a significant improvement in the
slowMap
benchmark results over 1.2.0.Writers are given exclusive access and may result in jitter, but refreshable updates tend to be relatively few and far between unless there are object equality problems.
Cleaner
The Cleaner uses a background thread to dispose of objects when they're no longer referenced. This allows us to move work off threads which request updates in favor of the single background thread. Note that it's possible many threads may concurrently map or subscribe while only one is cleaning up references, so in an exceedingly hot loop this can result in memory pressure. However, in the original implementation we were limited to a single thread at any given time, it was just borrowed from a caller, and worked under an exclusive lock to prevent other threads from mapping in parallel.
==COMMIT_MSG==
Refreshable Performance improvements to prevent services from grinding to a halt when they use Refreshable in unintended ways. This change requires a java 11 minimum version.
==COMMIT_MSG==
Possible downsides?
Oh goodness yes! Instead of blocking on locks when
map
functions are provided we can spin along quickly and happily until we OOM because there's a single cleaner thread which cannot clean as fast as N other threads callmap
. This scenario was already broken, and it's not likely we're in loops that hot, so I would consider this a reasonable trade-off.Concurrency is hard, this likely introduces a bug or two that we'll have to discover and fix. It dulls some sharp edges so that some types of misuse are less problematic, so the trade-off is likely reasonable.
Bumps to java 11
Alternatives
We could produce 'lazy' refreshable instances from the
map
function which don't require subscriptions on their parents untilsubscribe
is called on the instance or one of its children. The vast majority of refreshables are never subscribed but only used as convenient fluent suppliers. This approach would present some serious risks: today eachcurrent
orget
call is guaranteed to be immediate, at worst a volatile read, however a lazy implementation would execute the mapping function on the calling thread, potentially blocking other callers, or impacting subscriber update order. I'm not comfortable with that sort of change at the moment, but I suppose I could be convinced otherwise -- I think the risks and additional complexity outweigh the upside compared to the optimization in this PR.