Skip to content
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

BoundedReplayBuffer no longer leaks memory #5282

Closed
wants to merge 3 commits into from

Conversation

AttwellBrian
Copy link

@AttwellBrian AttwellBrian commented Apr 13, 2017

The Issue: When BoundedReplayBuffer evicts an item to make room for a new item it still contains a strong reference to the evicted item.

New Test: This diff adds a new unit test to verify this behavior. Given this unit test needs to look under the hood of BoundedReplayBuffer in order to ensure no memory leaks exist, writing this test requires a new package private method.

Change: This diff fixes misbehavior.

@codecov
Copy link

codecov bot commented Apr 13, 2017

Codecov Report

Merging #5282 into 2.x will decrease coverage by 0.13%.
The diff coverage is 84.21%.

Impacted file tree graph

@@             Coverage Diff              @@
##                2.x    #5282      +/-   ##
============================================
- Coverage     96.13%   95.99%   -0.14%     
+ Complexity     5748     5746       -2     
============================================
  Files           628      628              
  Lines         41077    41088      +11     
  Branches       5699     5703       +4     
============================================
- Hits          39488    39443      -45     
- Misses          621      656      +35     
- Partials        968      989      +21
Impacted Files Coverage Δ Complexity Δ
...nternal/operators/observable/ObservableReplay.java 95.4% <84.21%> (-0.7%) 22 <0> (ø)
...ors/observable/ObservableSampleWithObservable.java 92.77% <0%> (-6.03%) 3% <0%> (ø)
...ava/io/reactivex/processors/BehaviorProcessor.java 87.61% <0%> (-5.76%) 61% <0%> (-1%)
.../operators/flowable/FlowableBlockingSubscribe.java 91.66% <0%> (-5.56%) 9% <0%> (-1%)
...in/java/io/reactivex/subjects/BehaviorSubject.java 85.93% <0%> (-4.69%) 56% <0%> (ø)
...l/operators/observable/ObservableFlatMapMaybe.java 93.46% <0%> (-3.93%) 2% <0%> (ø)
...x/internal/operators/flowable/FlowablePublish.java 92.07% <0%> (-3.09%) 9% <0%> (ø)
...ternal/operators/completable/CompletableCache.java 96.96% <0%> (-3.04%) 23% <0%> (-1%)
...ex/internal/operators/maybe/MaybeTimeoutMaybe.java 95.58% <0%> (-2.95%) 2% <0%> (ø)
...main/java/io/reactivex/subjects/SingleSubject.java 95.23% <0%> (-2.39%) 38% <0%> (-1%)
... and 29 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 85e0ea5...a772a7e. Read the comment docs.

Copy link
Member

@akarnokd akarnokd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to think about this a bit. Also do you want to fix ReplayProcessor and the replay() operators?

@@ -751,6 +757,26 @@ void truncateFinal() {
}
}
}
/* test */ final void collectObjectsInMemory(Collection<? super T> output) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a collect(Collection) method just above this addition, what's wrong with that?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collect(Collection) intentionally ignores the value in head.

setFirst(next);
Node newHead = new Node(null);
Node newNext = next.get();
newHead.set(newNext);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be lazySet.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a look at that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't lazySet() cause bugs here?

@akarnokd akarnokd added the 2.x label Apr 13, 2017
Problem: When `BoundedReplayBuffer` evicts an item to make room for a new item it still contains a strong reference to the evicted reference.

This diff adds a new unit test to verify this behavior. Given this unit test needs to look under the hood of `BoundedReplayBuffer` in order to ensure no memory leaks exist, writing this test requires a new package private method.

This diff fixes the behavior.
@AttwellBrian
Copy link
Author

I have to think about this a bit. Also do you want to fix ReplayProcessor and the replay() operators?

Why? Do these have similar problems?

