Generalize finalization with Cleaner (#4362)

This commit is contained in:
Alexander Shabalin
2020-10-19 11:57:24 +03:00
committed by GitHub
parent 6937ac56e7
commit ac1a466cc9
41 changed files with 1506 additions and 29 deletions
@@ -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")
}
@@ -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()!! }
@@ -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
@@ -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
}
}
+92
View File
@@ -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
+6 -2
View File
@@ -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")
+12 -3
View File
@@ -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;
+130
View File
@@ -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);
}
+20
View File
@@ -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
+80
View File
@@ -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);
}
+23 -2
View File
@@ -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"
+3
View File
@@ -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
+14
View File
@@ -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"
+2
View File
@@ -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();
+2
View File
@@ -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.
+1
View File
@@ -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;
+51 -15
View File
@@ -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() {
+6 -2
View File
@@ -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
+1 -3
View File
@@ -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