Hi, I have a question on kotlin multiplatform and ...
# multiplatform
r
Hi, I have a question on kotlin multiplatform and memory management for ios and I am very grateful if someone has an hint for me here. When using classes from a kotlin multiplatform shared module in an ios app (f.e. in a local function context in a view controller) - when this view controller gets removed from memory the kotlin multiplatform sdk classes still continue living in the memory. Is there a way to also remove them from there? Is that a known issue?
โœ… 1
m
Hello, were you able to find an answer to this?
l
This is normal behavior so long only the classes and top-level objects stay there, and unused instances are eventually garbage collected.
r
Thank you for the response here ๐Ÿ™‚. The current situation is that the ios classes requesting kotlin code were already removed from the memory as expected (f.e. when view controller closed). But the kotlin classes used by the removed swift classes are still there in the memory. It looks like unused instances who still live somewhere in the memory. And the caller is not there anymore. But if you say that this is normal behavior and the garbage collector remove them automatically after some time then this is probably the answer ๐Ÿ™‚.
l
You can check that by allocating a few large arrays (e.g. of size 1M), and not reference them after creating them. They should not stay in memory.
๐Ÿ‘ 1
r
Thank you for your response @louiscad So I tried what you proposed, I created an array of kotlin multiplatform sdk classes in a local context of a function:
Copy code
func testFunc() {
        let testArray = [UcLoginRequests(), UcLoginRequests(), UcLoginRequests(), UcLoginRequests(), UcSignupRequests(), UcSignupRequests(), UcSignupRequests(), UcSignupRequests(), UcSignupRequests()]
    }
That function is inside a viewModel. It is called from a viewController. After that viewController is closed and removed from stack, also the associated viewModel is gone from the memory. But unfortunately this classes are still in the memory:
This is probably not the expected behavior you described right? This references should not be in the memory after the viewController and viewModel is removed from memory right?
l
@rb90 If I understand correctly, you call that
testFunc
Swift function that creates an array out of Kotlin objects, and the Kotlin objects stay in memory long after? How long after do you check the memory contents? Also, do you have the debugger connected all that time? I know it can delay GC on Android, I don't know if the same is true for iOS with Kotlin/Native.
r
If I understand correctly, you call thatย 
testFunc
ย Swift function that creates an array out of Kotlin objects, and the Kotlin objects stay in memory long after?
that's right ๐Ÿ‘ I check the memory by debugging the memory graph in xcode. I started that 1-2 minutes after the viewModel and the viewController calling that function are gone. And the elements are still present in the memory as you can see ๐Ÿ˜ž. I would expect them to not be removed from memory like the viewController and the viewModel. I cannot understand why they are not garbage collected when the parent classes are not there anymore
l
@rb90 Can you reproduce the same issue with a custom type that holds no reference to anything? Somthing like that:
Copy code
class WIllILeak(val justSomeRandomText: String)
๐Ÿ‘€ 1
r
Aaaah interesting - I init a "data class" from the kotlin sdk in the my test function and with this I cannot reproduce the issue: EDIT: It is reproducable with the data class ErrorObject. Unfortunately a call in the view Controller was not triggered that was why I thought that the data class does not leak ๐Ÿ˜ž:
Copy code
func testFunc() {
        let testArray = [ErrorObject(errorMessage: "hello", errorStacktrace: "a stacktrace", errorType: .dataMissingInResponseError, statusCode: nil), ErrorObject(errorMessage: "hello", errorStacktrace: "a stacktrace", errorType: .dataMissingInResponseError, statusCode: nil), ErrorObject(errorMessage: "hello", errorStacktrace: "a stacktrace", errorType: .dataMissingInResponseError, statusCode: nil), ErrorObject(errorMessage: "hello", errorStacktrace: "a stacktrace", errorType: .dataMissingInResponseError, statusCode: nil), ErrorObject(errorMessage: "hello", errorStacktrace: "a stacktrace", errorType: .dataMissingInResponseError, statusCode: nil)]
    }
