Skip to content

Conversation

@FranAguilera
Copy link
Contributor

@FranAguilera FranAguilera commented Oct 20, 2025

Resolves BIT-6673

Big shout out to @pyricau for the help with the investigation, repro steps and suggestions for fix!

What

Fixes a memory leak in JankStats monitoring that can occur when the SDK is initialized after UI is already displayed and there are multiple root views present (including dialogs). The SDK could attach to a dialog window instead of an activity window, causing the JankStats instance to leak when the dialog is dismissed.

The solution for now will be to pick the first valid activity window instead (as a follow up will explore into updating the product to track multiple windows)

Below the original leaktrace report

┬───
│ GC Root: Global variable in native code
│
├─ io.bitdrift.capture.events.SessionReplayTarget instance
│    Leaking: UNKNOWN
│    Retaining 3.1 kB in 104 objects
│    ↓ SessionReplayTarget.logger
│                          ~~~~~~
├─ io.bitdrift.capture.LoggerImpl instance
│    Leaking: UNKNOWN
│    Retaining 293 B in 9 objects
│    ↓ LoggerImpl.jankStatsMonitor
│                 ~~~~~~~~~~~~~~~~
├─ io.bitdrift.capture.events.performance.JankStatsMonitor instance
│    Leaking: UNKNOWN
│    Retaining 38.7 kB in 705 objects
│    application instance of com.squareup.superpos.development.T2SuperPosDevApp
│    ↓ JankStatsMonitor.jankStats
│                       ~~~~~~~~~
├─ androidx.metrics.performance.JankStats instance
│    Leaking: UNKNOWN
│    Retaining 38.5 kB in 694 objects
│    ↓ JankStats.implementation
│                ~~~~~~~~~~~~~~
├─ androidx.metrics.performance.JankStatsApi26Impl instance
│    Leaking: UNKNOWN
│    Retaining 38.5 kB in 693 objects
│    ↓ JankStatsApi24Impl.window
│                         ~~~~~~
├─ com.android.internal.policy.PhoneWindow instance
│    Leaking: YES (Window#mDestroyed is true)
│    Retaining 38.2 kB in 686 objects
│    mContext instance of com.squareup.ui.main.MainActivity with mDestroyed = true
│    mOnWindowDismissedCallback instance of com.squareup.ui.main.MainActivity with mDestroyed = true
│    mWindowControllerCallback instance of com.squareup.ui.main.MainActivity with mDestroyed = true
│    ↓ PhoneWindow.mDecor
╰→ com.android.internal.policy.DecorView instance
     Leaking: YES (ObjectWatcher was watching this because com.android.internal.policy.DecorView received View#onDetachedFromWindow() callback and View.mContext references a destroyed activity)
     Retaining 4.6 kB in 92 objects
     key = 7eaef5ab-b2b1-4c84-a9ab-abf4f4779801
     watchDurationMillis = 5268
     retainedDurationMillis = 263
     View not part of a window view hierarchy
     View.mAttachInfo is null (view detached)
     View.mWindowAttachCount = 1
     mContext instance of com.android.internal.policy.DecorContext, wrapping activity com.squareup.ui.main.MainActivity with mDestroyed = true

Verification

Test steps

  • Verify the leak no longer reproduces on the leaky app repo provided by @pyricau . For setup; I've used maven local to point to a local version of capture-sdk (see instructions here)
  • On our gradle-test-app. Added a new SecondActivity that mimic setup from the repo above, this will setup jank instance on SecondActivity. When we navigate back to MainActivity, we can verify that the we detach and retach for the proper activity now using the proper onActivityResumed/onActivityPaused callbacks

e.g. logs

2025-10-20 12:24:41.999 11220-11220 JankStatsLeakFix        io.bitdrift.gradletestapp            D  should start now with enableLeakFix=true
2025-10-20 12:24:42.145 11220-11220 JankStatsLeakFix        io.bitdrift.gradletestapp            D  LeakFix Enabled. Attempting to resolve current Activity from root views
2025-10-20 12:24:42.145 11220-11220 JankStatsLeakFix        io.bitdrift.gradletestapp            D  Current window is com.android.internal.policy.PhoneWindow@d3f0366 and activity name SecondActivity
2025-10-20 12:24:43.978 11220-11220 JankStatsLeakFix        io.bitdrift.gradletestapp            D  SecondActivityPaused detaching JankStats
2025-10-20 12:24:43.991 11220-11220 JankStatsLeakFix        io.bitdrift.gradletestapp            D MainActivityResumed attaching JankStats

Follow-up

BIT-6709. We'll explore updating the product to track multiple windows

@FranAguilera FranAguilera force-pushed the franjam/prevent-dialog-window-fix branch from 67f464d to 7a69e4e Compare October 20, 2025 11:26
@github-actions
Copy link

Size Comparison Report (x86_64)

Metric APK (KB) SO (KB)
Baseline 3310 1192
Current 3310 1192
Difference 0 0

APK size unchanged. SO size unchanged.

@FranAguilera FranAguilera force-pushed the franjam/prevent-dialog-window-fix branch from 0f16f96 to 44160e2 Compare October 21, 2025 08:01
@FranAguilera FranAguilera changed the title [Do Not Review Yet] [Android] Fix JankStats memory leak due to picking Dialog window [Android] Fix JankStats memory leak due to picking Dialog window Oct 21, 2025
@FranAguilera FranAguilera marked this pull request as ready for review October 21, 2025 08:47
@FranAguilera FranAguilera requested a review from murki October 21, 2025 08:47
setJankStatsForCurrentWindow(it.window)
}
// We are done detecting initial Application ON_CREATE, we don't need to listen anymore
processLifecycleOwner.lifecycle.removeObserver(this)
Copy link
Contributor Author

@FranAguilera FranAguilera Oct 21, 2025

Choose a reason for hiding this comment

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

Also minor change to make sure we always remove the observer upon initial ON_CREATE signal

<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="leak_canary_watcher_watch_dismissed_dialogs">true</bool>
</resources> No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

is this leftover from testing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Originally yes, but given gradle-test-app is small it would be nice to have more signal for these type of leaks moving forward. If becomes too noisy for future use case in test app we can disable the flag

override fun findFirstValidActivity(): Activity? =
getAllRootViews().firstNotNullOfOrNull { view ->
val activity = view.unwrapToActivity()
//noinspection NewApi
Copy link
Contributor

Choose a reason for hiding this comment

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

why is this needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's because the min API level for activity.isDestroyed is API 17, but the common module doesn't specify a min SDK in its build.gradle and it infers as API level 1 so the warning is shown.

Copy link
Contributor

Choose a reason for hiding this comment

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

sounds like the better fix wouldl be to make our modules all match in terms if min API

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yup I agree, updated here 5364cf012e6b7064fad6d634b399292f058ee010 following other modules

@FranAguilera FranAguilera enabled auto-merge (squash) October 21, 2025 15:20
@FranAguilera FranAguilera merged commit b5b4703 into main Oct 21, 2025
15 checks passed
@FranAguilera FranAguilera deleted the franjam/prevent-dialog-window-fix branch October 21, 2025 15:32
@github-actions github-actions bot locked and limited conversation to collaborators Oct 21, 2025
@FranAguilera FranAguilera restored the franjam/prevent-dialog-window-fix branch October 22, 2025 10:15
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants