Skip to content

Fix memory leak by stopping server in pageFragment by onDestroy#18172

Merged
mikehardy merged 1 commit intoankidroid:mainfrom
DrunkenCloud:page-fragment-memory-leak-fix
Apr 2, 2025
Merged

Fix memory leak by stopping server in pageFragment by onDestroy#18172
mikehardy merged 1 commit intoankidroid:mainfrom
DrunkenCloud:page-fragment-memory-leak-fix

Conversation

@DrunkenCloud
Copy link
Contributor

@DrunkenCloud DrunkenCloud commented Mar 28, 2025

Purpose / Description

This pull request addresses a memory leak that occurred when using the AnkiServer in conjunction with PageFragment. The memory leak was solved by explicitly shutting down the AnkiServer instance in the onDestroyView()

Fixes

  • Fixes Im not sure?

Approach

The primary cause of the leak was that the AnkiServer held a direct reference to the PageFragment through the postHandler. The server, intended to run in the background, could outlive the fragment, when it was not shutdown.

To fix this, we took the following steps:

  1. Using onDestroy: In the AnkiPackageImporterFragment, the onDestroy function is used to remove the OnBackPressedCallback.

These changes ensure that the PageFragment is no longer kept alive by the AnkiServer, allowing the garbage collector to reclaim the memory.

How Has This Been Tested?

  1. LeakCanary: Used to verify that no leaks are detected when navigating to and from PageFragment instances.

Before Fix:
https://drive.google.com/file/d/1FjHFKZtKQ-J6JEIFaicgekwLEqO1HONI/view?usp=drive_link

After Fix (Apologies for splitting it across videos, LeakCanary took some time to decompile a mem dump)
https://drive.google.com/file/d/1FikDQn1mKcwQ9bggxFXjzdHMCZosTCZ8/view?usp=sharing
https://drive.google.com/file/d/1FgU01ggq7DLjnpQSTKO1Z5vn4Z1ndwpT/view?usp=sharing
https://drive.google.com/file/d/1FlOGDmmxR5qjcVq3i8H4jLv8fMYMP8zn/view?usp=sharing

Learning (optional, can help others)

Not particularly anything noteworthy. There are some other memory leaks here and there as it can be seen in the videos so I shall work on those as well

Checklist

Please, go through these checks before submitting the PR.

  • [✅] You have a descriptive commit message with a short title (first line, max 50 chars).
  • [✅] You have commented your code, particularly in hard-to-understand areas
  • [✅] You have performed a self-review of your own code
  • [✅] UI changes: include screenshots of all affected screens (in particular showing any new or changed strings)
  • [✅] UI Changes: You have tested your change using the Google Accessibility Scanner

@welcome
Copy link

welcome bot commented Mar 28, 2025

First PR! 🚀 We sincerely appreciate that you have taken the time to propose a change to AnkiDroid! Please have patience with us as we are all volunteers - we will get to this as soon as possible.

@DrunkenCloud DrunkenCloud marked this pull request as ready for review March 28, 2025 06:39
Copy link
Member

@lukstbit lukstbit left a comment

Choose a reason for hiding this comment

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

Thanks for contributing!


open class AnkiServer(
private val postHandler: PostRequestHandler,
private var postHandler: PostRequestHandler? = null,
Copy link
Member

Choose a reason for hiding this comment

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

This could potentially crash below were it is used with !!.
Isn't just calling stop() in shutdown() enough to close the server and clear the reference to the fragment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well originally I thought that postHandler will probably never be null so I just left it but you are right. Changed it back.

@DrunkenCloud DrunkenCloud requested a review from lukstbit March 31, 2025 09:17
newChunkedResponse(status, mimeType, ByteArrayInputStream(data))
}