ErrorObject
is a simple data class.... I also add now a simple class into my module and will try that - it takes a few minutes the build ๐Ÿ˜ - I do not use the mono repo structure...
But really what I can say in the meantime is - I really love kotlin multiplatform. It is very flexible and a nice opportunity I hope that you guys get a "release release" soon. I believe if it works as intended it will be a game changer. And also a big thank you here for the support. It is also nice to look together on something if it does not behave and work like intended....
It is 100% reproducible with a TestClass looking like you wrote. My Kotlin TestClass:
Copy code
class TestClass(val justSomeRandomText: String) { }
my test function in the viewModel:
Copy code
func testFunc() {
        let test = TestClass(justSomeRandomText: "bla")
        let testArray = [TestClass(justSomeRandomText: "bla"), TestClass(justSomeRandomText: "bla"), TestClass(justSomeRandomText: "bla"), TestClass(justSomeRandomText: "bla"), TestClass(justSomeRandomText: "bla"), TestClass(justSomeRandomText: "bla"), TestClass(justSomeRandomText: "bla")]
    }
And same behaviour, viewModel and viewController removed from the memory but the kotlin class instances still there:
@louiscad unfortunately it leaks also with the data class I was wrong that it doesn't. I updated my post above regarding that problem. So seems that kotlin multiplatform cause memory leaks on ios
l
Interestingโ€ฆ Do you see the same issue in a brand new project? Also, which Kotlin version is your project using?
r
I used the newest kotlin and android studio. I did not tried it in a brand new project until now ๐Ÿ˜•. I will give it a try ๐Ÿ™‚
l
@rb90 Writing he "newest kotlin" will never age well. I'm asking about the Kotlin version configured in the Gradle files, not the IDE version. In the Kotlin world, the IDE is "only" a tool to view the project, it's not the build tool itself like you might be used to with XCode.
r
Ah yes sorry ๐Ÿ˜‰ - I am a kotlin beginner ๐Ÿ˜•. The kotlin version used is 1.4.30
l
Can you try with the freshly released 1.5.0?
r
Of course - I will try it ๐ŸŽ‰ ๐Ÿ‘ - thanks for the hint ๐Ÿ‘
l
Another question: the Swift code allocation the array, is it called from the main thread, or is it called from somewhere else like a non main thread dispatch queue?
r
This is called from an IBAction - so main thread ๐Ÿ™‚.
Copy code
@IBAction func myAction(_ sender: Any) {
      viewModel?.testFunc()
}
๐Ÿ‘ 1
l
If the problem still reproduces with Kotlin 1.5, can you try generating a way larger list/array (so not manually, but using some code to give a size an initialize each element with a new object), with at least 100 000 items in the array? I'm asking because I think the Kotlin GC has a memory threshold where it doesn't come cleaning objects below a certain memory usage from the Kotlin/Native world
In Kotlin, it'd look like
List(100_000) { WillILeak("Hello " + Random.nextInt()) }
r
Yeah will do that ๐Ÿ‘ - of course anything what help ๐Ÿ™‚ I do my best
Oh no - it still happens with Kotlin version 1.5.0 ๐Ÿ˜ž. My test function:
Copy code
func testFunction() {
        var array = [TestClass]()
        
        for index in 0..<100000 {
            array.append(TestClass(justSomeRandomText: "abc"))
        }
    }
