RUM-15390: Fix memory leak in app launch#3349
Conversation
9cf862c to
cdfe385
Compare
This comment has been minimized.
This comment has been minimized.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #3349 +/- ##
===========================================
+ Coverage 71.49% 71.79% +0.30%
===========================================
Files 947 948 +1
Lines 34919 34952 +33
Branches 5921 5804 -117
===========================================
+ Hits 24965 25093 +128
+ Misses 8290 8255 -35
+ Partials 1664 1604 -60
🚀 New features to boost your workflow:
|
cdfe385 to
4fc463a
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4fc463a8b2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| val nowNs = timeProviderNs() | ||
| callback.onFirstFrameDrawn(nowNs) |
There was a problem hiding this comment.
Capture TTID timestamp at draw time
onDraw() is the event boundary for TTID, but this refactor now reads timeProviderNs() inside reportFirstFrame() after the callback has been queued, so the recorded timestamp can drift later than the actual first draw when the main thread is busy. That inflates durationNs and makes startup metrics less accurate. Capture the timestamp in onFirstDraw() and pass that fixed value into the queued runnable (as the previous implementation did).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This is a good comment. However this change is on purpose. In the original RFC I wanted to do exactly this (read here).
But at some point (I don't remember when) apparently I made a mistake and instead of using the time of when the Runnable is executed, I used the time of when onDraw was called.
There was a problem hiding this comment.
🤔 Just to understand, why don't we want to use the time when onDraw is called?
There was a problem hiding this comment.
It is explained in the link to the RFC that I mentioned above. In short - it is more precise.
4fc463a to
c106edc
Compare
| true | ||
| }.doWait(timeoutMs = FINAL_WAIT_MS) | ||
|
|
||
| LeakAssertions.assertNoLeaks() |
There was a problem hiding this comment.
I do it individually in each test, not in one place, because:
- If we do it here, some strange memory leak is detected. It is 100% not a problem in our SDK, but rather in our tests. I haven't managed to find a solution to this.
- We can't use the test rule, because it checks the leaks after
Datadog.stopis called here which clears all features includingRumFeaturethat itself clears this. It allows interstitialActivityto be garbage collected and the leak isn't detected.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c106edcd9f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| private val trackedActivities = Collections.newSetFromMap(WeakHashMap<Activity, Boolean>()) | ||
| private val firstFrameHandles = WeakHashMap<Activity, RumFirstDrawTimeReporter.Handle>() |
There was a problem hiding this comment.
just curious: Won't it be enough just to keep identityHashCode as a key? Don't we risk to have a minor leak here by keeping the key longer than needed (it will be GC-ed eventually since it is a WeakHashMap)?
But I guess onActivityDestroyed is called all the time during the normal app lifecycle.
There was a problem hiding this comment.
My current understanding is that identityHashCode should not be used as a key in hash maps.
identityHashCode returns an int. So it can represent only 2^32 distinct values. This means that for 100 000 items the probability of at least one collision is approximately 68% (see this and to calculate this). We will not have 100 000 Activities - yes, but some other things (some kind of spans maybe) - why not. So I don't think it is a good practice in general.
Also see this.
But I guess onActivityDestroyed is called all the time during the normal app lifecycle.
yes, exactly. Here I use WeakHashMap just as a precaution against some kind of mistake. It could be replaced by a normal Map.
There was a problem hiding this comment.
There was a problem hiding this comment.
That is true. My idea is that since it is just used as a key in the map, where lookup is done by first checking hashCode for a bucket and then reference comparison, then maybe we can capture some identity property of Activity instead and use it.
But as long as it is guaranteed to do a cleanup in onActivityDestroyed - it is okay.
There was a problem hiding this comment.
Looking at the reproducer: I think it is expected behavior, because obj can be GC-ed on each loop end, so system can give the int which was already seen before, but it is not attached to any object (since previous owner was already GC-ed).
There was a problem hiding this comment.
yes, you are right. This one is better https://pl.kotl.in/mp61XH2LK
There was a problem hiding this comment.
Also the interesting thing about this https://pl.kotl.in/mp61XH2LK is that every time you run it it finds the collision at exactly the same iteration 105668 - which proves the point here that some kind of pseudo-random number generator is involved in generating the identity hash code.
hamorillo
left a comment
There was a problem hiding this comment.
Left some minor comments, LGTM overall 🚀
| val nowNs = timeProviderNs() | ||
| callback.onFirstFrameDrawn(nowNs) |
There was a problem hiding this comment.
🤔 Just to understand, why don't we want to use the time when onDraw is called?
d04be48 to
737adf2
Compare
What does this PR do?
Fixing the memory leak.
How it happens:
Activitythat launches anotherActivityin itsonCreatemethod.Activity.Activitycallsfinishin itsonCreate, thusremoveListenerfor it is never called here.Activityis leaked here (ActivityimplementsWindow.Callback).Changes in the PR
Handlethat makes it possible to unsubscribe theActivityinonActivityDestroyed.The number of lines in the diff is large, but mostly because of the tests.
Motivation
What inspired you to submit this pull request?
Additional Notes
Anything else we should know when reviewing?
Review checklist (to be filled by reviewers)