diff --git a/kotlin-native/backend.native/tests/build.gradle b/kotlin-native/backend.native/tests/build.gradle index 1109f096d1d..2170aa56f19 100644 --- a/kotlin-native/backend.native/tests/build.gradle +++ b/kotlin-native/backend.native/tests/build.gradle @@ -3427,6 +3427,12 @@ standaloneTest("leakMemoryWithTestRunner") { } } +standaloneTest("gcStats") { + source = "runtime/memory/gcStats.kt" + flags = ['-tr', "-Xbinary=gcSchedulerType=disabled"] + enabled = isExperimentalMM +} + standaloneTest("stress_gc_allocations") { // TODO: Support obtaining peak RSS on more platforms. enabled = (project.testTarget != "wasm32") && diff --git a/kotlin-native/backend.native/tests/runtime/memory/gcStats.kt b/kotlin-native/backend.native/tests/runtime/memory/gcStats.kt new file mode 100644 index 00000000000..1078c3d80ef --- /dev/null +++ b/kotlin-native/backend.native/tests/runtime/memory/gcStats.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +@file:OptIn(kotlin.ExperimentalStdlibApi::class) + +import kotlin.native.internal.GC +import kotlin.test.* + + +@Test +fun `nothing new collected`() { + GC.collect() + GC.collect(); + val stat = GC.lastGCInfo + assertNotNull(stat) + assertEquals(stat.memoryUsageBefore.keys, stat.memoryUsageAfter.keys) + for (key in stat.memoryUsageBefore.keys) { + assertEquals(stat.memoryUsageBefore[key]!!.objectsCount, stat.memoryUsageAfter[key]!!.objectsCount) + assertEquals(stat.memoryUsageBefore[key]!!.totalObjectsSizeBytes, stat.memoryUsageAfter[key]!!.totalObjectsSizeBytes) + } +} + +object Global { + val x = listOf(1, 2, 3) +} + +@Test +fun `stable refs in root set`() { + GC.collect() + val stat0 = GC.lastGCInfo + assertNotNull(stat0) + val rootSet0 = stat0.rootSet + assertNotNull(rootSet0) + val x = listOf(1, 2, 3) + val stable = kotlinx.cinterop.StableRef.create(x) + GC.collect(); + val stat1 = GC.lastGCInfo + assertNotNull(stat1) + val rootSet1 = stat1.rootSet + assertNotNull(rootSet1) + assertEquals(rootSet0.stableReferences + 1, rootSet1.stableReferences) + stable.dispose() + Global.x // to initialize and register global object + GC.collect(); + val stat2 = GC.lastGCInfo + assertNotNull(stat2) + val rootSet2 = stat2.rootSet + assertNotNull(rootSet2) + assertEquals(rootSet0.stableReferences, rootSet2.stableReferences) + assertEquals(rootSet1.globalReferences + 1, rootSet2.globalReferences) +} + +@Test +fun `check everything is filled at the end`() { + GC.collect() + val stat = GC.lastGCInfo + assertNotNull(stat) + // GC.collect is waiting for finalizers, so it should bet not null + assertNotNull(stat.postGcCleanupTimeNs) +} + diff --git a/kotlin-native/runtime/src/gc/common/cpp/GCStatistics.cpp b/kotlin-native/runtime/src/gc/common/cpp/GCStatistics.cpp index 94c9b914067..7b2e9a82311 100644 --- a/kotlin-native/runtime/src/gc/common/cpp/GCStatistics.cpp +++ b/kotlin-native/runtime/src/gc/common/cpp/GCStatistics.cpp @@ -15,11 +15,38 @@ #include +extern "C" { +void Kotlin_Internal_GC_GCInfoBuilder_setEpoch(KRef thiz, KLong value); +void Kotlin_Internal_GC_GCInfoBuilder_setStartTime(KRef thiz, KLong value); +void Kotlin_Internal_GC_GCInfoBuilder_setEndTime(KRef thiz, KLong value); +void Kotlin_Internal_GC_GCInfoBuilder_setPauseStartTime(KRef thiz, KLong value); +void Kotlin_Internal_GC_GCInfoBuilder_setPauseEndTime(KRef thiz, KLong value); +void Kotlin_Internal_GC_GCInfoBuilder_setPostGcCleanupTime(KRef thiz, KLong value); +void Kotlin_Internal_GC_GCInfoBuilder_setRootSet(KRef thiz, + KLong threadLocalReferences, KLong stackReferences, + KLong globalReferences, KLong stableReferences); +void Kotlin_Internal_GC_GCInfoBuilder_setMemoryUsageBefore(KRef thiz, KNativePtr name, KLong objectsCount, KLong totalObjectsSize); +void Kotlin_Internal_GC_GCInfoBuilder_setMemoryUsageAfter(KRef thiz, KNativePtr name, KLong objectsCount, KLong totalObjectsSize); +} + namespace { struct MemoryUsageMap { std::optional heap; std::optional extra; + + void build(KRef builder, void (*add)(KRef, KNativePtr, KLong, KLong)) { + if (heap) { + add(builder, const_cast(static_cast("heap")), + static_cast(heap->objectsCount), + static_cast(heap->totalObjectsSize)); + } + if (extra) { + add(builder, const_cast(static_cast("extra")), + static_cast(extra->objectsCount), + static_cast(extra->totalObjectsSize)); + } + } }; struct RootSetStatistics { @@ -41,6 +68,22 @@ struct GCInfo { std::optional markStats; MemoryUsageMap memoryUsageBefore; MemoryUsageMap memoryUsageAfter; + + void build(KRef builder) { + if (!epoch) return; + Kotlin_Internal_GC_GCInfoBuilder_setEpoch(builder, static_cast(*epoch)); + if (startTime) Kotlin_Internal_GC_GCInfoBuilder_setStartTime(builder, *startTime); + if (endTime) Kotlin_Internal_GC_GCInfoBuilder_setEndTime(builder, *endTime); + if (pauseStartTime) Kotlin_Internal_GC_GCInfoBuilder_setPauseStartTime(builder, *pauseStartTime); + if (pauseEndTime) Kotlin_Internal_GC_GCInfoBuilder_setPauseEndTime(builder, *pauseEndTime); + if (finalizersDoneTime) Kotlin_Internal_GC_GCInfoBuilder_setPostGcCleanupTime(builder, *finalizersDoneTime); + if (rootSet) + Kotlin_Internal_GC_GCInfoBuilder_setRootSet( + builder, rootSet->threadLocalReferences, rootSet->stackReferences, rootSet->globalReferences, + rootSet->stableReferences); + memoryUsageBefore.build(builder, Kotlin_Internal_GC_GCInfoBuilder_setMemoryUsageBefore); + memoryUsageAfter.build(builder, Kotlin_Internal_GC_GCInfoBuilder_setMemoryUsageAfter); + } }; GCInfo last; @@ -56,6 +99,22 @@ GCInfo* statByEpoch(uint64_t epoch) { } // namespace +extern "C" void Kotlin_Internal_GC_GCInfoBuilder_Fill(KRef builder, int id) { + GCInfo copy; + { + kotlin::ThreadStateGuard stateGuard(kotlin::ThreadState::kNative); + std::lock_guard guard(lock); + if (id == 0) { + copy = last; + } else if (id == 1) { + copy = current; + } else { + return; + } + } + copy.build(builder); +} + namespace kotlin::gc { GCHandle GCHandle::create(uint64_t epoch) { std::lock_guard guard(lock); diff --git a/kotlin-native/runtime/src/main/kotlin/kotlin/native/internal/GC.kt b/kotlin-native/runtime/src/main/kotlin/kotlin/native/internal/GC.kt index 6f367fd061d..53e6d9b1533 100644 --- a/kotlin-native/runtime/src/main/kotlin/kotlin/native/internal/GC.kt +++ b/kotlin-native/runtime/src/main/kotlin/kotlin/native/internal/GC.kt @@ -1,11 +1,12 @@ /* - * Copyright 2010-2018 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. + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ package kotlin.native.internal -import kotlin.time.Duration +import kotlin.native.internal.gc.GCInfo +import kotlin.time.* import kotlin.time.Duration.Companion.microseconds /** @@ -276,6 +277,18 @@ object GC { @Deprecated("No-op in modern GC implementation") external fun detectCycles(): Array? + /** + * Returns statistics of the last finished garbage collection run. + * This information is supposed to be used for testing and debugging purposes only + * + * Can return null, if there was no garbage collection runs yet. + * + * Legacy MM: Always returns null + */ + @ExperimentalStdlibApi + val lastGCInfo: GCInfo? + get() = GCInfo.lastGCInfo + /** * Deprecated and unused. Always returns null. * diff --git a/kotlin-native/runtime/src/main/kotlin/kotlin/native/internal/gc/GCInfo.kt b/kotlin-native/runtime/src/main/kotlin/kotlin/native/internal/gc/GCInfo.kt new file mode 100644 index 00000000000..956af804f97 --- /dev/null +++ b/kotlin-native/runtime/src/main/kotlin/kotlin/native/internal/gc/GCInfo.kt @@ -0,0 +1,170 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.native.internal.gc + +import kotlin.native.internal.* +import kotlin.native.internal.NativePtr +import kotlin.native.concurrent.* +import kotlin.time.* +import kotlin.time.Duration.Companion.nanoseconds +import kotlinx.cinterop.* +import kotlin.system.* + +/** + * This class represents statistics of memory usage in one memory pool. + * + * @property objectsCount The number of allocated objects. + * @property totalObjectsSizeBytes The total size of allocated objects. System allocator overhead is not included, + * so it can not perfectly match the value received by os tools. + * All alignment and auxiliary object headers are included. + */ +@ExperimentalStdlibApi +public class MemoryUsage( + val objectsCount: Long, + val totalObjectsSizeBytes: Long, +) + +/** + * This class represents statistics of the root set for garbage collector run, separated by root set pools. + * These nodes are assumed to be used, even if there are no references for them. + * + * @property threadLocalReferences The number of objects in global variables with `@ThreadLocal` annotation. + * Object is counted once per each thread it was initialized in. + * @property stackReferences The number of objects referenced from the stack of any thread. + * These are function local variables and different temporary values, as function call arguments and + * return values. They would be automatically removed from the root set when a corresponding function + * call is finished. + * @property globalReferences The number of objects in global variables. The object is counted only if the variable is initialized. + * @property stableReferences The number of objects referenced by [kotlinx.cinterop.StableRef]. It includes both explicit usage + * of this API, and internal usages, e.g. inside interop and Worker API. + */ +@ExperimentalStdlibApi +public class RootSetStatistics( + val threadLocalReferences: Long, + val stackReferences: Long, + val globalReferences: Long, + val stableReferences: Long +) + +/** + * This class represents statistics about the single run of the garbage collector. + * It is supposed to be used for testing and debugging purposes only. + * + * @property epoch ID of garbage collector run. + * @property startTimeNs Time, when garbage collector run is started, meausered by [kotlin.system.getTimeNanos]. + * @property endTimeNs Time, when garbage collector run is ended, measured by [kotlin.system.getTimeNanos]. + * After this point, most of the memory is reclaimed, and a new garbage collector run can start. + * @property pauseStartTimeNs Time, when mutator threads are suspended, mesured by [kotlin.system.getTimeNanos]. + * @property pauseEndTimeNs Time, when mutator threads are unsuspended, mesured by [kotlin.system.getTimeNanos]. + * @property postGcCleanupTimeNs Time, when all memory is reclaimed, measured by [kotlin.system.getTimeNanos]. + * If null, memory reclamation is still in progress. + * @property rootSet The number of objects in each root set pool. Check [RootSetStatistics] doc for details. + * @property memoryUsageAfter Memory usage at the start of garbage collector run, separated by memory pools. + * The set of memory pools depends on the collector implementation. + * Can be empty, of colelction is in progress. + * @property memoryUsageBefore Memory usage at the end of garbage collector run, separated by memory pools. + * The set of memory pools depends on the collector implementation. + * Can be empty, of colelction is in progress. + */ +@ExperimentalStdlibApi +public class GCInfo( + val epoch: Long, + val startTimeNs: Long, + val endTimeNs: Long, + val pauseStartTimeNs: Long, + val pauseEndTimeNs: Long, + val postGcCleanupTimeNs: Long?, + val rootSet: RootSetStatistics, + val memoryUsageBefore: Map, + val memoryUsageAfter: Map, +) { + internal companion object { + val lastGCInfo: GCInfo? + get() = getGcInfo(0) + + private fun getGcInfo(id: Int) = GCInfoBuilder().apply { fill(id) }.build(); + } +} + + +@ExperimentalStdlibApi +private class GCInfoBuilder() { + var epoch: Long? = null + var startTimeNs: Long? = null + var endTimeNs: Long? = null + var pauseStartTimeNs: Long? = null + var pauseEndTimeNs: Long? = null + var postGcCleanupTimeNs: Long? = null + var rootSet: RootSetStatistics? = null + var memoryUsageBefore = mutableMapOf() + var memoryUsageAfter = mutableMapOf() + + @ExportForCppRuntime("Kotlin_Internal_GC_GCInfoBuilder_setEpoch") + private fun setEpoch(value: Long) { + epoch = value + } + + @ExportForCppRuntime("Kotlin_Internal_GC_GCInfoBuilder_setStartTime") + private fun setStartTime(value: Long) { + startTimeNs = value + } + + @ExportForCppRuntime("Kotlin_Internal_GC_GCInfoBuilder_setEndTime") + private fun setEndTime(value: Long) { + endTimeNs = value + } + + @ExportForCppRuntime("Kotlin_Internal_GC_GCInfoBuilder_setPauseStartTime") + private fun setPauseStartTime(value: Long) { + pauseStartTimeNs = value + } + + @ExportForCppRuntime("Kotlin_Internal_GC_GCInfoBuilder_setPauseEndTime") + private fun setPauseEndTime(value: Long) { + pauseEndTimeNs = value + } + + @ExportForCppRuntime("Kotlin_Internal_GC_GCInfoBuilder_setPostGcCleanupTime") + private fun setFinalizersDoneTime(value: Long) { + postGcCleanupTimeNs = value + } + + @ExportForCppRuntime("Kotlin_Internal_GC_GCInfoBuilder_setRootSet") + private fun setRootSet(threadLocalReferences: Long, stackReferences: Long, globalReferences: Long, stableReferences: Long) { + rootSet = RootSetStatistics(threadLocalReferences, stackReferences, globalReferences, stableReferences) + } + + @ExportForCppRuntime("Kotlin_Internal_GC_GCInfoBuilder_setMemoryUsageBefore") + private fun setMemoryUsageBefore(name: NativePtr, objectsCount: Long, totalObjectsSize: Long) { + val nameString = interpretCPointer(name)!!.toKString() + val memoryUsage = MemoryUsage(objectsCount, totalObjectsSize) + memoryUsageBefore[nameString] = memoryUsage + } + + @ExportForCppRuntime("Kotlin_Internal_GC_GCInfoBuilder_setMemoryUsageAfter") + private fun setMemoryUsageAfter(name: NativePtr, objectsCount: Long, totalObjectsSize: Long) { + val nameString = interpretCPointer(name)!!.toKString() + val memoryUsage = MemoryUsage(objectsCount, totalObjectsSize) + memoryUsageAfter[nameString] = memoryUsage + } + + fun build(): GCInfo? { + return GCInfo( + epoch ?: return null, + startTimeNs ?: return null, + endTimeNs ?: return null, + pauseStartTimeNs ?: return null, + pauseEndTimeNs ?: return null, + postGcCleanupTimeNs, + rootSet ?: return null, + memoryUsageBefore.toMap(), + memoryUsageAfter.toMap() + ) + } + + @GCUnsafeCall("Kotlin_Internal_GC_GCInfoBuilder_Fill") + external fun fill(id: Int) +} \ No newline at end of file diff --git a/kotlin-native/runtime/src/test_support/cpp/CompilerGenerated.cpp b/kotlin-native/runtime/src/test_support/cpp/CompilerGenerated.cpp index 3b9a4301359..5e6f8b65398 100644 --- a/kotlin-native/runtime/src/test_support/cpp/CompilerGenerated.cpp +++ b/kotlin-native/runtime/src/test_support/cpp/CompilerGenerated.cpp @@ -220,6 +220,35 @@ void Kotlin_WorkerBoundReference_freezeHook(KRef thiz) { throw std::runtime_error("Not implemented for tests"); } +void Kotlin_Internal_GC_GCInfoBuilder_setEpoch(KRef thiz, KLong value) { + throw std::runtime_error("Not implemented for tests"); +} +void Kotlin_Internal_GC_GCInfoBuilder_setStartTime(KRef thiz, KLong value) { + throw std::runtime_error("Not implemented for tests"); +} +void Kotlin_Internal_GC_GCInfoBuilder_setEndTime(KRef thiz, KLong value) { + throw std::runtime_error("Not implemented for tests"); +} +void Kotlin_Internal_GC_GCInfoBuilder_setPauseStartTime(KRef thiz, KLong value) { + throw std::runtime_error("Not implemented for tests"); +} +void Kotlin_Internal_GC_GCInfoBuilder_setPauseEndTime(KRef thiz, KLong value) { + throw std::runtime_error("Not implemented for tests"); +} +void Kotlin_Internal_GC_GCInfoBuilder_setPostGcCleanupTime(KRef thiz, KLong value) { + throw std::runtime_error("Not implemented for tests"); +} +void Kotlin_Internal_GC_GCInfoBuilder_setRootSet(KRef thiz, KLong threadLocalReferences, KLong stackReferences, KLong globalReferences, KLong stableReferences) { + throw std::runtime_error("Not implemented for tests"); +} +void Kotlin_Internal_GC_GCInfoBuilder_setMemoryUsageBefore(KRef thiz, KNativePtr name, KLong objectsCount, KLong totalObjectsSize) { + throw std::runtime_error("Not implemented for tests"); +} +void Kotlin_Internal_GC_GCInfoBuilder_setMemoryUsageAfter(KRef thiz, KNativePtr name, KLong objectsCount, KLong totalObjectsSize) { + throw std::runtime_error("Not implemented for tests"); +} + + extern const KBoolean BOOLEAN_RANGE_FROM = false; extern const KBoolean BOOLEAN_RANGE_TO = true; extern KBox BOOLEAN_CACHE[] = {