Memory graph:
๐Ÿ™ 1
hmmm.... okay I tried it now in the monorepoproject. I used the framework packed inside "xcode-frameworks" folder. There it does not happen anymore. It looks normal and as it should be. Normally I build the framework file with the multiplatform-swiftpackage gradle plugin. Maybe the way this plugin generates classes is the reason for that problems. The generated classes may cause this behaviour....
๐Ÿค” 1
I guess I got the reason.
multiplatform-swiftpackage
packs an
*.xcframework
file. I did that a few minutes ago on my own with xcodebuild command without
multiplatform-swiftpackage
. And the result is the same. The classes are causing leaks in a project where the generated framework is added (added manually the
*.xcframework
without swift package manager and
multiplatform-swiftpackage
).... It seems that this problem occurs when the kotlin classes are packed in an
*.xcframework
๐Ÿ˜ž. Then this leaks happen ๐Ÿ˜ข.
This was unfortunately not the reason. Same behaviour compiling the project:
Copy code
- ./gradlew clean
- ./gradlew build
- ./gradlew packForXcode
Putting that
*.framework
in an app (like f.e. described here in the docs https://kotlinlang.org/docs/mobile/integrate-in-existing-app.html#create-a-shared-module-for-cross-platform-code) cause exactly same issues - so the
*.xcframework
file seems not to be the problem when
*.framework
behaves exactly the same ๐Ÿ˜ž
Edited my post above - I forget to click the button calling the
testFunc()
when playing around with the monorepo project. In the monorepo project the bahviour is exactly the same - there are memory leaks:
What happens in that test project in the mono repoย โ˜๏ธย (the same like in my main project): โ€ข We have a ViewController "FirstScreen" โ€ข Action "Go to next screen" opens "NextViewController" (via aย 
present segue
ย ) โ€ข NextViewController has a viewModel dependency โ€ข this viewModel dependency has this test func:
Copy code
func testFunc() {
        var array = [TestClass]()
        for index in 0..<100000 {
            array.append(TestClass(justSomeRandomText: "abc"))
        }
    }
โ€ข SecondViewController has an action ("My Action") โ€ข inside @IBAction methodย 
viewModel?.testFunc()
is triggered โ€ข after that close Button is clicked which dismisses "NextViewController" -> when this vc and the viewModel is gone I would expect that also the memory is cleanup and there are no references anymore of the instantiated array containingย 
TestClass
ย objects:
l
@svyatoslav.scherbina Did you ever see such a leak in Swift<->Kotlin interop?
s
Kotlin/Native doesnโ€™t reclaim the memory immediately. Try
kotlin.native.internal.GC.collect()
to force unused memory to be reclaimed.
r
@svyatoslav.scherbina Thank you for your reply on this and the information regarding the memory handling. I would like to try your approach. Can you please tell me how to call that exactly? Where should I make that call? In the Swift Code? Or would I need to create a method in the jolting Multiplattform with that line and call this function then somewhere in my Swift code?
l
@rb90 It's not visible to Swift/Obj-C, so you'll need a Kotlin function that does it than you can call from Swift/Obj-C
r
Aaah okay, nice ๐Ÿ‘, that is good to know. In that case I will put it in a helper kotlin class and call it from Swift like you suggest. I will let you know here how it goes guys. Thank you so much for the support.
๐Ÿ‘ 2
AMAZING it is working with that magic line. The objects in the memory disappeared magically ๐Ÿ‘. Thank you so much. But also without this line they would be probably removed after some time right? From what I understand from Svyatoslav answer ๐Ÿ™‚.
l
I guess they would be removed after some memory pressure, more than after some time, but I don't know the internals of the current GC in Kotlin/Native that much. I attended a talk from Nikolay Igotti (now working on Compose dekstop stuff AFAIK) on the topic in march 2020, and shared with him the last handshake of the year after it, but I understood about a quarter of it ๐Ÿ˜…
๐Ÿ‘ 1
r
Ah okay ๐Ÿ™‚ - anyways as long as we can manage memory ourselfs if necessary with that line - it is fine. I was just curious that is why I asked ๐Ÿ˜‰.
s
I guess they would be removed after some memory pressure
Yes.
๐Ÿ‘ 1
r
Nice stuff ๐Ÿ™Œ - thank you very much for your reply on this Svyatoslav
๐Ÿ‘ 1