[K/N] New MM migration guide
This commit is contained in:
committed by
Space
parent
92fac304e7
commit
db34f67c1c
@@ -0,0 +1,149 @@
|
||||
# New memory model migration guide
|
||||
|
||||
**NOTE**: _The new MM is still in an experimental stage. It's **not** production-ready._
|
||||
|
||||
In the new MM we are lifting restrictions placed on object sharing: there's no need to freeze objects to share them
|
||||
between threads.
|
||||
|
||||
In particular:
|
||||
* Top level properties can be accessed and modified by any thread without the need to use `@SharedImmutable`.
|
||||
* Objects passing through interop can be accessed and modified by any thread without the need to freeze them.
|
||||
* `Worker.executeAfter` will no longer require `operation` to be frozen, and `Worker.execute` will no longer require
|
||||
`producer` to return an isolated object subgraph.
|
||||
|
||||
A few caveats:
|
||||
* As with the previous MM, memory is not reclaimed eagerly: an object is reclaimed only when GC happens. This extends
|
||||
to Swift/ObjC objects that crossed interop boundary into Kotlin/Native.
|
||||
* `AtomicReference` from `kotlin.native.concurrent` still requires freezing the `value`. `FreezableAtomicReference`
|
||||
can be used instead, or, alternatively, `AtomicRef` from `atomicfu` can be used (**NOTE**: _`atomicfu` has not reached 1.x yet_).
|
||||
* `deinit` on Swift/ObjC objects (and the objects referred by them) will be called on a different thread if these objects
|
||||
cross interop boundary into Kotlin/Native.
|
||||
* When calling Kotlin suspend functions from Swift, completion handlers might be called on threads other than the main.
|
||||
|
||||
Together with the new MM we are bringing in another set of changes:
|
||||
* Global properties are initialized lazily, when the file they are defined in is first accessed. Previously global properties were
|
||||
initialized at the program startup.
|
||||
This is in line with Kotlin/JVM. As a workaround, properties that must be initialized at the program start can be marked with `@EagerInitialization`
|
||||
(please, consult the docs for `@EagerInitialization` before using).
|
||||
* `by lazy {}` properties support thread safety modes and do not handle unbounded recursion. This is in line with Kotlin/JVM.
|
||||
* Exceptions escaping `operation` in `Worker.executeAfter` are processed like in other parts of the runtime:
|
||||
by trying to execute a user-defined unhandled exception hook, or terminating the program if the hook was not found or
|
||||
failed with exception itself.
|
||||
|
||||
## Enable the new MM
|
||||
|
||||
**NOTE**: _The new MM is still in an experimental stage. It's **not** production-ready._
|
||||
|
||||
### Update the Kotlin/Native compiler
|
||||
|
||||
Update to Kotlin/Native 1.6.0-dev (**TODO**: specify the exact version) and enable dev repositories (**TODO**: Remove after we update to M1):
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
|
||||
repositories {
|
||||
maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev")
|
||||
}
|
||||
|
||||
// settings.gradle.kts
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev")
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Switch to the new memory model
|
||||
|
||||
Add compilation flag `-Xbinary=memoryModel=experimental`. With `gradle` it's enough to append this line to `gradle.properties`:
|
||||
```properties
|
||||
kotlin.native.binary.memoryModel=experimental
|
||||
```
|
||||
|
||||
Alternatively,
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
|
||||
kotlin.targets.withType(KotlinNativeTarget::class.java) {
|
||||
binaries.all {
|
||||
binaryOptions["memoryModel"] = "experimental"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Update the libraries
|
||||
|
||||
To fully take advantage of the new MM, newer versions of libraries were released:
|
||||
* `kotlinx.coroutines`: `1.5.1-new-mm-dev2` at https://maven.pkg.jetbrains.space/public/p/kotlinx-coroutines/maven
|
||||
* No freezing, every common primitive (Channels, Flows, coroutines) work through worker boundaries.
|
||||
* `Dispatchers.Default` is backed by a pool of workers on Linux and Windows, and by a global queue on Apple targets.
|
||||
* `newSingleThreadContext` to create coroutine dispatcher backed by a worker.
|
||||
* `newFixedThreadPoolContext` to create coroutine dispatcher backed by a pool of `N` workers.
|
||||
* `Dispatchers.Main` backed by main queue on Darwin and by standalone worker on other platforms. **NOTE**: _Don't use `Dispatchers.Main` in unit-tests, because nothing is processing the main thread queue in unit-tests._
|
||||
* `ktor`: `1.6.2-native-mm-eap-196` at https://maven.pkg.jetbrains.space/public/p/ktor/eap
|
||||
|
||||
Older versions (including `native-mt` for `kotlinx.coroutines`) could still be used, and the existing code will work just like with the previous MM.
|
||||
|
||||
## Performance issues
|
||||
|
||||
For the first preview we are using the simplest scheme for garbage collection: single-threaded stop-the-world
|
||||
mark-and-sweep algorithm, which is triggered after enough functions, loop iterations and allocations were executed. This greatly hinders
|
||||
the performance, and one of our top priorities now is addressing these performance problems.
|
||||
|
||||
We don't yet have nice instruments to monitor performance of the GC, so for now diagnosing requires looking at GC logs.
|
||||
To enable the logs add compilation flag `-Xruntime-logs=gc=info` compiler. Or, with `gradle`:
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
|
||||
kotlin.targets.withType(KotlinNativeTarget::class.java) {
|
||||
binaries.all {
|
||||
freeCompilerArgs += "-Xruntime-logs=gc=info"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Currently, the logs are only printed to stderr. **NOTE**: _the exact contents of the logs is subject to change._
|
||||
|
||||
A number of known performance issues:
|
||||
* Since the collector is single-threaded stop-the-world, the pause time of every thread linearly depends on the number of
|
||||
objects in the heap. The more objects that are kept alive, the longer pauses will be. Large pauses on the main thread
|
||||
can result in laggy UI event handling. Both the pause time and the amount of objects in the heap are printed to the logs for each
|
||||
cycle of GC.
|
||||
* Being stop-the-world also means that all threads with Kotlin/Native runtime active on them need to synchronize at the same
|
||||
time in order for the collection to begin. This also affects the pause time.
|
||||
* There is a complicated relationship between Swift/ObjC objects and their Kotlin/Native counterparts, that causes Swift/ObjC objects
|
||||
to linger longer than necessary, which means that their Kotlin/Native counterparts are kept in the heap for longer, contributing
|
||||
to the slower collection time. This typically doesn't happen, but in some corner cases, for example, when
|
||||
there's a long loop, that on each iteration creates a number of temporary objects that cross the Swift/ObjC
|
||||
interop boundary (e.g. calling a kotlin callback from a loop in swift or vice versa).
|
||||
In the logs there's a number of stable refs in the root set. If this number keeps growing, it may indicate that Swift/ObjC objects
|
||||
are not being freed when they should.
|
||||
Try putting `autoreleasepool` around loop bodies (both Swift/ObjC and Kotlin) that do interop calls.
|
||||
* Our GC triggers do not adapt to the workload: collection may be requested far more frequently than necessary, which means
|
||||
that GC time may dominate actually useful application run time and pause the threads more frequently than needed.
|
||||
This manifests in time between cycles being close (or even less) than the pause time. Both of these numbers are printed
|
||||
to the logs. Try increasing `kotlin.native.internal.GC.threshold` and `kotlin.native.internal.GC.thresholdAllocations` to force GC
|
||||
to happen less often. Note that, the exact meaning of `threshold` and `thresholdAllocations` may change in the future.
|
||||
* Freezing is currently implemented suboptimally: internally a separate memory allocation may occur for each frozen object
|
||||
(this recursively includes the object subgraph), which puts unnecessary pressure on the heap.
|
||||
* Unterminated `Worker`s and unconsumed `Future`s have objects pinned to the heap, which contributes to the pause time.
|
||||
Just like Swift/ObjC interop, this also manifests in a growing number of stable refs in the root set. To mitigate, look for
|
||||
`Worker.execute` methods being called with the resulting `Future` never being consumed (via `Future.consume` or `Future.result`) and
|
||||
make sure to either consume the `Future` or replace calls with `Worker.executeAfter` instead. Also look for `Worker`s that were
|
||||
`Worker.start`ed, but were never stopped via `Worker.requestTermination()` (also note that this call also returns a `Future`).
|
||||
And finally, make sure that `execute` and `executeAfter` is only called on `Worker`s that were `Worker.start`ed or if the receiving
|
||||
worker manually processes events with `Worker.processQueue`.
|
||||
|
||||
## Known bugs
|
||||
|
||||
* Compiler caches are not supported, so compilation of debug binaries will be slower.
|
||||
* Freezing machinery is not thread-safe: if an object is being frozen on one thread, and its subgraph is being modified
|
||||
on another, by the end the object will be frozen, but some subgraph of it might be not.
|
||||
* Documentation is not updated to reflect changes for the new MM.
|
||||
* There's no handling of application state on iOS: if application goes into the background, the collector will not be
|
||||
throttled down; on the other hand the collection is not forced upon going into the background, which leaves
|
||||
the application with a larger memory footprint than necessary, making it a more likely target to be terminated by the OS.
|
||||
* WASM (or indeed any target that does not have pthreads) is not supported with the new MM.
|
||||
|
||||
**TODO**: A place to submit feedback
|
||||
Reference in New Issue
Block a user