fun shutdown() {
Copy link
Member

Choose a reason for hiding this comment

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

As the method just calls stop() we can just call stop() directly on the AnkiServer instance in the fragment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah

Copy link
Member

@lukstbit lukstbit left a comment

Choose a reason for hiding this comment

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

LGTM, thank you!

@lukstbit lukstbit added the Needs Second Approval Has one approval, one more approval to merge label Apr 1, 2025
Copy link
Member

@mikehardy mikehardy left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution
The PR description no longer matches the actual implementation so this is a bit confusing to review. Have you verified that the leak you detected previously is still addressed with the new + smaller code style?

@DrunkenCloud
Copy link
Contributor Author

DrunkenCloud commented Apr 2, 2025

Yes, it would still be fixing the leak. Changed the PR as well, my mistake, I misunderstood how I solved it.

@DrunkenCloud DrunkenCloud changed the title Fix memory leak in AnkiPackageImporterFragment by clearing postHandler reference Fix memory leak by stopping server in pageFragment by onDestroy Apr 2, 2025
Copy link
Member

@mikehardy mikehardy left a comment

Choose a reason for hiding this comment

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

Nice! Okay, so I see one last thing that may waste developer time in future:

There is an annotation here:

that suppresses LeakingThis, but it appears that your PR here is aimed at that specific leak

Can we remove that annotation now? If you try that does lint still pass when you check it locally? If not, is there some additional code that would allow it to pass (like perhaps onDestroyView setting server = null or something (no idea if that would work)?

These are the lint checks

run: ./gradlew lintPlayDebug :api:lintDebug ktLintCheck lintVitalFullRelease lint-rules:test --daemon

If there is no way to remove that Suppress then it should at least receive a comment that it does not in fact leak anymore because the server is shutdown in onDestroyView, but the lint check can't determine that so still flags it

Copy link
Member

@BrayanDSO BrayanDSO left a comment

Choose a reason for hiding this comment

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

If you want to remove the deprecation suppresion:

Index: AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt
--- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt	(revision 6979fd8bf916551a778d075c3f5b1bebbbc70f45)
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt	(date 1743604073667)
@@ -38,13 +38,12 @@
 /**
  * Base class for displaying Anki HTML pages
  */
-@Suppress("LeakingThis")
 open class PageFragment(
     @LayoutRes contentLayoutId: Int = R.layout.page_fragment,
 ) : Fragment(contentLayoutId),
     PostRequestHandler {
     lateinit var webView: WebView
-    private val server = AnkiServer(this).also { it.start() }
+    private lateinit var server: AnkiServer
 
     /**
      * A loading indicator for the page. May be shown before the WebView is loaded to
@@ -102,6 +101,7 @@
         view: View,
         savedInstanceState: Bundle?,
     ) {
+        server = AnkiServer(this).also { it.start() }
         val pageWebViewClient = onCreateWebViewClient(savedInstanceState)
         webView =
             view.findViewById<WebView>(R.id.webview).apply {

…andler reference

fix: memory leak in AnkiPackageImporterFragment due to retained postHandler reference

fix: memory leak in AnkiPackageImporterFragment due to retained postHandler reference

fix: Memory leak caused by unhandled AnkiServer Instance on deletion and removed LeakingThis Annotation
@DrunkenCloud
Copy link
Contributor Author

Implemented the suggested changes and submitted the updated PR. The @Suppress("LeakingThis") annotation has been removed, and the server initialization has been adjusted accordingly as given. Let me know if anything else needs tweaking!

Copy link
Member

@mikehardy mikehardy left a comment

Choose a reason for hiding this comment

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

LGTM - and now with one less Lint suppression, thanks!

@mikehardy mikehardy added Pending Merge Things with approval that are waiting future merge (e.g. targets a future release, CI wait, etc) and removed Needs Second Approval Has one approval, one more approval to merge labels Apr 2, 2025
@mikehardy mikehardy added this pull request to the merge queue Apr 2, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Apr 2, 2025
@mikehardy mikehardy added this pull request to the merge queue Apr 2, 2025
@mikehardy
Copy link
Member

Looks like we have a new flaky test:

CardBrowserTest > FindReplace - replaces text based on regular expression FAILED
    androidx.test.espresso.NoMatchingViewException: No views in hierarchy found matching: an instance of android.widget.TextView and view.getText() with or without transformation to match: is "⁨1⁩ note updated."

@DrunkenCloud
Copy link
Contributor Author

Saw that. Im thinking maybe its because I used onDestroyView? I am testing it locally using ./gradlew and changing to onDestroy

Merged via the queue into ankidroid:main with commit fe92a68 Apr 2, 2025
9 checks passed
@github-actions github-actions bot removed the Pending Merge Things with approval that are waiting future merge (e.g. targets a future release, CI wait, etc) label Apr 2, 2025
@github-actions github-actions bot added this to the 2.21 release milestone Apr 2, 2025
@DrunkenCloud
Copy link
Contributor Author

oh nevermind

@david-allison
Copy link
Member

david-allison commented Apr 3, 2025

Just a general flake, nothing to do with this PR (thanks for this PR!!)

@david-allison
Copy link
Member

Hi there @DrunkenCloud! This is the OpenCollective Notice for PRs merged from 2025-04-01 through 2025-04-30

If you are interested in compensation for this work, the process with details is here: https://github.com/ankidroid/Anki-Android/wiki/OpenCollective-Payment-Process#how-to-get-paid

We only post one comment per person per month to avoid spamming you, regardless of the number of PRs merged, but this note applies to all PRs merged for this month

Please understand that our monthly budget is never guaranteed to cover all claims - the cap on payments-per-person may be lower, but we try to make our process as fair and transparent as possible, we just need your understanding.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants