From 00a87444218221944b45a7481a2d5d999be8f370 Mon Sep 17 00:00:00 2001 From: Viacheslav Kulish <32914239+vcoolish@users.noreply.github.com> Date: Tue, 1 Oct 2024 03:18:16 -0700 Subject: [PATCH] Kotlin multiplatform leaking memory (#4037) * Add deinit for KMP iOS and JVM targets * Add deinit for JS target * Add deinit for JS target * Fix JVM native name * Reuse one thread on JVM --------- Co-authored-by: satoshiotomakan <127754187+satoshiotomakan@users.noreply.github.com> --- .../lib/templates/kotlin/android_class.erb | 9 ++++ codegen/lib/templates/kotlin/ios_class.erb | 6 +++ .../templates/kotlin/js_accessors_class.erb | 3 ++ codegen/lib/templates/kotlin/js_class.erb | 15 ++++++ .../core/GenericPhantomReference.kt | 47 +++++++++++++++++++ 5 files changed, 80 insertions(+) create mode 100644 kotlin/wallet-core-kotlin/src/commonAndroidJvmMain/kotlin/com/trustwallet/core/GenericPhantomReference.kt diff --git a/codegen/lib/templates/kotlin/android_class.erb b/codegen/lib/templates/kotlin/android_class.erb index 3091b094ca7..e853300300c 100644 --- a/codegen/lib/templates/kotlin/android_class.erb +++ b/codegen/lib/templates/kotlin/android_class.erb @@ -9,6 +9,9 @@ actual class <%= entity.name %> private constructor( init { if (nativeHandle == 0L) throw IllegalArgumentException() +<% unless entity.methods.select{ |x| x.name == "Delete" }.empty? -%> + GenericPhantomReference.register(this, nativeHandle, ::delete) +<% end -%> } <%# Constructors -%> <%- constructors.each do |constructor| -%> @@ -52,6 +55,12 @@ actual class <%= entity.name %> private constructor( @JvmStatic @JvmName("createFromNative") private fun createFromNative(nativeHandle: Long) = <%= entity.name %>(nativeHandle) + +<% unless entity.methods.select{ |x| x.name == "Delete" }.empty? -%> + @JvmStatic + @JvmName("delete") + private external fun delete(handle: Long) +<%- end -%> <%- constructors.each do |constructor| -%> @JvmStatic diff --git a/codegen/lib/templates/kotlin/ios_class.erb b/codegen/lib/templates/kotlin/ios_class.erb index 36bc94e8276..240b2dc7f23 100644 --- a/codegen/lib/templates/kotlin/ios_class.erb +++ b/codegen/lib/templates/kotlin/ios_class.erb @@ -9,6 +9,12 @@ import kotlinx.cinterop.CPointer actual class <%= entity.name %> constructor( val pointer: CPointer>, ) { +<% unless entity.methods.select{ |x| x.name == "Delete" }.empty? -%> + @OptIn(ExperimentalStdlibApi::class) + private val cleaner = kotlin.native.internal.createCleaner(pointer) { ptr -> + TW<%= entity.name %>Delete(ptr) + } +<% end -%> <%# Constructors -%> <%- constructors.each do |constructor| -%> diff --git a/codegen/lib/templates/kotlin/js_accessors_class.erb b/codegen/lib/templates/kotlin/js_accessors_class.erb index 77645552c83..3e3577d4ead 100644 --- a/codegen/lib/templates/kotlin/js_accessors_class.erb +++ b/codegen/lib/templates/kotlin/js_accessors_class.erb @@ -8,6 +8,9 @@ external interface Js<%= entity.name %> { <%- entity.properties.each do |property| -%> fun <%= KotlinHelper.fix_name(WasmCppHelper.format_name(property.name)) %>()<%= KotlinHelper.js_return_type(property.return_type) %> <%- end -%> +<% unless entity.methods.select{ |x| x.name == "Delete" }.empty? -%> + fun delete() +<% end -%> <% entity.methods.each do |method| -%> <% next if method.name == "Delete" -%> fun <%= KotlinHelper.fix_name(WasmCppHelper.format_name(method.name)) %>(<%= KotlinHelper.js_parameters(method.parameters.drop(1)) %>)<%= KotlinHelper.js_return_type(method.return_type) %> diff --git a/codegen/lib/templates/kotlin/js_class.erb b/codegen/lib/templates/kotlin/js_class.erb index 14cd6ef5e40..5241d394cb5 100644 --- a/codegen/lib/templates/kotlin/js_class.erb +++ b/codegen/lib/templates/kotlin/js_class.erb @@ -6,6 +6,21 @@ actual class <%= entity.name %> constructor( val jsValue: Js<%= entity.name %>, ) { +<% unless entity.methods.select{ |x| x.name == "Delete" }.empty? -%> + private val finalizationRegistry = + js( + """ + new FinalizationRegistry(function(heldValue) { + heldValue.delete(); + }) + """ + ) + + init { + finalizationRegistry.register(this, jsValue) + } +<% end -%> + <%# Constructors -%> <%- constructors.each do |constructor| -%> diff --git a/kotlin/wallet-core-kotlin/src/commonAndroidJvmMain/kotlin/com/trustwallet/core/GenericPhantomReference.kt b/kotlin/wallet-core-kotlin/src/commonAndroidJvmMain/kotlin/com/trustwallet/core/GenericPhantomReference.kt new file mode 100644 index 00000000000..b6ca8dd3653 --- /dev/null +++ b/kotlin/wallet-core-kotlin/src/commonAndroidJvmMain/kotlin/com/trustwallet/core/GenericPhantomReference.kt @@ -0,0 +1,47 @@ +package com.trustwallet.core + +import java.lang.ref.PhantomReference +import java.lang.ref.ReferenceQueue + +internal class GenericPhantomReference private constructor( + referent: Any, + private val handle: Long, + private val onDelete: (Long) -> Unit, +) : PhantomReference(referent, queue) { + + companion object { + private val references: MutableSet = HashSet() + private val queue: ReferenceQueue = ReferenceQueue() + + init { + Thread { + try { + doDeletes() + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + } + }.apply { + name = "WCFinalizingDaemon" + isDaemon = true + priority = Thread.NORM_PRIORITY + start() + } + } + + fun register( + referent: Any, + handle: Long, + onDelete: (Long) -> Unit, + ) { + references.add(GenericPhantomReference(referent, handle, onDelete)) + } + + private fun doDeletes() { + while (true) { + val ref = queue.remove() as GenericPhantomReference + ref.onDelete(ref.handle) + references.remove(ref) + } + } + } +}