I took a look at merging the `collect()` and `collectValuesInMemory()`
methods into a single method at the prompting of akarnokd. This made me
realize the following:
1) There are more memory leaks. I updated the setNext() method to fix some of these.
2) We can't avoid all these memory leaks without introducing schduling
because of the TimedBuffer. I don't believe engineers should have any
expectation that the TimedBuffer values will be eagerly cleaned up after
they timeout. So I left the seperate `collect()` and
`collectValuesInMemory()` methods. This allows us to test memory for
memory leaks only when we think it is appropriate.
@@ -617,23 +617,20 @@ final void removeFirst() {
// can't just move the head because it would retain the very first value
// can't null out the head's value because of late replayers would see null
setFirst(next);
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When testing you can just call removeFirst() repeatedly. No need to introduce a test-only method. Plus, doing this reduces some memory leak false positives.

@AttwellBrian
Copy link
Author

Updated my change so that it fixes leaks due to size truncation in both BoundedReplayBuffer and SizeAndTimeBoundReplayBuffer.

This pull request makes no attempt to fix leaks that occur due to timeouts. Fully fixing such leaks would require scheduling. Therefore, eagerly fixing these leaks is impractical. This is fine since Rx consumers won't expect timeouts to perform eager cleanup.

@akarnokd
Copy link
Member

I've thought about this and the problem with the change is that now there are two Node allocations happening per item instead of one. I would accept such change if it happened when the terminal events are received; the head no longer references +1 item for an unknown duration.

Why is this extra 1 item causing you trouble?

Why? Do these have similar problems?

There are a bunch of operators having the same replay like behavior implemented with the same node logic:

  • ReplaySubject
  • ReplayProcessor
  • ObservableReplay
  • FlowableReplay

@AttwellBrian
Copy link
Author

I've thought about this and the problem with the change is that now there are two Node allocations happening per item instead of one. I would accept such change if it happened when the terminal events are received; the head no longer references +1 item for an unknown duration.

Doesn't sound like a significant improvement. I can't think of any improvement ideas for this. Can you?

Why is this extra 1 item causing you trouble?

While Rx is designed with immutable value objects in mind, it is useful for asynchronously emitting other types as well. For example, passing around an Observable<MapUI> instead of a MapUI inside the Uber app makes it easy to continue progressing application startup while waiting for the map to load.

Optimizations like this have a large affect on application startup time. However, if something in Rx leaks MapUI we're in trouble! This will leak the entire app's Activity.

There are a bunch of operators having the same replay like behavior implemented with the same node logic

Interesting. I see this was true with Rx1 as well. So I guess this leak isn't a huge deal. It hasn't caused many issues for us in the last two years.

My takeaway is: never put objects you don't want leaked into rx caches because the cache may live longer than expected.

@akarnokd
Copy link
Member

There was a previous report when the user actually wanted 1 item cached but due to the head reference, there was 2 object retained. The workaround for that case is the cacheLast extension operator.

However, if something in Rx leaks MapUI we're in trouble!

I think caching Activities is by itself bad practice.

I'm not willing to pay this extra per item allocation in the standard operator, however, the following are acceptable changes:

  • Introduce a trim() method on the Subject and Processor that performs this head swap on demand.
  • Have this head swap only implemented for onError and onComplete paths, thus no more +1 retention after the subject/processor has terminated.

@AttwellBrian
Copy link
Author

There was a previous report when the user actually wanted 1 item cached but due to the head reference, there was 2 object retained. The workaround for that case is the cacheLast extension operator.

Neat. I'll take a look at that. Looks like it only supports Flowable for now. But I guess I can transform back and forth to a flowable.

I'm not willing to pay this extra per item allocation in the standard operator, however, the following are acceptable changes:
Introduce a trim() method on the Subject and Processor that performs this head swap on demand.
Have this head swap only implemented for onError and onComplete paths, thus no more +1 retention after the subject/processor has terminated.

Okay. I'll think about this a bit.

Thanks for the help and promptness!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants