[K/N] Add API for garbage collection statistics
^KT-53064
This commit is contained in:
committed by
Space Team
parent
cdf6ffa167
commit
0be789e9e3
@@ -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[] = {
|
||||
|
||||
Reference in New Issue
Block a user