[K/N] Add API for garbage collection statistics

^KT-53064
This commit is contained in:
Pavel Kunyavskiy
2022-10-04 14:58:01 +02:00
committed by Space Team
parent cdf6ffa167
commit 0be789e9e3
6 changed files with 343 additions and 3 deletions
@@ -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") &&
@@ -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)
}
@@ -15,11 +15,38 @@
#include <mutex>
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<kotlin::gc::MemoryUsage> heap;
std::optional<kotlin::gc::MemoryUsage> extra;
void build(KRef builder, void (*add)(KRef, KNativePtr, KLong, KLong)) {
if (heap) {
add(builder, const_cast<KNativePtr>(static_cast<const void*>("heap")),
static_cast<KLong>(heap->objectsCount),
static_cast<KLong>(heap->totalObjectsSize));
}
if (extra) {
add(builder, const_cast<KNativePtr>(static_cast<const void*>("extra")),
static_cast<KLong>(extra->objectsCount),
static_cast<KLong>(extra->totalObjectsSize));
}
}
};
struct RootSetStatistics {
@@ -41,6 +68,22 @@ struct GCInfo {
std::optional<kotlin::gc::MemoryUsage> markStats;
MemoryUsageMap memoryUsageBefore;
MemoryUsageMap memoryUsageAfter;
void build(KRef builder) {
if (!epoch) return;
Kotlin_Internal_GC_GCInfoBuilder_setEpoch(builder, static_cast<KLong>(*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);
@@ -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<Any>?
/**
* 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.
*
@@ -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<String, MemoryUsage>,
val memoryUsageAfter: Map<String, MemoryUsage>,
) {
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<String, MemoryUsage>()
var memoryUsageAfter = mutableMapOf<String, MemoryUsage>()
@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<ByteVar>(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<ByteVar>(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)
}
@@ -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<KBoolean> BOOLEAN_CACHE[] = {