+84
-107
@@ -1,44 +1,36 @@
|
|||||||
# New memory model migration guide
|
# New memory model migration guide
|
||||||
|
|
||||||
**NOTE**: _The new MM is still in an experimental stage. It's **not** production-ready._
|
> The new memory model is Experimental. It's not production-ready and may be changed at any time.
|
||||||
|
> Opt-in is required (see the details below), and you should use it only for evaluation purposes. We would appreciate your feedback on it in [YouTrack](https://youtrack.jetbrains.com/issue/KT-48525).
|
||||||
|
|
||||||
In the new MM we are lifting restrictions placed on object sharing: there's no need to freeze objects to share them
|
In the new memory model (MM), we're lifting restrictions on object sharing: there's no need to freeze objects to share them
|
||||||
between threads.
|
between threads anymore.
|
||||||
|
|
||||||
In particular:
|
In particular:
|
||||||
* Top level properties can be accessed and modified by any thread without the need to use `@SharedImmutable`.
|
* Top-level properties can be accessed and modified by any thread without using `@SharedImmutable`.
|
||||||
* Objects passing through interop can be accessed and modified by any thread without the need to freeze them.
|
* Objects passing through interop can be accessed and modified by any thread without freezing them.
|
||||||
* `Worker.executeAfter` will no longer require `operation` to be frozen, and `Worker.execute` will no longer require
|
* `Worker.executeAfter` no longer requires `operation` to be frozen.
|
||||||
`producer` to return an isolated object subgraph.
|
* `Worker.execute` no longer requires `producer` to return an isolated object subgraph.
|
||||||
|
* Reference cycles containing `AtomicReference` and `FreezableAtomicReference` do not cause memory leaks.
|
||||||
|
|
||||||
Also, reference cycles containing `AtomicReference` and `FreezableAtomicReference` do not cause memory leaks anymore.
|
A few precautions:
|
||||||
|
* 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.
|
||||||
A few caveats:
|
* `AtomicReference` from `kotlin.native.concurrent` still requires freezing the `value`. Instead, you can use `FreezableAtomicReference` or `AtomicRef` from `atomicfu`. _Note that `atomicfu` has not reached 1.x yet_.
|
||||||
* As with the previous MM, memory is not reclaimed eagerly: an object is reclaimed only when GC happens. This extends
|
* `deinit` on Swift/ObjC objects (and the objects they refer to) will be called on a different thread if these objects cross interop boundary into Kotlin/Native.
|
||||||
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.
|
* 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:
|
The new MM also brings another set of changes:
|
||||||
* Global properties are initialized lazily, when the file they are defined in is first accessed. Previously global properties were
|
* 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.
|
||||||
initialized at the program startup.
|
As a workaround, properties that must be initialized at the program start can be marked with `@EagerInitialization`. Before using, consult the [`@EagerInitialization`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native/-eager-initialization/) documentation.
|
||||||
This is in line with Kotlin/JVM. As a workaround, properties that must be initialized at the program start can be marked with `@EagerInitialization`
|
* `by lazy {}` properties support thread-safety modes and do not handle unbounded recursion. This is in line with Kotlin/JVM.
|
||||||
(please, consult the docs for `@EagerInitialization` before using).
|
* Exceptions that escape `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 an exception itself.
|
||||||
* `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
|
## 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 the Kotlin/Native compiler
|
||||||
|
|
||||||
Update to Kotlin/Native 1.6.0-M1-139 and enable dev repositories:
|
Update to Kotlin/Native 1.6.0-M1-139 and enable dev repositories:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// build.gradle.kts
|
// build.gradle.kts
|
||||||
|
|
||||||
@@ -56,14 +48,16 @@ pluginManagement {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Switch to the new memory model
|
### Switch to the new MM
|
||||||
|
|
||||||
Add compilation flag `-Xbinary=memoryModel=experimental`. With `gradle` it's enough to append this line to `gradle.properties`:
|
Add the compilation flag `-Xbinary=memoryModel=experimental`. In Gradle, you can alternatively do one of the following:
|
||||||
|
|
||||||
|
* In `gradle.properties`:
|
||||||
```properties
|
```properties
|
||||||
kotlin.native.binary.memoryModel=experimental
|
kotlin.native.binary.memoryModel=experimental
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively,
|
* In a Gradle build script:
|
||||||
```kotlin
|
```kotlin
|
||||||
// build.gradle.kts
|
// build.gradle.kts
|
||||||
|
|
||||||
@@ -74,44 +68,43 @@ kotlin.targets.withType(KotlinNativeTarget::class.java) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If `kotlin.native.isExperimentalMM()` returns `true`, you've successfully enabled the new memory model.
|
If `kotlin.native.isExperimentalMM()` returns `true`, you've successfully enabled the new MM.
|
||||||
|
|
||||||
### Update the libraries
|
### Update the libraries
|
||||||
|
|
||||||
To fully take advantage of the new MM, newer versions of libraries were released:
|
To take full advantage of the new MM, we released new versions of the following libraries:
|
||||||
* `kotlinx.coroutines`: `1.5.1-new-mm-dev2` at https://maven.pkg.jetbrains.space/public/p/kotlinx-coroutines/maven
|
* `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.
|
* No freezing. Every common primitive (Channels, Flows, coroutines) works through `Worker` boundaries.
|
||||||
* Unlike `native-mt` version, library objects are transparent for `freeze`: if you freeze e.g. a channel, all of its internals will get frozen,
|
* Unlike the `native-mt` version, library objects are transparent for `freeze`. For example, if you freeze a channel, all of its internals will get frozen, so it won't work as expected. In particular, this can happen when freezing something that captures a channel.
|
||||||
and it won't work as expected. In particular, this can happen when freezing something that captures a channel.
|
* `Dispatchers.Default` is backed by a pool of `Worker`s on Linux and Windows and by a global queue on Apple targets.
|
||||||
* `Dispatchers.Default` is backed by a pool of workers on Linux and Windows, and by a global queue on Apple targets.
|
* `newSingleThreadContext` to create a coroutine dispatcher that is backed by a `Worker`.
|
||||||
* `newSingleThreadContext` to create coroutine dispatcher backed by a worker.
|
* `newFixedThreadPoolContext` to create a coroutine dispatcher backed by a pool of `N` `Worker`s.
|
||||||
* `newFixedThreadPoolContext` to create coroutine dispatcher backed by a pool of `N` workers.
|
* `Dispatchers.Main` is backed by the main queue on Darwin and by a standalone `Worker` on other platforms.
|
||||||
* `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._
|
_Don't use `Dispatchers.Main` in unit tests because, in unit tests, nothing processes the main thread queue._
|
||||||
* `ktor`: `1.6.2-native-mm-eap-196` at https://maven.pkg.jetbrains.space/public/p/ktor/eap
|
* `ktor`: `1.6.2-native-mm-eap-196` at https://maven.pkg.jetbrains.space/public/p/ktor/eap
|
||||||
|
|
||||||
### Using previous versions of the libraries
|
### Using previous versions of the libraries
|
||||||
|
|
||||||
You can continue using previous versions of the libraries in your project.
|
In your project, you can continue using previous versions of the libraries (including `native-mt` for `kotlinx.coroutines`). The existing code will work just like with the previous MM. The only known exception is creating a Ktor HTTP client with the default engine using `HttpClient()`. In this case, you get the following error:
|
||||||
Older versions (including `native-mt` for `kotlinx.coroutines`) could still be used, and the existing code will work just like with the previous MM.
|
|
||||||
The only known exception is creating Ktor HTTP client with default engine using `HttpClient()`. You will get
|
|
||||||
```
|
```
|
||||||
kotlin.IllegalStateException: Failed to find HttpClientEngineContainer. Consider adding [HttpClientEngine] implementation in dependencies.
|
kotlin.IllegalStateException: Failed to find HttpClientEngineContainer. Consider adding [HttpClientEngine] implementation in dependencies.
|
||||||
```
|
```
|
||||||
To fix this, specify the engine explicitly, by replacing `HttpClient()` with `HttpClient(Ios)` or other supported engine
|
|
||||||
(see [the documentation](https://ktor.io/docs/http-client-engines.html#native) for more details).
|
|
||||||
|
|
||||||
Other libraries might also have compatibility issues. If you encounter any, please report to the library authors.
|
To fix this, specify the engine explicitly by replacing `HttpClient()` with `HttpClient(Ios)` or other supported engines
|
||||||
|
(see the [Ktor documentation](https://ktor.io/docs/http-client-engines.html#native) for more details).
|
||||||
|
|
||||||
|
Other libraries might also have compatibility issues. If you encounter any, report to the library authors.
|
||||||
|
|
||||||
Known issues:
|
Known issues:
|
||||||
* SQLDelight: https://github.com/cashapp/sqldelight/issues/2556
|
* SQLDelight: https://github.com/cashapp/sqldelight/issues/2556
|
||||||
|
|
||||||
## Performance issues
|
## Performance issues
|
||||||
|
|
||||||
For the first preview we are using the simplest scheme for garbage collection: single-threaded stop-the-world
|
For the first preview, we're 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
|
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 issues.
|
||||||
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.
|
We don't have nice instruments to monitor the GC performance yet. So far, diagnosing requires looking at GC logs. To enable the logs, add the compilation flag `-Xruntime-logs=gc=info` in a Gradle build script:
|
||||||
To enable the logs add compilation flag `-Xruntime-logs=gc=info` compiler. Or, with `gradle`:
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// build.gradle.kts
|
// build.gradle.kts
|
||||||
|
|
||||||
@@ -122,65 +115,54 @@ kotlin.targets.withType(KotlinNativeTarget::class.java) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Currently, the logs are only printed to stderr. **NOTE**: _the exact contents of the logs is subject to change._
|
Currently, the logs are only printed to stderr. _Note that the exact contents of the logs will change._
|
||||||
|
|
||||||
A number of known performance issues:
|
The list 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
|
* 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 the pauses are. Long pauses on the main thread can result in laggy UI event handling. Both the pause time and the number of objects in the heap are printed to the logs for each GC cycle.
|
||||||
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
|
* Being stop-the-world also means that all threads with Kotlin/Native runtime active on them need to synchronize simultaneously for the collection to begin. This also affects the pause time.
|
||||||
cycle of GC.
|
* There is a complicated relationship between Swift/ObjC objects and their Kotlin/Native counterparts, which causes Swift/ObjC objects to linger longer than necessary. It means that their Kotlin/Native counterparts are kept in the heap longer, contributing to the slower collection time. This typically doesn't happen, but in some corner cases, for example, when a long loop creates several temporary objects that cross the Swift/ObjC interop boundary on each iteration (for example, calling a Kotlin callback from a loop in Swift or vice versa).
|
||||||
* Being stop-the-world also means that all threads with Kotlin/Native runtime active on them need to synchronize at the same
|
In the logs, there's a number of stable refs in the root set. If this number keeps growing, it may indicate that the Swift/ObjC objects are not being freed when they should. Try putting `autoreleasepool` around loop bodies (both in Swift/ObjC and Kotlin) that do interop calls.
|
||||||
time in order for the collection to begin. This also affects the pause time.
|
* Our GC triggers do not adapt to the workload: collections may be requested far more frequently than necessary, which means that GC time may dominate useful application run time and pause the threads more frequently than needed.
|
||||||
* There is a complicated relationship between Swift/ObjC objects and their Kotlin/Native counterparts, that causes Swift/ObjC objects
|
This manifests in time between cycles being close (or even less) than the pause time. Both of these numbers are printed to the logs.
|
||||||
to linger longer than necessary, which means that their Kotlin/Native counterparts are kept in the heap for longer, contributing
|
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.
|
||||||
to the slower collection time. This typically doesn't happen, but in some corner cases, for example, when
|
* 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.
|
||||||
there's a long loop, that on each iteration creates a number of temporary objects that cross the Swift/ObjC
|
* Unterminated `Worker`s and unconsumed `Future`s have objects pinned to the heap, contributing to the pause time. Like Swift/ObjC interop, this also manifests in a growing number of stable refs in the root set.
|
||||||
interop boundary (e.g. calling a kotlin callback from a loop in swift or vice versa).
|
To mitigate:
|
||||||
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
|
* Look for calls to `Worker.execute` with the resulting `Future` objects that are never consumed using `Future.consume` or `Future.result`.
|
||||||
are not being freed when they should.
|
Make sure to either consume all `Future` objects or replace these calls with `Worker.executeAfter` instead.
|
||||||
Try putting `autoreleasepool` around loop bodies (both Swift/ObjC and Kotlin) that do interop calls.
|
* Look for `Worker`s that were `Worker.start`ed, but never stopped via `Worker.requestTermination()` (also, note that this call also returns a `Future`).
|
||||||
* Our GC triggers do not adapt to the workload: collection may be requested far more frequently than necessary, which means
|
* Make sure that `execute` and `executeAfter` are only called on `Worker`s that were `Worker.start`ed or if the receiving `Worker` manually processes events with `Worker.processQueue`.
|
||||||
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
|
We measured performance regressions with a slowdown up to a factor of 5. If you observe anything more significant, report to [this performance meta issue](https://youtrack.jetbrains.com/issue/KT-48526).
|
||||||
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
|
## Known bugs
|
||||||
|
|
||||||
* Compiler caches are not supported, so compilation of debug binaries will be slower.
|
* Compiler caches are not supported, so the 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
|
* 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.
|
||||||
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.
|
* 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
|
* There's no application state handling on iOS: the collector will not be throttled down if the application goes into the background.
|
||||||
throttled down; on the other hand the collection is not forced upon going into the background, which leaves
|
However, 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.
|
||||||
the application with a larger memory footprint than necessary, making it a more likely target to be terminated by the OS.
|
* WASM (or any target that doesn't have pthreads) is not supported with the new MM.
|
||||||
* WASM (or indeed any target that does not have pthreads) is not supported with the new MM.
|
|
||||||
|
|
||||||
## Workarounds
|
## Workarounds
|
||||||
|
|
||||||
### Unexpected object freezing
|
### Unexpected object freezing
|
||||||
Some libraries might not be ready for the new memory model and freeze-transparency of `kotlinx.coroutines`, so unexpected `InvalidMutabilityException`
|
|
||||||
or `FreezingException` might appear.
|
Some libraries might not be ready for the new MM and freeze-transparency of `kotlinx.coroutines`, so unexpected `InvalidMutabilityException` or `FreezingException` might appear.
|
||||||
To workaround such cases, we added a `freezing` binary option that disables freezing fully (`disabled`) or
|
|
||||||
partially (`explicitOnly`).
|
To workaround such cases, we added a `freezing` binary option that disables freezing fully (`disabled`) or partially (`explicitOnly`).
|
||||||
The former disables freezing mechanism at runtime (thus, making it no-op) while the latter disables automatic freezing of
|
The former disables the freezing mechanism at runtime (thus, making it a no-op), while the latter disables automatic freezing of
|
||||||
[`@SharedImmutable`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-shared-immutable/) globals,
|
[`@SharedImmutable`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-shared-immutable/) globals, but keeps direct calls to [`freeze`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/freeze.html) fully functional.
|
||||||
but keeps direct calls to [`freeze`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/freeze.html) fully functional.
|
|
||||||
Similar to the [new memory model](#switch-to-the-new-memory-model), there are several ways to enable this option:
|
To enable this, add the compilation flag `-Xbinary=freezing=disabled`. In Gradle, you can alternatively do one of the following:
|
||||||
* In `gradle.properties`:
|
|
||||||
|
* In `gradle.properties`:
|
||||||
```properties
|
```properties
|
||||||
kotlin.native.binary.freezing=disabled
|
kotlin.native.binary.freezing=disabled
|
||||||
```
|
```
|
||||||
* In Gradle build script:
|
* In a Gradle build script:
|
||||||
```kotlin
|
```kotlin
|
||||||
// build.gradle.kts
|
// build.gradle.kts
|
||||||
|
|
||||||
@@ -190,18 +172,13 @@ kotlin.targets.withType(KotlinNativeTarget::class.java) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
* Compiler flag:
|
|
||||||
```
|
|
||||||
-Xbinary=freezing=disabled
|
|
||||||
```
|
|
||||||
Please note that this option works only with the new MM.
|
|
||||||
|
|
||||||
If you want not just workaround the problem, but actually track down the source of the exceptions then
|
> **NOTE**: this option works only with the new MM.
|
||||||
[`ensureNeverFrozen`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/ensure-never-frozen.html) is your best friend.
|
|
||||||
|
If you want not just workaround the problem, but track down the source of the exceptions, then [`ensureNeverFrozen`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/ensure-never-frozen.html) is your best friend.
|
||||||
|
|
||||||
## Feedback
|
## Feedback
|
||||||
|
|
||||||
In our measurements we observed performance regressions with a slowdown up to a factor of 5.
|
If you encounter performance regressions with a slowdown of more than a factor of 5, report to [this performance meta issue](https://youtrack.jetbrains.com/issue/KT-48526).
|
||||||
If you observe anything much more significant, please report to [the performance meta issue](https://youtrack.jetbrains.com/issue/KT-48526).
|
|
||||||
|
|
||||||
Other problems encountered during migration to the new MM can be reported to [this meta issue](https://youtrack.jetbrains.com/issue/KT-48525).
|
You can report other issues with migration to the new MM to [this meta issue](https://youtrack.jetbrains.com/issue/KT-48525).
|
||||||
|
|||||||
Reference in New Issue
Block a user