Search code examples
performancekotlinmemoryjvmcompose-multiplatform

native memory leak of compose multiplatform


We have a native memory issue in our kotlin compose multiplatform application which targets jvm on desktop (using amazon java as runtime) and android. In order to fully benchmark our performance, I chose to repeat a set of expected user interactions on our app, in a fixed amount of time with the same speed, and then profile our performance. On android, our RAM usage this:

Android

enter image description here

Our problem is on desktop platform (linux, windows, macOs, they all have the same problem ). As I said, Heap + non-heap memory seems to be working just fine, graphed by different tools, and tested by playing with -Xmx values. I am sure there is no heap memory leak.

Heap

[Heap ram usage captured with jProfiler](https://i.sstatic.net/Tp9YMXwJ.png)

My non-heap memory ( codecash, heap space) is also just fine.

but when I measure the actual memory footage of the process:

Native

[Native ram footage on OS](https://i.sstatic.net/lQCodjB9.png) It is increased to 1.2 gigabytes!

Java NMI

I tried Java NMT , and I got the following:

java native memory capture with jcmd

Edit: More Info:

  • Memory growth seems to be having a limit( It's growth rate decreases overtime )
  • We do not have any JNI code or any native library access in our own code, but the library we use for our UI ( compose multiplatfofm ) uses skiko ( skio for kotlin ) dll and this is the only dll there is alongside jar files in build output.
  • I've tested and had the same problem in jdk 17 and 21 ( microsoft and correto builds )
  • No jvm startuo argument and everything is default
  • The app does not crash and has no OutOfMemory exceptions
  • we use these kotlin multiplatfom libraries : compose multiplatform, kotlinx-coroutines, kotlinx.io, voyager, koin, SQLDelight, BuildConfig.

jemalloc

I also used jemalloc profiler: enter image description here

Questions

  • I really think there is sth wrong with that ParagraphBuilder node. How can I actually map this to my kotlin source code and packages to identify the root of problem?
  • What third party libraries or compose APIs should I be suspicious of?
  • What else can I do to actually find the root of my native memory lea and solve it?

Solution

  • I ended up watching this resource. In linux, after using pmap in terminal, and sorting the output based on PSS memory usage ( which I had read was a better way that RSS in this article ), I realized there are many memory sections around 65 megabytes in my process:

    enter image description here

    If such thing happens ( you have so many blocks with 2^16 bytes of memory ), this probably means there is something wrong with the standard memory allocator that is malloc(Which seems to happen because ByteBuffer type in java uses memory directly with malloc and there is problem when releasing it ). So I changed malloc to jemalloc ( and after that to mimalloc ) and it worked! Right now our process has no longer these memory sections. although It's native memory usage can stil reach the same 1.2 limit, it generously frees up resources at specific points ( and drops to a baseline of 450 MB ) , as compared to when it did not really free anything as in malloc.

    Update: After further investigation, we I understood that the problem was with the skiko-windows-x64.dll ( a shared dll used by basically the entire compose multiplatform library ). This file manages all it's memory allocations by itself, and can not be tracked by usual java memory profiling tools ( like jprofiler, yourkit, jconsole or even with java native memory tracking ). Seems like our code is using the compose Canvas function in a way that makes the .dll consume so much memory, and seems like mimalloc and jemalloc could handle this pattern of memory usage better than malloc.