Generalize finalization with Cleaner (#4362)
This commit is contained in:
committed by
GitHub
parent
6937ac56e7
commit
ac1a466cc9
+1
@@ -30,4 +30,5 @@ object KonanFqNames {
|
||||
val canBePrecreated = FqName("kotlin.native.internal.CanBePrecreated")
|
||||
val typedIntrinsic = FqName("kotlin.native.internal.TypedIntrinsic")
|
||||
val objCMethod = FqName("kotlinx.cinterop.ObjCMethod")
|
||||
val hasFinalizer = FqName("kotlin.native.internal.HasFinalizer")
|
||||
}
|
||||
|
||||
+6
@@ -239,6 +239,12 @@ internal class KonanSymbols(
|
||||
.single()
|
||||
)
|
||||
|
||||
val createCleaner = symbolTable.referenceSimpleFunction(
|
||||
builtIns.builtInsModule.getPackage(FqName("kotlin.native.internal")).memberScope
|
||||
.getContributedFunctions(Name.identifier("createCleaner"), NoLookupLocation.FROM_BACKEND)
|
||||
.single()
|
||||
)
|
||||
|
||||
val areEqualByValue = context.getKonanInternalFunctions("areEqualByValue").map {
|
||||
symbolTable.referenceSimpleFunction(it)
|
||||
}.associateBy { it.descriptor.valueParameters[0].type.computePrimitiveBinaryTypeOrNull()!! }
|
||||
|
||||
+5
@@ -70,6 +70,10 @@ internal class RTTIGenerator(override val context: Context) : ContextUtils {
|
||||
result = result or TF_SUSPEND_FUNCTION
|
||||
}
|
||||
|
||||
if (irClass.hasAnnotation(KonanFqNames.hasFinalizer)) {
|
||||
result = result or TF_HAS_FINALIZER
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -629,4 +633,5 @@ private const val TF_INTERFACE = 4
|
||||
private const val TF_OBJC_DYNAMIC = 8
|
||||
private const val TF_LEAK_DETECTOR_CANDIDATE = 16
|
||||
private const val TF_SUSPEND_FUNCTION = 32
|
||||
private const val TF_HAS_FINALIZER = 64
|
||||
|
||||
|
||||
+38
@@ -65,6 +65,33 @@ internal class InteropLowering(context: Context) : FileLoweringPass {
|
||||
}
|
||||
}
|
||||
|
||||
private fun IrExpression.isNonCapturingFunction(): Boolean {
|
||||
if (!type.isFunctionTypeOrSubtype())
|
||||
return false
|
||||
|
||||
val fromContainerExpression = fun(expr: IrExpression): IrConstructorCall? {
|
||||
if (expr !is IrContainerExpression)
|
||||
return null
|
||||
if (expr.statements.size != 2)
|
||||
return null
|
||||
|
||||
val firstStatement = expr.statements[0]
|
||||
if (firstStatement !is IrContainerExpression || firstStatement.statements.size != 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
val secondStatement = expr.statements[1]
|
||||
|
||||
return secondStatement as? IrConstructorCall
|
||||
}
|
||||
|
||||
val constructorCall = this as? IrConstructorCall
|
||||
?: fromContainerExpression(this)
|
||||
?: return false
|
||||
|
||||
return constructorCall.valueArgumentsCount == 0
|
||||
}
|
||||
|
||||
private abstract class BaseInteropIrTransformer(private val context: Context) : IrBuildingTransformer(context) {
|
||||
|
||||
protected inline fun <T> generateWithStubs(element: IrElement? = null, block: KotlinStubs.() -> T): T =
|
||||
@@ -1203,6 +1230,17 @@ private class InteropTransformer(val context: Context, override val irFile: IrFi
|
||||
builder.irCall(symbols.interopCPointerGetRawValue).apply {
|
||||
extensionReceiver = expression.dispatchReceiver
|
||||
}
|
||||
// TODO: Move this check out of InteropLowering.
|
||||
symbols.createCleaner.owner -> {
|
||||
val irCallableReference = expression.getValueArgument(1)
|
||||
if (irCallableReference == null || !irCallableReference.isNonCapturingFunction()) {
|
||||
context.reportCompilationError(
|
||||
"${function.fqNameForIrSerialization} must take an unbound, non-capturing function or lambda",
|
||||
irFile, expression
|
||||
)
|
||||
}
|
||||
expression
|
||||
}
|
||||
else -> expression
|
||||
}
|
||||
}
|
||||
|
||||
@@ -896,6 +896,74 @@ task empty_substring(type: KonanLocalTest) {
|
||||
source = "runtime/basic/empty_substring.kt"
|
||||
}
|
||||
|
||||
standaloneTest("cleaner_basic") {
|
||||
enabled = (project.testTarget != 'wasm32') // Cleaners need workers
|
||||
source = "runtime/basic/cleaner_basic.kt"
|
||||
flags = ['-tr', '-Xopt-in=kotlin.native.internal.InternalForKotlinNative']
|
||||
}
|
||||
|
||||
standaloneTest("cleaner_workers") {
|
||||
enabled = (project.testTarget != 'wasm32') // Cleaners need workers
|
||||
source = "runtime/basic/cleaner_workers.kt"
|
||||
flags = ['-tr', '-Xopt-in=kotlin.native.internal.InternalForKotlinNative']
|
||||
}
|
||||
|
||||
standaloneTest("cleaner_in_main_with_checker") {
|
||||
enabled = (project.testTarget != 'wasm32') // Cleaners need workers
|
||||
source = "runtime/basic/cleaner_in_main_with_checker.kt"
|
||||
goldValue = "42\n"
|
||||
}
|
||||
|
||||
standaloneTest("cleaner_in_main_without_checker") {
|
||||
enabled = (project.testTarget != 'wasm32') // Cleaners need workers
|
||||
source = "runtime/basic/cleaner_in_main_without_checker.kt"
|
||||
goldValue = ""
|
||||
}
|
||||
|
||||
standaloneTest("cleaner_leak_release") {
|
||||
enabled = !project.globalTestArgs.contains('-g') && (project.testTarget != 'wasm32') // Cleaners need workers
|
||||
source = "runtime/basic/cleaner_leak.kt"
|
||||
goldValue = ""
|
||||
}
|
||||
|
||||
standaloneTest("cleaner_leak_debug") {
|
||||
enabled = !project.globalTestArgs.contains('-opt') && (project.testTarget != 'wasm32') // Cleaners need workers
|
||||
source = "runtime/basic/cleaner_leak.kt"
|
||||
flags = ['-g']
|
||||
expectedExitStatusChecker = { it != 0 }
|
||||
outputChecker = { s -> (s =~ /Cleaner (0x)?[0-9a-fA-F]+ was disposed during program exit/).find() }
|
||||
}
|
||||
|
||||
standaloneTest("cleaner_leak_without_checker") {
|
||||
enabled = (project.testTarget != 'wasm32') // Cleaners need workers
|
||||
source = "runtime/basic/cleaner_leak_without_checker.kt"
|
||||
goldValue = ""
|
||||
}
|
||||
|
||||
standaloneTest("cleaner_leak_with_checker") {
|
||||
enabled = (project.testTarget != 'wasm32') // Cleaners need workers
|
||||
source = "runtime/basic/cleaner_leak_with_checker.kt"
|
||||
expectedExitStatusChecker = { it != 0 }
|
||||
outputChecker = { s -> (s =~ /Cleaner (0x)?[0-9a-fA-F]+ was disposed during program exit/).find() }
|
||||
}
|
||||
|
||||
standaloneTest("cleaner_in_tls_main_without_checker") {
|
||||
enabled = (project.testTarget != 'wasm32') // Cleaners need workers
|
||||
source = "runtime/basic/cleaner_in_tls_main_without_checker.kt"
|
||||
}
|
||||
|
||||
standaloneTest("cleaner_in_tls_main_with_checker") {
|
||||
enabled = (project.testTarget != 'wasm32') // Cleaners need workers
|
||||
source = "runtime/basic/cleaner_in_tls_main_with_checker.kt"
|
||||
expectedExitStatusChecker = { it != 0 }
|
||||
outputChecker = { s -> (s =~ /Cleaner (0x)?[0-9a-fA-F]+ was disposed during program exit/).find() }
|
||||
}
|
||||
|
||||
standaloneTest("cleaner_in_tls_worker") {
|
||||
enabled = (project.testTarget != 'wasm32') // Cleaners need workers
|
||||
source = "runtime/basic/cleaner_in_tls_worker.kt"
|
||||
}
|
||||
|
||||
standaloneTest("worker_bound_reference0") {
|
||||
enabled = (project.testTarget != 'wasm32') // Workers need pthreads.
|
||||
source = "runtime/concurrent/worker_bound_reference0.kt"
|
||||
@@ -4221,6 +4289,30 @@ dynamicTest("interop_kt42397") {
|
||||
flags = ['-g']
|
||||
}
|
||||
|
||||
dynamicTest("interop_cleaners_main_thread") {
|
||||
disabled = (project.testTarget != null && project.testTarget != project.hostName)
|
||||
source = "interop/cleaners/cleaners.kt"
|
||||
cSource = "$projectDir/interop/cleaners/main_thread.cpp"
|
||||
clangTool = "clang++"
|
||||
goldValue = "42\n"
|
||||
}
|
||||
|
||||
dynamicTest("interop_cleaners_second_thread") {
|
||||
disabled = (project.testTarget != null && project.testTarget != project.hostName)
|
||||
source = "interop/cleaners/cleaners.kt"
|
||||
cSource = "$projectDir/interop/cleaners/second_thread.cpp"
|
||||
clangTool = "clang++"
|
||||
goldValue = "42\n"
|
||||
}
|
||||
|
||||
dynamicTest("interop_cleaners_leak") {
|
||||
disabled = (project.testTarget != null && project.testTarget != project.hostName)
|
||||
source = "interop/cleaners/cleaners.kt"
|
||||
cSource = "$projectDir/interop/cleaners/leak.cpp"
|
||||
clangTool = "clang++"
|
||||
goldValue = ""
|
||||
}
|
||||
|
||||
task library_mismatch(type: KonanDriverTest) {
|
||||
// Does not work for cross targets yet.
|
||||
enabled = !(project.testTarget != null && project.target.name != project.hostName)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
@file:OptIn(ExperimentalStdlibApi::class)
|
||||
|
||||
import kotlin.native.internal.*
|
||||
|
||||
fun createCleaner() {
|
||||
createCleaner(42) {
|
||||
println(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun performGC() {
|
||||
GC.collect()
|
||||
performGCOnCleanerWorker()
|
||||
}
|
||||
|
||||
private var globalCleaner: Cleaner? = null
|
||||
|
||||
fun initializeGlobalCleaner() {
|
||||
globalCleaner = createCleaner(11) {
|
||||
println(it)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "testlib_api.h"
|
||||
|
||||
int main() {
|
||||
testlib_symbols()->kotlin.root.initializeGlobalCleaner();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "testlib_api.h"
|
||||
|
||||
int main() {
|
||||
testlib_symbols()->kotlin.root.createCleaner();
|
||||
testlib_symbols()->kotlin.root.performGC();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "testlib_api.h"
|
||||
|
||||
#include <thread>
|
||||
|
||||
int main() {
|
||||
std::thread t([]() { testlib_symbols()->kotlin.root.createCleaner(); });
|
||||
t.join();
|
||||
testlib_symbols()->kotlin.root.performGC();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
@file:OptIn(ExperimentalTime::class, ExperimentalStdlibApi::class)
|
||||
|
||||
package runtime.basic.cleaner_basic
|
||||
|
||||
import kotlin.test.*
|
||||
|
||||
import kotlin.native.internal.*
|
||||
import kotlin.native.concurrent.*
|
||||
import kotlin.native.ref.WeakReference
|
||||
import kotlin.time.*
|
||||
|
||||
class AtomicBoolean(initialValue: Boolean) {
|
||||
private val impl = AtomicInt(if (initialValue) 1 else 0)
|
||||
|
||||
init {
|
||||
freeze()
|
||||
}
|
||||
|
||||
public var value: Boolean
|
||||
get() = impl.value != 0
|
||||
set(new) { impl.value = if (new) 1 else 0 }
|
||||
}
|
||||
|
||||
class FunBox(private val impl: () -> Unit) {
|
||||
fun call() {
|
||||
impl()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCleanerLambda() {
|
||||
val called = AtomicBoolean(false);
|
||||
var funBoxWeak: WeakReference<FunBox>? = null
|
||||
var cleanerWeak: WeakReference<Cleaner>? = null
|
||||
{
|
||||
val cleaner = {
|
||||
val funBox = FunBox { called.value = true }.freeze()
|
||||
funBoxWeak = WeakReference(funBox)
|
||||
createCleaner(funBox) { it.call() }
|
||||
}()
|
||||
GC.collect() // Make sure local funBox reference is gone
|
||||
cleanerWeak = WeakReference(cleaner)
|
||||
assertFalse(called.value)
|
||||
}()
|
||||
|
||||
GC.collect()
|
||||
performGCOnCleanerWorker()
|
||||
|
||||
assertNull(cleanerWeak!!.value)
|
||||
assertTrue(called.value)
|
||||
assertNull(funBoxWeak!!.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCleanerAnonymousFunction() {
|
||||
val called = AtomicBoolean(false);
|
||||
var funBoxWeak: WeakReference<FunBox>? = null
|
||||
var cleanerWeak: WeakReference<Cleaner>? = null
|
||||
{
|
||||
val cleaner = {
|
||||
val funBox = FunBox { called.value = true }.freeze()
|
||||
funBoxWeak = WeakReference(funBox)
|
||||
createCleaner(funBox, fun (it: FunBox) { it.call() })
|
||||
}()
|
||||
GC.collect() // Make sure local funBox reference is gone
|
||||
cleanerWeak = WeakReference(cleaner)
|
||||
assertFalse(called.value)
|
||||
}()
|
||||
|
||||
GC.collect()
|
||||
performGCOnCleanerWorker()
|
||||
|
||||
assertNull(cleanerWeak!!.value)
|
||||
assertTrue(called.value)
|
||||
assertNull(funBoxWeak!!.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCleanerFunctionReference() {
|
||||
val called = AtomicBoolean(false);
|
||||
var funBoxWeak: WeakReference<FunBox>? = null
|
||||
var cleanerWeak: WeakReference<Cleaner>? = null
|
||||
{
|
||||
val cleaner = {
|
||||
val funBox = FunBox { called.value = true }.freeze()
|
||||
funBoxWeak = WeakReference(funBox)
|
||||
createCleaner(funBox, FunBox::call)
|
||||
}()
|
||||
GC.collect() // Make sure local funBox reference is gone
|
||||
cleanerWeak = WeakReference(cleaner)
|
||||
assertFalse(called.value)
|
||||
}()
|
||||
|
||||
GC.collect()
|
||||
performGCOnCleanerWorker()
|
||||
|
||||
assertNull(cleanerWeak!!.value)
|
||||
assertTrue(called.value)
|
||||
assertNull(funBoxWeak!!.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCleanerFailWithNonShareableArgument() {
|
||||
val funBox = FunBox {}
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
createCleaner(funBox) {}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun tryWithTimeout(timeoutSeconds: Int, f: () -> Unit): Unit {
|
||||
val timeout = TimeSource.Monotonic.markNow() + timeoutSeconds.seconds
|
||||
while (true) {
|
||||
try {
|
||||
f()
|
||||
return
|
||||
} catch (e: Throwable) {
|
||||
if (timeout.hasPassedNow()) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCleanerCleansWithoutGC() {
|
||||
val called = AtomicBoolean(false);
|
||||
var funBoxWeak: WeakReference<FunBox>? = null
|
||||
var cleanerWeak: WeakReference<Cleaner>? = null
|
||||
{
|
||||
val cleaner = {
|
||||
val funBox = FunBox { called.value = true }.freeze()
|
||||
funBoxWeak = WeakReference(funBox)
|
||||
createCleaner(funBox) { it.call() }
|
||||
}()
|
||||
GC.collect() // Make sure local funBox reference is gone
|
||||
cleaner.freeze()
|
||||
cleanerWeak = WeakReference(cleaner)
|
||||
assertFalse(called.value)
|
||||
}()
|
||||
|
||||
GC.collect()
|
||||
|
||||
assertNull(cleanerWeak!!.value)
|
||||
tryWithTimeout(3) {
|
||||
GC.collect() // Collect local stack (from previous iteration)
|
||||
assertTrue(called.value)
|
||||
// If this fails, GC has somehow ran on the cleaners worker.
|
||||
assertNotNull(funBoxWeak!!.value)
|
||||
}
|
||||
}
|
||||
|
||||
val globalInt = AtomicInt(0)
|
||||
|
||||
@Test
|
||||
fun testCleanerWithInt() {
|
||||
var cleanerWeak: WeakReference<Cleaner>? = null
|
||||
{
|
||||
val cleaner = createCleaner(42) {
|
||||
globalInt.value = it
|
||||
}.freeze()
|
||||
cleanerWeak = WeakReference(cleaner)
|
||||
assertEquals(0, globalInt.value)
|
||||
}()
|
||||
|
||||
GC.collect()
|
||||
performGCOnCleanerWorker()
|
||||
|
||||
assertNull(cleanerWeak!!.value)
|
||||
assertEquals(42, globalInt.value)
|
||||
}
|
||||
|
||||
val globalPtr = AtomicNativePtr(NativePtr.NULL)
|
||||
|
||||
@Test
|
||||
fun testCleanerWithNativePtr() {
|
||||
var cleanerWeak: WeakReference<Cleaner>? = null
|
||||
{
|
||||
val cleaner = createCleaner(NativePtr.NULL + 42L) {
|
||||
globalPtr.value = it
|
||||
}
|
||||
cleanerWeak = WeakReference(cleaner)
|
||||
assertEquals(NativePtr.NULL, globalPtr.value)
|
||||
}()
|
||||
|
||||
GC.collect()
|
||||
performGCOnCleanerWorker()
|
||||
|
||||
assertNull(cleanerWeak!!.value)
|
||||
assertEquals(NativePtr.NULL + 42L, globalPtr.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCleanerWithException() {
|
||||
val called = AtomicBoolean(false);
|
||||
var funBoxWeak: WeakReference<FunBox>? = null
|
||||
var cleanerWeak: WeakReference<Cleaner>? = null
|
||||
{
|
||||
val funBox = FunBox { called.value = true }.freeze()
|
||||
funBoxWeak = WeakReference(funBox)
|
||||
val cleaner = createCleaner(funBox) {
|
||||
it.call()
|
||||
error("Cleaner block failed")
|
||||
}
|
||||
cleanerWeak = WeakReference(cleaner)
|
||||
}()
|
||||
|
||||
GC.collect()
|
||||
performGCOnCleanerWorker()
|
||||
|
||||
assertNull(cleanerWeak!!.value)
|
||||
// Cleaners block started executing.
|
||||
assertTrue(called.value)
|
||||
// Even though the block failed, the captured funBox is freed.
|
||||
assertNull(funBoxWeak!!.value)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
@file:OptIn(ExperimentalStdlibApi::class)
|
||||
|
||||
import kotlin.native.internal.*
|
||||
import kotlin.native.Platform
|
||||
|
||||
fun main() {
|
||||
// Cleaner holds onto a finalization lambda. If it doesn't get executed,
|
||||
// the memory will leak. Suppress memory leak checker to check for cleaners
|
||||
// leak only.
|
||||
Platform.isMemoryLeakCheckerActive = false
|
||||
Platform.isCleanersLeakCheckerActive = true
|
||||
// This cleaner will run, because with the checker active this cleaner
|
||||
// will get collected, block scheduled and executed before cleaners are disabled.
|
||||
createCleaner(42) {
|
||||
println(it)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
@file:OptIn(ExperimentalStdlibApi::class)
|
||||
|
||||
import kotlin.native.internal.*
|
||||
import kotlin.native.Platform
|
||||
|
||||
fun main() {
|
||||
// Cleaner holds onto a finalization lambda. If it doesn't get executed,
|
||||
// the memory will leak. Suppress memory leak checker to check for cleaners
|
||||
// leak only.
|
||||
Platform.isMemoryLeakCheckerActive = false
|
||||
Platform.isCleanersLeakCheckerActive = false
|
||||
// This cleaner will not run, because with the checker inactive this cleaner
|
||||
// will not get collected before cleaners are disabled.
|
||||
createCleaner(42) {
|
||||
println(it)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
@file:OptIn(ExperimentalStdlibApi::class)
|
||||
|
||||
import kotlin.test.*
|
||||
|
||||
import kotlin.native.concurrent.*
|
||||
import kotlin.native.internal.*
|
||||
import kotlin.native.Platform
|
||||
|
||||
@ThreadLocal
|
||||
var tlsCleaner: Cleaner? = null
|
||||
|
||||
fun main() {
|
||||
// Cleaner holds onto a finalization lambda. If it doesn't get executed,
|
||||
// the memory will leak. Suppress memory leak checker to check for cleaners
|
||||
// leak only.
|
||||
Platform.isMemoryLeakCheckerActive = false
|
||||
Platform.isCleanersLeakCheckerActive = true
|
||||
// This cleaner won't be run
|
||||
tlsCleaner = createCleaner(42) {
|
||||
println(it)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
@file:OptIn(ExperimentalStdlibApi::class)
|
||||
|
||||
import kotlin.test.*
|
||||
|
||||
import kotlin.native.concurrent.*
|
||||
import kotlin.native.internal.*
|
||||
import kotlin.native.Platform
|
||||
|
||||
@ThreadLocal
|
||||
var tlsCleaner: Cleaner? = null
|
||||
|
||||
fun main() {
|
||||
// Cleaner holds onto a finalization lambda. If it doesn't get executed,
|
||||
// the memory will leak. Suppress memory leak checker to check for cleaners
|
||||
// leak only.
|
||||
Platform.isMemoryLeakCheckerActive = false
|
||||
Platform.isCleanersLeakCheckerActive = false
|
||||
// This cleaner won't be run
|
||||
tlsCleaner = createCleaner(42) {
|
||||
println(it)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
@file:OptIn(ExperimentalTime::class, ExperimentalStdlibApi::class)
|
||||
|
||||
import kotlin.test.*
|
||||
|
||||
import kotlin.native.concurrent.*
|
||||
import kotlin.native.internal.*
|
||||
import kotlin.time.*
|
||||
|
||||
@ThreadLocal
|
||||
var tlsCleaner: Cleaner? = null
|
||||
|
||||
val value = AtomicInt(0)
|
||||
|
||||
fun main() {
|
||||
val worker = Worker.start()
|
||||
|
||||
worker.execute(TransferMode.SAFE, {}) {
|
||||
tlsCleaner = createCleaner(42) {
|
||||
value.value = it
|
||||
}
|
||||
}
|
||||
|
||||
worker.requestTermination().result
|
||||
|
||||
val timeout = TimeSource.Monotonic.markNow() + 3.seconds
|
||||
while (value.value == 0 || timeout.hasNotPassedNow()) {}
|
||||
|
||||
assertEquals(42, value.value)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
@file:OptIn(ExperimentalStdlibApi::class)
|
||||
|
||||
import kotlin.test.*
|
||||
|
||||
import kotlin.native.internal.*
|
||||
|
||||
// This cleaner won't be run, because it's deinitialized with globals after
|
||||
// cleaners are disabled.
|
||||
val globalCleaner = createCleaner(42) {
|
||||
println(it)
|
||||
}
|
||||
|
||||
fun main() {
|
||||
// Cleaner holds onto a finalization lambda. If it doesn't get executed,
|
||||
// the memory will leak. Suppress memory leak checker to check for cleaners
|
||||
// leak only.
|
||||
Platform.isMemoryLeakCheckerActive = false
|
||||
// Make sure cleaner is initialized.
|
||||
assertNotNull(globalCleaner)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
@file:OptIn(ExperimentalStdlibApi::class)
|
||||
|
||||
import kotlin.test.*
|
||||
|
||||
import kotlin.native.internal.*
|
||||
import kotlin.native.Platform
|
||||
|
||||
// This cleaner won't be run, because it's deinitialized with globals after
|
||||
// cleaners are disabled.
|
||||
val globalCleaner = createCleaner(42) {
|
||||
println(it)
|
||||
}
|
||||
|
||||
fun main() {
|
||||
// Cleaner holds onto a finalization lambda. If it doesn't get executed,
|
||||
// the memory will leak. Suppress memory leak checker to check for cleaners
|
||||
// leak only.
|
||||
Platform.isMemoryLeakCheckerActive = false
|
||||
Platform.isCleanersLeakCheckerActive = true
|
||||
// Make sure cleaner is initialized.
|
||||
assertNotNull(globalCleaner)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
@file:OptIn(ExperimentalStdlibApi::class)
|
||||
|
||||
import kotlin.test.*
|
||||
|
||||
import kotlin.native.internal.*
|
||||
import kotlin.native.Platform
|
||||
|
||||
// This cleaner won't be run, because it's deinitialized with globals after
|
||||
// cleaners are disabled.
|
||||
val globalCleaner = createCleaner(42) {
|
||||
println(it)
|
||||
}
|
||||
|
||||
fun main() {
|
||||
// Cleaner holds onto a finalization lambda. If it doesn't get executed,
|
||||
// the memory will leak. Suppress memory leak checker to check for cleaners
|
||||
// leak only.
|
||||
Platform.isMemoryLeakCheckerActive = false
|
||||
Platform.isCleanersLeakCheckerActive = false
|
||||
// Make sure cleaner is initialized.
|
||||
assertNotNull(globalCleaner)
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
@file:OptIn(ExperimentalTime::class, ExperimentalStdlibApi::class)
|
||||
|
||||
package runtime.basic.cleaner_workers
|
||||
|
||||
import kotlin.test.*
|
||||
|
||||
import kotlin.native.internal.*
|
||||
import kotlin.native.concurrent.*
|
||||
import kotlin.native.ref.WeakReference
|
||||
import kotlin.time.*
|
||||
|
||||
class AtomicBoolean(initialValue: Boolean) {
|
||||
private val impl = AtomicInt(if (initialValue) 1 else 0)
|
||||
|
||||
init {
|
||||
freeze()
|
||||
}
|
||||
|
||||
public var value: Boolean
|
||||
get() = impl.value != 0
|
||||
set(new) { impl.value = if (new) 1 else 0 }
|
||||
}
|
||||
|
||||
class FunBox(private val impl: () -> Unit) {
|
||||
fun call() {
|
||||
impl()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCleanerDestroyInChild() {
|
||||
val worker = Worker.start()
|
||||
|
||||
val called = AtomicBoolean(false);
|
||||
var funBoxWeak: WeakReference<FunBox>? = null
|
||||
var cleanerWeak: WeakReference<Cleaner>? = null
|
||||
worker.execute(TransferMode.SAFE, {
|
||||
val funBox = FunBox { called.value = true }.freeze()
|
||||
funBoxWeak = WeakReference(funBox)
|
||||
val cleaner = createCleaner(funBox) { it.call() }
|
||||
cleanerWeak = WeakReference(cleaner)
|
||||
Pair(called, cleaner)
|
||||
}) { (called, cleaner) ->
|
||||
assertFalse(called.value)
|
||||
}.result
|
||||
|
||||
GC.collect()
|
||||
worker.execute(TransferMode.SAFE, {}) { GC.collect() }.result
|
||||
performGCOnCleanerWorker()
|
||||
|
||||
assertNull(cleanerWeak!!.value)
|
||||
assertTrue(called.value)
|
||||
assertNull(funBoxWeak!!.value)
|
||||
|
||||
worker.requestTermination().result
|
||||
}
|
||||
|
||||
inline fun tryWithTimeout(timeoutSeconds: Int, f: () -> Unit): Unit {
|
||||
val timeout = TimeSource.Monotonic.markNow() + timeoutSeconds.seconds
|
||||
while (true) {
|
||||
try {
|
||||
f()
|
||||
return
|
||||
} catch (e: Throwable) {
|
||||
if (timeout.hasPassedNow()) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCleanerDestroyWithChild() {
|
||||
val worker = Worker.start()
|
||||
|
||||
val called = AtomicBoolean(false);
|
||||
var funBoxWeak: WeakReference<FunBox>? = null
|
||||
var cleanerWeak: WeakReference<Cleaner>? = null
|
||||
worker.execute(TransferMode.SAFE, {
|
||||
val funBox = FunBox { called.value = true }.freeze()
|
||||
funBoxWeak = WeakReference(funBox)
|
||||
val cleaner = createCleaner(funBox) { it.call() }
|
||||
cleanerWeak = WeakReference(cleaner)
|
||||
Pair(called, cleaner)
|
||||
}) { (called, cleaner) ->
|
||||
assertFalse(called.value)
|
||||
}.result
|
||||
|
||||
GC.collect()
|
||||
worker.requestTermination().result
|
||||
|
||||
tryWithTimeout(3) {
|
||||
GC.collect() // Collect local stack (from previous iteration)
|
||||
performGCOnCleanerWorker() // Collect cleaners stack
|
||||
|
||||
assertNull(cleanerWeak!!.value)
|
||||
assertTrue(called.value)
|
||||
assertNull(funBoxWeak!!.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCleanerDestroyFrozenWithChild() {
|
||||
val worker = Worker.start()
|
||||
|
||||
val called = AtomicBoolean(false);
|
||||
var funBoxWeak: WeakReference<FunBox>? = null
|
||||
var cleanerWeak: WeakReference<Cleaner>? = null
|
||||
worker.execute(TransferMode.SAFE, {
|
||||
val funBox = FunBox { called.value = true }.freeze()
|
||||
funBoxWeak = WeakReference(funBox)
|
||||
val cleaner = createCleaner(funBox) { it.call() }.freeze()
|
||||
cleanerWeak = WeakReference(cleaner)
|
||||
Pair(called, cleaner)
|
||||
}) { (called, cleaner) ->
|
||||
assertFalse(called.value)
|
||||
}.result
|
||||
|
||||
GC.collect()
|
||||
worker.requestTermination().result
|
||||
|
||||
tryWithTimeout(3) {
|
||||
GC.collect() // Collect local stack (from previous iteration)
|
||||
performGCOnCleanerWorker() // Collect cleaners stack
|
||||
|
||||
assertNull(cleanerWeak!!.value)
|
||||
assertTrue(called.value)
|
||||
assertNull(funBoxWeak!!.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCleanerDestroyFrozenInChild() {
|
||||
val worker = Worker.start()
|
||||
|
||||
val called = AtomicBoolean(false);
|
||||
var funBoxWeak: WeakReference<FunBox>? = null
|
||||
var cleanerWeak: WeakReference<Cleaner>? = null
|
||||
{
|
||||
worker.execute(TransferMode.SAFE, {
|
||||
val funBox = FunBox { called.value = true }.freeze()
|
||||
funBoxWeak = WeakReference(funBox)
|
||||
val cleaner = createCleaner(funBox) { it.call() }.freeze()
|
||||
cleanerWeak = WeakReference(cleaner)
|
||||
Pair(called, cleaner)
|
||||
}) { (called, cleaner) ->
|
||||
assertFalse(called.value)
|
||||
}.result
|
||||
}()
|
||||
|
||||
GC.collect()
|
||||
worker.execute(TransferMode.SAFE, {}) { GC.collect() }.result
|
||||
performGCOnCleanerWorker()
|
||||
|
||||
assertNull(cleanerWeak!!.value)
|
||||
assertTrue(called.value)
|
||||
assertNull(funBoxWeak!!.value)
|
||||
|
||||
worker.requestTermination().result
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCleanerDestroyInMain() {
|
||||
val worker = Worker.start()
|
||||
|
||||
val called = AtomicBoolean(false);
|
||||
var funBoxWeak: WeakReference<FunBox>? = null
|
||||
var cleanerWeak: WeakReference<Cleaner>? = null
|
||||
{
|
||||
val result = worker.execute(TransferMode.SAFE, { called }) { called ->
|
||||
val funBox = FunBox { called.value = true }.freeze()
|
||||
val cleaner = createCleaner(funBox) { it.call() }
|
||||
Triple(cleaner, WeakReference(funBox), WeakReference(cleaner))
|
||||
}.result
|
||||
val cleaner = result.first
|
||||
funBoxWeak = result.second
|
||||
cleanerWeak = result.third
|
||||
assertFalse(called.value)
|
||||
}()
|
||||
|
||||
GC.collect()
|
||||
worker.execute(TransferMode.SAFE, {}) { GC.collect() }.result
|
||||
performGCOnCleanerWorker()
|
||||
|
||||
assertNull(cleanerWeak!!.value)
|
||||
assertTrue(called.value)
|
||||
assertNull(funBoxWeak!!.value)
|
||||
|
||||
worker.requestTermination().result
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCleanerDestroyFrozenInMain() {
|
||||
val worker = Worker.start()
|
||||
|
||||
val called = AtomicBoolean(false);
|
||||
var funBoxWeak: WeakReference<FunBox>? = null
|
||||
var cleanerWeak: WeakReference<Cleaner>? = null
|
||||
{
|
||||
val result = worker.execute(TransferMode.SAFE, { called }) { called ->
|
||||
val funBox = FunBox { called.value = true }.freeze()
|
||||
val cleaner = createCleaner(funBox) { it.call() }.freeze()
|
||||
Triple(cleaner, WeakReference(funBox), WeakReference(cleaner))
|
||||
}.result
|
||||
val cleaner = result.first
|
||||
funBoxWeak = result.second
|
||||
cleanerWeak = result.third
|
||||
assertFalse(called.value)
|
||||
}()
|
||||
|
||||
GC.collect()
|
||||
worker.execute(TransferMode.SAFE, {}) { GC.collect() }.result
|
||||
performGCOnCleanerWorker()
|
||||
|
||||
assertNull(cleanerWeak!!.value)
|
||||
assertTrue(called.value)
|
||||
assertNull(funBoxWeak!!.value)
|
||||
|
||||
worker.requestTermination().result
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCleanerDestroyShared() {
|
||||
val worker = Worker.start()
|
||||
|
||||
val called = AtomicBoolean(false);
|
||||
var funBoxWeak: WeakReference<FunBox>? = null
|
||||
var cleanerWeak: WeakReference<Cleaner>? = null
|
||||
val cleanerHolder: AtomicReference<Cleaner?> = AtomicReference(null);
|
||||
{
|
||||
val funBox = FunBox { called.value = true }.freeze()
|
||||
funBoxWeak = WeakReference(funBox)
|
||||
val cleaner = createCleaner(funBox) { it.call() }.freeze()
|
||||
cleanerWeak = WeakReference(cleaner)
|
||||
cleanerHolder.value = cleaner
|
||||
worker.execute(TransferMode.SAFE, { Pair(called, cleanerHolder) }) { (called, cleanerHolder) ->
|
||||
cleanerHolder.value = null
|
||||
assertFalse(called.value)
|
||||
}.result
|
||||
}()
|
||||
|
||||
GC.collect()
|
||||
worker.execute(TransferMode.SAFE, {}) { GC.collect() }.result
|
||||
performGCOnCleanerWorker()
|
||||
|
||||
assertNull(cleanerWeak!!.value)
|
||||
assertTrue(called.value)
|
||||
assertNull(funBoxWeak!!.value)
|
||||
|
||||
worker.requestTermination().result
|
||||
}
|
||||
|
||||
@ThreadLocal
|
||||
var tlsValue = 11
|
||||
|
||||
@Test
|
||||
fun testCleanerWithTLS() {
|
||||
val worker = Worker.start()
|
||||
|
||||
tlsValue = 12
|
||||
|
||||
val value = AtomicInt(0)
|
||||
worker.execute(TransferMode.SAFE, {value}) {
|
||||
tlsValue = 13
|
||||
createCleaner(it) {
|
||||
it.value = tlsValue
|
||||
}
|
||||
Unit
|
||||
}.result
|
||||
|
||||
worker.execute(TransferMode.SAFE, {}) { GC.collect() }.result
|
||||
performGCOnCleanerWorker()
|
||||
|
||||
assertEquals(11, value.value)
|
||||
|
||||
worker.requestTermination().result
|
||||
}
|
||||
@@ -154,7 +154,8 @@ open class LinkNativeTest @Inject constructor(
|
||||
fun createTestTask(
|
||||
project: Project,
|
||||
testTaskName: String,
|
||||
testedTaskNames: List<String>
|
||||
testedTaskNames: List<String>,
|
||||
configureCompileToBitcode: CompileToBitcode.() -> Unit = {},
|
||||
): Task {
|
||||
val platformManager = project.rootProject.findProperty("platformManager") as PlatformManager
|
||||
val googleTestExtension = project.extensions.getByName(RuntimeTestingPlugin.GOOGLE_TEST_EXTENSION_NAME) as GoogleTestExtension
|
||||
@@ -179,6 +180,7 @@ fun createTestTask(
|
||||
dependsOn(it)
|
||||
compilerArgs.addAll(it.compilerArgs)
|
||||
headersDirs += googleTestExtension.headersDirs
|
||||
this.configureCompileToBitcode()
|
||||
}
|
||||
if (task.inputFiles.count() == 0)
|
||||
null
|
||||
|
||||
@@ -104,7 +104,9 @@ targetList.forEach { targetName ->
|
||||
"${targetName}Release",
|
||||
"${targetName}StdAlloc"
|
||||
)
|
||||
)
|
||||
) {
|
||||
includeRuntime()
|
||||
}
|
||||
|
||||
createTestTask(
|
||||
project,
|
||||
@@ -116,7 +118,9 @@ targetList.forEach { targetName ->
|
||||
"${targetName}Mimalloc",
|
||||
"${targetName}OptAlloc"
|
||||
)
|
||||
)
|
||||
) {
|
||||
includeRuntime()
|
||||
}
|
||||
|
||||
tasks.register("${targetName}RuntimeTests") {
|
||||
dependsOn("${targetName}StdAllocRuntimeTests")
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "Cleaner.h"
|
||||
#include "Memory.h"
|
||||
#include "Natives.h"
|
||||
#include "Runtime.h"
|
||||
@@ -57,9 +58,17 @@ extern "C" RUNTIME_USED int Init_and_run_start(int argc, const char** argv, int
|
||||
KInt exitStatus = Konan_run_start(argc, argv);
|
||||
|
||||
if (memoryDeInit) {
|
||||
if (Kotlin_memoryLeakCheckerEnabled())
|
||||
WaitNativeWorkersTermination();
|
||||
Kotlin_deinitRuntimeIfNeeded();
|
||||
if (Kotlin_cleanersLeakCheckerEnabled()) {
|
||||
// Make sure to collect any lingering cleaners.
|
||||
PerformFullGC();
|
||||
// Execute all the cleaner blocks and stop the Cleaner worker.
|
||||
ShutdownCleaners(true);
|
||||
} else {
|
||||
// Stop the cleaner worker without executing remaining cleaner blocks.
|
||||
ShutdownCleaners(false);
|
||||
}
|
||||
if (Kotlin_memoryLeakCheckerEnabled()) WaitNativeWorkersTermination();
|
||||
Kotlin_deinitRuntimeIfNeeded();
|
||||
}
|
||||
|
||||
return exitStatus;
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "Cleaner.h"
|
||||
|
||||
#include "Memory.h"
|
||||
#include "Runtime.h"
|
||||
#include "Worker.h"
|
||||
|
||||
// Defined in Cleaner.kt
|
||||
extern "C" void Kotlin_CleanerImpl_shutdownCleanerWorker(KInt, bool);
|
||||
extern "C" KInt Kotlin_CleanerImpl_createCleanerWorker();
|
||||
|
||||
namespace {
|
||||
|
||||
struct CleanerImpl {
|
||||
ObjHeader header;
|
||||
KNativePtr cleanerStablePtr;
|
||||
};
|
||||
|
||||
constexpr KInt kCleanerWorkerUninitialized = 0;
|
||||
constexpr KInt kCleanerWorkerInitializing = -1;
|
||||
constexpr KInt kCleanerWorkerShutdown = -2;
|
||||
|
||||
KInt globalCleanerWorker = kCleanerWorkerUninitialized;
|
||||
|
||||
void disposeCleaner(CleanerImpl* thiz) {
|
||||
auto worker = atomicGet(&globalCleanerWorker);
|
||||
RuntimeAssert(
|
||||
worker != kCleanerWorkerUninitialized && worker != kCleanerWorkerInitializing,
|
||||
"Cleaner worker must've been initialized by now");
|
||||
if (worker == kCleanerWorkerShutdown) {
|
||||
if (Kotlin_cleanersLeakCheckerEnabled()) {
|
||||
konan::consoleErrorf(
|
||||
"Cleaner %p was disposed during program exit\n"
|
||||
"Use `Platform.isCleanersLeakCheckerActive = false` to avoid this check.\n",
|
||||
thiz);
|
||||
RuntimeCheck(false, "Terminating now");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimeAssert(worker > 0, "Cleaner worker must be fully initialized here");
|
||||
|
||||
bool result = WorkerSchedule(worker, thiz->cleanerStablePtr);
|
||||
RuntimeAssert(result, "Couldn't find Cleaner worker");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
RUNTIME_NOTHROW void DisposeCleaner(KRef thiz) {
|
||||
#if KONAN_NO_EXCEPTIONS
|
||||
disposeCleaner(reinterpret_cast<CleanerImpl*>(thiz));
|
||||
#else
|
||||
try {
|
||||
disposeCleaner(reinterpret_cast<CleanerImpl*>(thiz));
|
||||
} catch (...) {
|
||||
// A trick to terminate with unhandled exception. This will print a stack trace
|
||||
// and write to iOS crash log.
|
||||
std::terminate();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ShutdownCleaners(bool executeScheduledCleaners) {
|
||||
KInt worker = 0;
|
||||
do {
|
||||
worker = atomicGet(&globalCleanerWorker);
|
||||
RuntimeAssert(worker != kCleanerWorkerShutdown, "Cleaner worker must not be shutdown twice");
|
||||
if (worker == kCleanerWorkerUninitialized) {
|
||||
if (!compareAndSet(&globalCleanerWorker, kCleanerWorkerUninitialized, kCleanerWorkerShutdown)) {
|
||||
// Someone is trying to initialize the worker. Try again.
|
||||
continue;
|
||||
}
|
||||
// worker was never initialized. Just return.
|
||||
return;
|
||||
}
|
||||
if (worker == kCleanerWorkerInitializing) {
|
||||
// Someone is trying to initialize the worker. Try again.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Worker is in some proper state.
|
||||
break;
|
||||
|
||||
} while (true);
|
||||
|
||||
RuntimeAssert(worker > 0, "Cleaner worker must be fully initialized here");
|
||||
|
||||
atomicSet(&globalCleanerWorker, kCleanerWorkerShutdown);
|
||||
Kotlin_CleanerImpl_shutdownCleanerWorker(worker, executeScheduledCleaners);
|
||||
WaitNativeWorkerTermination(worker);
|
||||
}
|
||||
|
||||
extern "C" KInt Kotlin_CleanerImpl_getCleanerWorker() {
|
||||
KInt worker = 0;
|
||||
do {
|
||||
worker = atomicGet(&globalCleanerWorker);
|
||||
RuntimeAssert(worker != kCleanerWorkerShutdown, "Cleaner worker must not have been shutdown");
|
||||
if (worker == kCleanerWorkerUninitialized) {
|
||||
if (!compareAndSet(&globalCleanerWorker, kCleanerWorkerUninitialized, kCleanerWorkerInitializing)) {
|
||||
// Someone else is trying to initialize the worker. Try again.
|
||||
continue;
|
||||
}
|
||||
worker = Kotlin_CleanerImpl_createCleanerWorker();
|
||||
if (!compareAndSet(&globalCleanerWorker, kCleanerWorkerInitializing, worker)) {
|
||||
RuntimeCheck(false, "Someone interrupted worker initializing");
|
||||
}
|
||||
// Worker is initialized.
|
||||
break;
|
||||
}
|
||||
if (worker == kCleanerWorkerInitializing) {
|
||||
// Someone is trying to initialize the worker. Try again.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Worker is in some proper state.
|
||||
break;
|
||||
} while (true);
|
||||
|
||||
RuntimeAssert(worker > 0, "Cleaner worker must be fully initialized here");
|
||||
|
||||
return worker;
|
||||
}
|
||||
|
||||
void ResetCleanerWorkerForTests() {
|
||||
atomicSet(&globalCleanerWorker, kCleanerWorkerUninitialized);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#ifndef RUNTIME_CLEANER_H
|
||||
#define RUNTIME_CLEANER_H
|
||||
|
||||
#include "Common.h"
|
||||
#include "Types.h"
|
||||
|
||||
RUNTIME_NOTHROW void DisposeCleaner(KRef thiz);
|
||||
|
||||
void ShutdownCleaners(bool executeScheduledCleaners);
|
||||
|
||||
extern "C" KInt Kotlin_CleanerImpl_getCleanerWorker();
|
||||
|
||||
void ResetCleanerWorkerForTests();
|
||||
|
||||
#endif // RUNTIME_CLEANER_H
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "Cleaner.h"
|
||||
|
||||
#include <future>
|
||||
#include <vector>
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "Atomic.h"
|
||||
#include "TestSupportCompilerGenerated.hpp"
|
||||
|
||||
using testing::_;
|
||||
|
||||
// TODO: Also test disposal. (This requires extracting Worker interface)
|
||||
|
||||
TEST(CleanerTest, ConcurrentCreation) {
|
||||
ResetCleanerWorkerForTests();
|
||||
|
||||
constexpr int threadCount = 100;
|
||||
constexpr KInt workerId = 42;
|
||||
|
||||
auto createCleanerWorkerMock = ScopedCreateCleanerWorkerMock();
|
||||
|
||||
EXPECT_CALL(*createCleanerWorkerMock, Call()).Times(1).WillOnce(testing::Return(workerId));
|
||||
|
||||
int startedThreads = 0;
|
||||
bool allowRunning = false;
|
||||
std::vector<std::future<KInt>> futures;
|
||||
for (int i = 0; i < threadCount; ++i) {
|
||||
auto future = std::async(std::launch::async, [&startedThreads, &allowRunning]() {
|
||||
atomicAdd(&startedThreads, 1);
|
||||
while (!atomicGet(&allowRunning)) {
|
||||
}
|
||||
return Kotlin_CleanerImpl_getCleanerWorker();
|
||||
});
|
||||
futures.push_back(std::move(future));
|
||||
}
|
||||
while (atomicGet(&startedThreads) != threadCount) {
|
||||
}
|
||||
atomicSet(&allowRunning, true);
|
||||
std::vector<KInt> values;
|
||||
for (auto& future : futures) {
|
||||
values.push_back(future.get());
|
||||
}
|
||||
|
||||
ASSERT_THAT(values.size(), threadCount);
|
||||
EXPECT_THAT(values, testing::Each(workerId));
|
||||
}
|
||||
|
||||
TEST(CleanerTest, ShutdownWithoutCreation) {
|
||||
ResetCleanerWorkerForTests();
|
||||
|
||||
auto createCleanerWorkerMock = ScopedCreateCleanerWorkerMock();
|
||||
auto shutdownCleanerWorkerMock = ScopedShutdownCleanerWorkerMock();
|
||||
|
||||
EXPECT_CALL(*createCleanerWorkerMock, Call()).Times(0);
|
||||
EXPECT_CALL(*shutdownCleanerWorkerMock, Call(_, _)).Times(0);
|
||||
ShutdownCleaners(true);
|
||||
}
|
||||
|
||||
TEST(CleanerTest, ShutdownWithCreation) {
|
||||
ResetCleanerWorkerForTests();
|
||||
|
||||
constexpr KInt workerId = 42;
|
||||
constexpr bool executeScheduledCleaners = true;
|
||||
|
||||
auto createCleanerWorkerMock = ScopedCreateCleanerWorkerMock();
|
||||
auto shutdownCleanerWorkerMock = ScopedShutdownCleanerWorkerMock();
|
||||
|
||||
EXPECT_CALL(*createCleanerWorkerMock, Call()).WillOnce(testing::Return(workerId));
|
||||
Kotlin_CleanerImpl_getCleanerWorker();
|
||||
|
||||
EXPECT_CALL(*shutdownCleanerWorkerMock, Call(workerId, executeScheduledCleaners));
|
||||
ShutdownCleaners(executeScheduledCleaners);
|
||||
}
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "Alloc.h"
|
||||
#include "KAssert.h"
|
||||
#include "Atomic.h"
|
||||
#include "Cleaner.h"
|
||||
#if USE_CYCLIC_GC
|
||||
#include "CyclicCollector.h"
|
||||
#endif // USE_CYCLIC_GC
|
||||
@@ -1063,14 +1064,26 @@ void freeAggregatingFrozenContainer(ContainerHeader* container) {
|
||||
MEMORY_LOG("Freeing subcontainers done\n");
|
||||
}
|
||||
|
||||
// Not inlining this call as it affects deallocation performance for
|
||||
// all types.
|
||||
NO_INLINE RUNTIME_NOTHROW void runFinalizers(ObjHeader* obj) {
|
||||
auto* type_info = obj->type_info();
|
||||
if (type_info == theCleanerImplTypeInfo) {
|
||||
DisposeCleaner(obj);
|
||||
}
|
||||
if (type_info == theWorkerBoundReferenceTypeInfo) {
|
||||
DisposeWorkerBoundReference(obj);
|
||||
}
|
||||
}
|
||||
|
||||
// This is called from 2 places where it's unconditionally called,
|
||||
// so better be inlined.
|
||||
ALWAYS_INLINE void runDeallocationHooks(ContainerHeader* container) {
|
||||
ObjHeader* obj = reinterpret_cast<ObjHeader*>(container + 1);
|
||||
for (uint32_t index = 0; index < container->objectCount(); index++) {
|
||||
auto* type_info = obj->type_info();
|
||||
if (type_info == theWorkerBoundReferenceTypeInfo) {
|
||||
DisposeWorkerBoundReference(obj);
|
||||
if ((type_info->flags_ & TF_HAS_FINALIZER) != 0) {
|
||||
runFinalizers(obj);
|
||||
}
|
||||
#if USE_CYCLIC_GC
|
||||
if ((type_info->flags_ & TF_LEAK_DETECTOR_CANDIDATE) != 0) {
|
||||
@@ -3543,4 +3556,12 @@ void Kotlin_native_internal_GC_setCyclicCollector(KRef gc, KBoolean value) {
|
||||
#endif // USE_CYCLIC_GC
|
||||
}
|
||||
|
||||
bool Kotlin_Any_isShareable(KRef thiz) {
|
||||
return thiz == nullptr || isShareable(thiz->container());
|
||||
}
|
||||
|
||||
void PerformFullGC() {
|
||||
garbageCollect(::memoryState, true);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -558,6 +558,9 @@ void GC_RegisterWorker(void* worker) RUNTIME_NOTHROW;
|
||||
void GC_UnregisterWorker(void* worker) RUNTIME_NOTHROW;
|
||||
void GC_CollectorCallback(void* worker) RUNTIME_NOTHROW;
|
||||
|
||||
bool Kotlin_Any_isShareable(ObjHeader* thiz);
|
||||
void PerformFullGC() RUNTIME_NOTHROW;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include "Alloc.h"
|
||||
#include "Atomic.h"
|
||||
#include "Cleaner.h"
|
||||
#include "Exceptions.h"
|
||||
#include "KAssert.h"
|
||||
#include "Memory.h"
|
||||
@@ -75,6 +76,7 @@ void InitOrDeinitGlobalVariables(int initialize, MemoryState* memory) {
|
||||
}
|
||||
|
||||
KBoolean g_checkLeaks = KonanNeedDebugInfo;
|
||||
KBoolean g_checkLeakedCleaners = KonanNeedDebugInfo;
|
||||
|
||||
constexpr RuntimeState* kInvalidRuntime = nullptr;
|
||||
|
||||
@@ -284,4 +286,16 @@ void Konan_Platform_setMemoryLeakChecker(KBoolean value) {
|
||||
g_checkLeaks = value;
|
||||
}
|
||||
|
||||
bool Kotlin_cleanersLeakCheckerEnabled() {
|
||||
return g_checkLeakedCleaners;
|
||||
}
|
||||
|
||||
KBoolean Konan_Platform_getCleanersLeakChecker() {
|
||||
return g_checkLeakedCleaners;
|
||||
}
|
||||
|
||||
void Konan_Platform_setCleanersLeakChecker(KBoolean value) {
|
||||
g_checkLeakedCleaners = value;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -55,6 +55,8 @@ void Kotlin_zeroOutTLSGlobals();
|
||||
|
||||
bool Kotlin_memoryLeakCheckerEnabled();
|
||||
|
||||
bool Kotlin_cleanersLeakCheckerEnabled();
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
template <class F>
|
||||
class ScopedStrictMockFunction {
|
||||
public:
|
||||
using Mock = testing::StrictMock<testing::MockFunction<F>>;
|
||||
|
||||
explicit ScopedStrictMockFunction(Mock** globalMockLocation) : globalMockLocation_(globalMockLocation) {
|
||||
RuntimeCheck(globalMockLocation != nullptr, "ScopedStrictMockFunction needs non-null global mock location");
|
||||
RuntimeCheck(*globalMockLocation == nullptr, "ScopedStrictMockFunction needs null global mock");
|
||||
// TODO: Use make_unique when sysroots on Linux get updated.
|
||||
mock_ = std::unique_ptr<Mock>(new Mock());
|
||||
*globalMockLocation_ = mock_.get();
|
||||
}
|
||||
|
||||
ScopedStrictMockFunction(const ScopedStrictMockFunction&) = delete;
|
||||
ScopedStrictMockFunction& operator=(const ScopedStrictMockFunction&) = delete;
|
||||
|
||||
ScopedStrictMockFunction(ScopedStrictMockFunction&& rhs) : globalMockLocation_(rhs.globalMockLocation_), mock_(std::move(rhs.mock_)) {
|
||||
rhs.globalMockLocation_ = nullptr;
|
||||
}
|
||||
|
||||
ScopedStrictMockFunction& operator=(ScopedStrictMockFunction&& rhs) {
|
||||
ScopedStrictMockFunction tmp(std::move(rhs));
|
||||
swap(tmp);
|
||||
return *this;
|
||||
}
|
||||
|
||||
~ScopedStrictMockFunction() {
|
||||
if (!globalMockLocation_) return;
|
||||
|
||||
RuntimeCheck(*globalMockLocation_ == mock_.get(), "unexpected global mock location");
|
||||
|
||||
testing::Mock::VerifyAndClear(mock_.get());
|
||||
mock_.reset();
|
||||
|
||||
*globalMockLocation_ = nullptr;
|
||||
}
|
||||
|
||||
void swap(ScopedStrictMockFunction& other) {
|
||||
std::swap(globalMockLocation_, other.globalMockLocation_);
|
||||
std::swap(mock_, other.mock_);
|
||||
}
|
||||
|
||||
Mock& get() { return *mock_; }
|
||||
Mock& operator*() { return *mock_; }
|
||||
|
||||
private:
|
||||
// Can be null if moved-out of.
|
||||
Mock** globalMockLocation_;
|
||||
std::unique_ptr<Mock> mock_;
|
||||
};
|
||||
|
||||
ScopedStrictMockFunction<KInt()> ScopedCreateCleanerWorkerMock();
|
||||
ScopedStrictMockFunction<void(KInt, bool)> ScopedShutdownCleanerWorkerMock();
|
||||
@@ -54,6 +54,7 @@ enum Konan_RuntimeType {
|
||||
};
|
||||
|
||||
// Flags per type.
|
||||
// Keep in sync with constants in RTTIGenerator.
|
||||
enum Konan_TypeFlags {
|
||||
TF_IMMUTABLE = 1 << 0,
|
||||
TF_ACYCLIC = 1 << 1,
|
||||
@@ -61,6 +62,7 @@ enum Konan_TypeFlags {
|
||||
TF_OBJC_DYNAMIC = 1 << 3,
|
||||
TF_LEAK_DETECTOR_CANDIDATE = 1 << 4,
|
||||
TF_SUSPEND_FUNCTION = 1 << 5,
|
||||
TF_HAS_FINALIZER = 1 << 6,
|
||||
};
|
||||
|
||||
// Flags per object instance.
|
||||
|
||||
@@ -106,6 +106,7 @@ extern const TypeInfo* theStringTypeInfo;
|
||||
extern const TypeInfo* theThrowableTypeInfo;
|
||||
extern const TypeInfo* theUnitTypeInfo;
|
||||
extern const TypeInfo* theWorkerBoundReferenceTypeInfo;
|
||||
extern const TypeInfo* theCleanerImplTypeInfo;
|
||||
|
||||
KBoolean IsInstance(const ObjHeader* obj, const TypeInfo* type_info) RUNTIME_PURE;
|
||||
KBoolean IsInstanceOfClassFast(const ObjHeader* obj, int32_t lo, int32_t hi) RUNTIME_PURE;
|
||||
|
||||
@@ -68,7 +68,7 @@ enum JobKind {
|
||||
// Order is important in sense that all job kinds after this one is considered
|
||||
// processed for APIs returning request process status.
|
||||
JOB_REGULAR = 2,
|
||||
JOB_EXECUTE_AFTER = 3
|
||||
JOB_EXECUTE_AFTER = 3,
|
||||
};
|
||||
|
||||
enum class WorkerKind {
|
||||
@@ -358,6 +358,23 @@ class State {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool scheduleJobInWorkerUnlocked(KInt id, KNativePtr operationStablePtr) {
|
||||
Worker* worker = nullptr;
|
||||
Locker locker(&lock_);
|
||||
|
||||
auto it = workers_.find(id);
|
||||
if (it == workers_.end()) {
|
||||
return false;
|
||||
}
|
||||
worker = it->second;
|
||||
|
||||
Job job;
|
||||
job.kind = JOB_EXECUTE_AFTER;
|
||||
job.executeAfter.operation = operationStablePtr;
|
||||
worker->putJob(job, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Returns `true` if something was indeed processed.
|
||||
bool processQueueUnlocked(KInt id) {
|
||||
// Can only process queue of the current worker.
|
||||
@@ -456,23 +473,28 @@ class State {
|
||||
terminating_native_workers_.erase(it);
|
||||
}
|
||||
|
||||
void waitNativeWorkersTerminationUnlocked() {
|
||||
std::vector<pthread_t> threadsToWait;
|
||||
{
|
||||
Locker locker(&lock_);
|
||||
template <typename F>
|
||||
void waitNativeWorkersTerminationUnlocked(F waitForWorker) {
|
||||
std::vector<std::pair<KInt, pthread_t>> workersToWait;
|
||||
{
|
||||
Locker locker(&lock_);
|
||||
|
||||
checkNativeWorkersLeakLocked();
|
||||
checkNativeWorkersLeakLocked();
|
||||
|
||||
for (auto& kvp : terminating_native_workers_) {
|
||||
RuntimeAssert(!pthread_equal(kvp.second, pthread_self()), "Native worker is joining with itself");
|
||||
threadsToWait.push_back(kvp.second);
|
||||
for (auto& kvp : terminating_native_workers_) {
|
||||
RuntimeAssert(!pthread_equal(kvp.second, pthread_self()), "Native worker is joining with itself");
|
||||
if (waitForWorker(kvp.first)) {
|
||||
workersToWait.push_back(kvp);
|
||||
}
|
||||
}
|
||||
for (auto worker : workersToWait) {
|
||||
terminating_native_workers_.erase(worker.first);
|
||||
}
|
||||
}
|
||||
terminating_native_workers_.clear();
|
||||
}
|
||||
|
||||
for (auto thread : threadsToWait) {
|
||||
pthread_join(thread, nullptr);
|
||||
}
|
||||
for (auto worker : workersToWait) {
|
||||
pthread_join(worker.second, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void checkNativeWorkersLeakLocked() {
|
||||
@@ -720,7 +742,13 @@ void WorkerDestroyThreadDataIfNeeded(KInt id) {
|
||||
|
||||
void WaitNativeWorkersTermination() {
|
||||
#if WITH_WORKERS
|
||||
theState()->waitNativeWorkersTerminationUnlocked();
|
||||
theState()->waitNativeWorkersTerminationUnlocked([](KInt worker) { return true; });
|
||||
#endif
|
||||
}
|
||||
|
||||
void WaitNativeWorkerTermination(KInt id) {
|
||||
#if WITH_WORKERS
|
||||
theState()->waitNativeWorkersTerminationUnlocked([id](KInt worker) { return worker == id; });
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -740,6 +768,14 @@ void WorkerResume(Worker* worker) {
|
||||
#endif // WITH_WORKERS
|
||||
}
|
||||
|
||||
bool WorkerSchedule(KInt id, KNativePtr jobStablePtr) {
|
||||
#if WITH_WORKERS
|
||||
return theState()->scheduleJobInWorkerUnlocked(id, jobStablePtr);
|
||||
#else
|
||||
return false;
|
||||
#endif // WITH_WORKERS
|
||||
}
|
||||
|
||||
#if WITH_WORKERS
|
||||
|
||||
Worker::~Worker() {
|
||||
|
||||
@@ -12,10 +12,14 @@ Worker* WorkerInit(KBoolean errorReporting);
|
||||
void WorkerDeinit(Worker* worker);
|
||||
// Clean up all associated thread state, if this was a native worker.
|
||||
void WorkerDestroyThreadDataIfNeeded(KInt id);
|
||||
// Wait until all terminating native workers finish termination. Expected to be called once.
|
||||
// Wait until all terminating native workers finish termination. Expected to be called at most once.
|
||||
void WaitNativeWorkersTermination();
|
||||
// Wait until terminating native worker `id` finishes termination. Expected to be called at most once for each worker.
|
||||
void WaitNativeWorkerTermination(KInt id);
|
||||
// Schedule the job without the result.
|
||||
bool WorkerSchedule(KInt id, KNativePtr jobStablePtr);
|
||||
|
||||
Worker* WorkerSuspend();
|
||||
void WorkerResume(Worker* worker);
|
||||
|
||||
#endif // RUNTIME_WORKER_H
|
||||
#endif // RUNTIME_WORKER_H
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
#include "Common.h"
|
||||
#include "Types.h"
|
||||
|
||||
// Not inlining this call as it affects deallocation performance for
|
||||
// all types.
|
||||
RUNTIME_NOTHROW void DisposeWorkerBoundReference(KRef thiz) NO_INLINE;
|
||||
RUNTIME_NOTHROW void DisposeWorkerBoundReference(KRef thiz);
|
||||
|
||||
RUNTIME_NOTHROW void WorkerBoundReferenceFreezeHook(KRef thiz);
|
||||
|
||||
|
||||
@@ -92,6 +92,10 @@ public object Platform {
|
||||
public var isMemoryLeakCheckerActive: Boolean
|
||||
get() = Platform_getMemoryLeakChecker()
|
||||
set(value) = Platform_setMemoryLeakChecker(value)
|
||||
|
||||
public var isCleanersLeakCheckerActive: Boolean
|
||||
get() = Platform_getCleanersLeakChecker()
|
||||
set(value) = Platform_setCleanersLeakChecker(value)
|
||||
}
|
||||
|
||||
@SymbolName("Konan_Platform_canAccessUnaligned")
|
||||
@@ -117,3 +121,9 @@ private external fun Platform_getMemoryLeakChecker(): Boolean
|
||||
|
||||
@SymbolName("Konan_Platform_setMemoryLeakChecker")
|
||||
private external fun Platform_setMemoryLeakChecker(value: Boolean): Unit
|
||||
|
||||
@SymbolName("Konan_Platform_getCleanersLeakChecker")
|
||||
private external fun Platform_getCleanersLeakChecker(): Boolean
|
||||
|
||||
@SymbolName("Konan_Platform_setCleanersLeakChecker")
|
||||
private external fun Platform_setCleanersLeakChecker(value: Boolean): Unit
|
||||
|
||||
@@ -28,6 +28,7 @@ external private fun describeWorkerBoundReference(ref: NativePtr): String
|
||||
*/
|
||||
@NoReorderFields
|
||||
@ExportTypeInfo("theWorkerBoundReferenceTypeInfo")
|
||||
@HasFinalizer
|
||||
public class WorkerBoundReference<out T : Any>(value: T) {
|
||||
|
||||
private var ptr = NativePtr.NULL
|
||||
|
||||
@@ -132,3 +132,16 @@ annotation class Independent
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
public annotation class CanBePrecreated
|
||||
|
||||
/**
|
||||
* Marks a class that has a finalizer.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
internal annotation class HasFinalizer
|
||||
|
||||
/**
|
||||
* Marks a declaration that is internal for Kotlin/Native and shouldn't be used externally.
|
||||
*/
|
||||
@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
|
||||
@Retention(value = AnnotationRetention.BINARY)
|
||||
internal annotation class InternalForKotlinNative
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
package kotlin.native.internal
|
||||
|
||||
import kotlin.native.concurrent.*
|
||||
import kotlinx.cinterop.NativePtr
|
||||
|
||||
public interface Cleaner
|
||||
|
||||
/**
|
||||
* Creates an object with a cleanup associated.
|
||||
*
|
||||
* After the resulting object ("cleaner") gets deallocated by memory manager,
|
||||
* [block] is eventually called once with [argument].
|
||||
*
|
||||
* Example of usage:
|
||||
* ```
|
||||
* class ResourceWrapper {
|
||||
* private val resource = Resource()
|
||||
*
|
||||
* private val cleaner = createCleaner(resource) { it.dispose() }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* When `ResourceWrapper` becomes unused and gets deallocated, its `cleaner`
|
||||
* is also deallocated, and the resource is disposed later.
|
||||
*
|
||||
* It is not specified which thread runs [block], as well as whether two or more
|
||||
* blocks from different cleaners can be run in parallel.
|
||||
*
|
||||
* Note: if [argument] refers (directly or indirectly) the cleaner, then both
|
||||
* might leak, and the [block] will not be called in this case.
|
||||
* For example, the code below has a leak:
|
||||
* ```
|
||||
* class LeakingResourceWrapper {
|
||||
* private val resource = Resource()
|
||||
* private val cleaner = createCleaner(this) { it.resource.dispose() }
|
||||
* }
|
||||
* ```
|
||||
* In this case cleaner's argument (`LeakingResourceWrapper`) can't be deallocated
|
||||
* until cleaner's block is executed, which can happen only strictly after
|
||||
* the cleaner is deallocated, which can't happen until `LeakingResourceWrapper`
|
||||
* is deallocated. So the requirements on object deallocations are contradictory
|
||||
* in this case, which can't be handled gracefully. The cleaner's block
|
||||
* is not executed then, and cleaner and its argument might leak
|
||||
* (depending on the implementation).
|
||||
*
|
||||
* [block] should not use `@ThreadLocal` globals, because it may
|
||||
* be executed on a different thread.
|
||||
*
|
||||
* If [block] throws an exception, the behavior is unspecified.
|
||||
*
|
||||
* Cleaners should not be kept in globals, because if cleaner is not deallocated
|
||||
* before exiting main(), it'll never get executed.
|
||||
* Use `Platform.isCleanersLeakCheckerActive` to warn about unexecuted cleaners.
|
||||
*
|
||||
* If cleaners are not GC'd before main() exits, then it's not guaranteed that
|
||||
* they will be run. Moreover, it depends on `Platform.isCleanersLeakCheckerActive`.
|
||||
* With the checker enabled, cleaners will be run (and therefore not reported as
|
||||
* unexecuted cleaners); with the checker disabled - they might not get run.
|
||||
*
|
||||
* @param argument must be shareable
|
||||
* @param block must not capture anything
|
||||
*/
|
||||
// TODO: Consider just annotating the lambda argument rather than hardcoding checking
|
||||
// by function name in the compiler.
|
||||
@ExperimentalStdlibApi
|
||||
@ExportForCompiler
|
||||
fun <T> createCleaner(argument: T, block: (T) -> Unit): Cleaner {
|
||||
if (!argument.isShareable())
|
||||
throw IllegalArgumentException("$argument must be shareable")
|
||||
|
||||
val clean = {
|
||||
// TODO: Maybe if this fails with exception, it should be (optionally) reported.
|
||||
block(argument)
|
||||
}.freeze()
|
||||
|
||||
// Make sure there's an extra reference to clean, so it's definitely alive when CleanerImpl is destroyed.
|
||||
val cleanPtr = createStablePointer(clean)
|
||||
|
||||
// Make sure cleaner worker is initialized.
|
||||
getCleanerWorker()
|
||||
|
||||
return CleanerImpl(cleanPtr).freeze()
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform GC on a worker that executes Cleaner blocks.
|
||||
*/
|
||||
@InternalForKotlinNative
|
||||
fun performGCOnCleanerWorker() =
|
||||
getCleanerWorker().execute(TransferMode.SAFE, {}) {
|
||||
GC.collect()
|
||||
}.result
|
||||
|
||||
@SymbolName("Kotlin_CleanerImpl_getCleanerWorker")
|
||||
external private fun getCleanerWorker(): Worker
|
||||
|
||||
@ExportForCppRuntime("Kotlin_CleanerImpl_shutdownCleanerWorker")
|
||||
private fun shutdownCleanerWorker(worker: Worker, executeScheduledCleaners: Boolean) {
|
||||
worker.requestTermination(executeScheduledCleaners).result
|
||||
}
|
||||
|
||||
@ExportForCppRuntime("Kotlin_CleanerImpl_createCleanerWorker")
|
||||
private fun createCleanerWorker(): Worker {
|
||||
return Worker.start(errorReporting = false, name = "Cleaner worker")
|
||||
}
|
||||
|
||||
@NoReorderFields
|
||||
@ExportTypeInfo("theCleanerImplTypeInfo")
|
||||
@HasFinalizer
|
||||
private class CleanerImpl(
|
||||
private val cleanPtr: NativePtr,
|
||||
): Cleaner {}
|
||||
|
||||
@SymbolName("Kotlin_Any_isShareable")
|
||||
external private fun Any?.isShareable(): Boolean
|
||||
|
||||
@SymbolName("CreateStablePointer")
|
||||
external private fun createStablePointer(obj: Any): NativePtr
|
||||
@@ -3,6 +3,8 @@
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "TestSupportCompilerGenerated.hpp"
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
namespace {
|
||||
@@ -26,6 +28,7 @@ TypeInfo theStringTypeInfoImpl = {};
|
||||
TypeInfo theThrowableTypeInfoImpl = {};
|
||||
TypeInfo theUnitTypeInfoImpl = {};
|
||||
TypeInfo theWorkerBoundReferenceTypeInfoImpl = {};
|
||||
TypeInfo theCleanerImplTypeInfoImpl = {};
|
||||
|
||||
ArrayHeader theEmptyStringImpl = { &theStringTypeInfoImpl, /* element count */ 0 };
|
||||
|
||||
@@ -35,6 +38,9 @@ struct KBox {
|
||||
const T value;
|
||||
};
|
||||
|
||||
testing::StrictMock<testing::MockFunction<KInt()>>* createCleanerWorkerMock = nullptr;
|
||||
testing::StrictMock<testing::MockFunction<void(KInt, bool)>>* shutdownCleanerWorkerMock = nullptr;
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
@@ -61,6 +67,7 @@ extern const TypeInfo* theStringTypeInfo = &theStringTypeInfoImpl;
|
||||
extern const TypeInfo* theThrowableTypeInfo = &theThrowableTypeInfoImpl;
|
||||
extern const TypeInfo* theUnitTypeInfo = &theUnitTypeInfoImpl;
|
||||
extern const TypeInfo* theWorkerBoundReferenceTypeInfo = &theWorkerBoundReferenceTypeInfoImpl;
|
||||
extern const TypeInfo* theCleanerImplTypeInfo = &theCleanerImplTypeInfoImpl;
|
||||
|
||||
extern const ArrayHeader theEmptyArray = { &theArrayTypeInfoImpl, /* element count */0 };
|
||||
|
||||
@@ -230,4 +237,24 @@ RUNTIME_NORETURN OBJ_GETTER(Kotlin_Throwable_getMessage, KRef throwable) {
|
||||
throw std::runtime_error("Not implemented for tests");
|
||||
}
|
||||
|
||||
void Kotlin_CleanerImpl_shutdownCleanerWorker(KInt worker, bool executeScheduledCleaners) {
|
||||
if (!shutdownCleanerWorkerMock) throw std::runtime_error("Not implemented for tests");
|
||||
|
||||
return shutdownCleanerWorkerMock->Call(worker, executeScheduledCleaners);
|
||||
}
|
||||
|
||||
KInt Kotlin_CleanerImpl_createCleanerWorker() {
|
||||
if (!createCleanerWorkerMock) throw std::runtime_error("Not implemented for tests");
|
||||
|
||||
return createCleanerWorkerMock->Call();
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
ScopedStrictMockFunction<KInt()> ScopedCreateCleanerWorkerMock() {
|
||||
return ScopedStrictMockFunction<KInt()>(&createCleanerWorkerMock);
|
||||
}
|
||||
|
||||
ScopedStrictMockFunction<void(KInt, bool)> ScopedShutdownCleanerWorkerMock() {
|
||||
return ScopedStrictMockFunction<void(KInt, bool)>(&shutdownCleanerWorkerMock);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#if KONAN_OBJC_INTEROP
|
||||
|
||||
#include "TestSupportCompilerGenerated.hpp"
|
||||
|
||||
#include <Foundation/NSObject.h>
|
||||
#include <objc/runtime.h>
|
||||
|
||||
@@ -67,4 +69,4 @@ void Kotlin_ObjCExport_resumeContinuationSuccess(KRef continuation, KRef result)
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif // KONAN_OBJC_INTEROP
|
||||
#endif // KONAN_OBJC_INTEROP
|
||||
|
||||
Reference in New Issue
Block a user