Shared cyclic garbage detector. (#3616)

This commit is contained in:
Nikolay Igotti
2019-12-10 16:47:32 +03:00
committed by GitHub
parent ac2729f316
commit cae31c7853
12 changed files with 337 additions and 56 deletions
@@ -25,6 +25,7 @@ object KonanFqNames {
val threadLocal = FqName("kotlin.native.concurrent.ThreadLocal")
val sharedImmutable = FqName("kotlin.native.concurrent.SharedImmutable")
val frozen = FqName("kotlin.native.internal.Frozen")
val leakDetectorCandidate = FqName("kotlin.native.internal.LeakDetectorCandidate")
val typedIntrinsic = FqName("kotlin.native.internal.TypedIntrinsic")
val objCMethod = FqName("kotlinx.cinterop.ObjCMethod")
}
@@ -60,6 +60,9 @@ internal class RTTIGenerator(override val context: Context) : ContextUtils {
result = result or TF_ACYCLIC
}
}
if (irClass.hasAnnotation(KonanFqNames.leakDetectorCandidate)) {
result = result or TF_LEAK_DETECTOR_CANDIDATE
}
if (irClass.isInterface)
result = result or TF_INTERFACE
return result
@@ -569,3 +572,6 @@ internal class RTTIGenerator(override val context: Context) : ContextUtils {
private const val TF_IMMUTABLE = 1
private const val TF_ACYCLIC = 2
private const val TF_INTERFACE = 4
private const val TF_OBJC_DYNAMIC = 8
private const val TF_LEAK_DETECTOR_CANDIDATE = 16
+6
View File
@@ -2721,6 +2721,12 @@ standaloneTest("memory_only_gc") {
source = "runtime/memory/only_gc.kt"
}
standaloneTest("leak_detector") {
disabled = project.globalTestArgs.contains('-opt')
flags = ['-g']
source = "runtime/memory/leak_detector.kt"
}
standaloneTest("mpp1") {
source = "codegen/mpp/mpp1.kt"
flags = ['-tr', '-Xmulti-platform']
@@ -0,0 +1,61 @@
import kotlin.native.concurrent.*
import kotlin.native.internal.GC
import kotlin.test.*
/*
* Typical snippet for the leak detector usage.
*/
fun dumpLeaks() {
GC.collect()
GC.detectCycles()?.let { cycles ->
cycles.firstOrNull()?.let { root ->
val cycle = GC.findCycle(root)
println(cycle?.contentToString())
}
}
}
fun test1() {
val a = AtomicReference<Any?>(null)
a.value = a
val cycles = GC.detectCycles()!!
assertEquals(1, cycles.size)
assertTrue(arrayOf(a).contentEquals(GC.findCycle(cycles[0])!!))
a.value = null
}
class Holder(var other: Any?)
fun test2() {
val array = arrayOf(AtomicReference<Any?>(null), AtomicReference<Any?>(null))
val obj1 = Holder(array).freeze()
array[0].value = obj1
val cycles = GC.detectCycles()!!
assertEquals(1, cycles.size)
assertTrue(arrayOf(obj1, array, array[0]).contentEquals(GC.findCycle(cycles[0])!!))
array[0].value = null
}
fun test3() {
val a1 = FreezableAtomicReference<Any?>(null)
val head = Holder(null)
var current = head
repeat(30) {
val next = Holder(null)
current.other = next
current = next
}
a1.value = head
current.other = a1
val cycles = GC.detectCycles()!!
assertEquals(1, cycles.size)
val cycle = GC.findCycle(cycles[0])!!
assertEquals(32, cycle.size)
a1.value = null
}
fun main() {
test1()
test2()
test3()
}
+177 -24
View File
@@ -89,8 +89,13 @@ constexpr size_t kMaxGcAllocThreshold = 8 * 1024 * 1024;
typedef KStdUnorderedSet<ContainerHeader*> ContainerHeaderSet;
typedef KStdVector<ContainerHeader*> ContainerHeaderList;
typedef KStdVector<KRef*> KRefPtrList;
typedef KStdDeque<ContainerHeader*> ContainerHeaderDeque;
typedef KStdVector<KRef> KRefList;
typedef KStdVector<KRef*> KRefPtrList;
typedef KStdUnorderedSet<KRef> KRefSet;
typedef KStdUnorderedMap<KRef, KInt> KRefIntMap;
typedef KStdDeque<KRef> KRefDeque;
typedef KStdDeque<KRefList> KRefListDeque;
// A little hack that allows to enable -O2 optimizations
// Prevents clang from replacing FrameOverlay struct
@@ -104,6 +109,10 @@ volatile int aliveMemoryStatesCount = 0;
KBoolean g_checkLeaks = KonanNeedDebugInfo;
// Only used by the leak detector.
KRef g_leakCheckerGlobalList = nullptr;
KInt g_leakCheckerGlobalLock = 0;
// TODO: can we pass this variable as an explicit argument?
THREAD_LOCAL_VARIABLE MemoryState* memoryState = nullptr;
THREAD_LOCAL_VARIABLE FrameOverlay* currentFrame = nullptr;
@@ -689,24 +698,38 @@ inline container_size_t objectSize(const ObjHeader* obj) {
return alignUp(size, kObjectAlignment);
}
template <typename func>
inline void traverseObjectFields(ObjHeader* obj, func process) {
const TypeInfo* typeInfo = obj->type_info();
if (typeInfo != theArrayTypeInfo) {
for (int index = 0; index < typeInfo->objOffsetsCount_; index++) {
ObjHeader** location = reinterpret_cast<ObjHeader**>(
reinterpret_cast<uintptr_t>(obj) + typeInfo->objOffsets_[index]);
process(location);
}
} else {
ArrayHeader* array = obj->array();
for (int index = 0; index < array->count_; index++) {
process(ArrayAddressOfElementAt(array, index));
}
}
}
template <typename func>
inline void traverseReferredObjects(ObjHeader* obj, func process) {
traverseObjectFields(obj, [process](ObjHeader** location) {
ObjHeader* ref = *location;
if (ref != nullptr) process(ref);
});
}
template <typename func>
inline void traverseContainerObjectFields(ContainerHeader* container, func process) {
RuntimeAssert(!isAggregatingFrozenContainer(container), "Must not be called on such containers");
ObjHeader* obj = reinterpret_cast<ObjHeader*>(container + 1);
for (int object = 0; object < container->objectCount(); object++) {
const TypeInfo* typeInfo = obj->type_info();
if (typeInfo != theArrayTypeInfo) {
for (int index = 0; index < typeInfo->objOffsetsCount_; index++) {
ObjHeader** location = reinterpret_cast<ObjHeader**>(
reinterpret_cast<uintptr_t>(obj) + typeInfo->objOffsets_[index]);
process(location);
}
} else {
ArrayHeader* array = obj->array();
for (int index = 0; index < array->count_; index++) {
process(ArrayAddressOfElementAt(array, index));
}
}
traverseObjectFields(obj, process);
obj = reinterpret_cast<ObjHeader*>(
reinterpret_cast<uintptr_t>(obj) + objectSize(obj));
}
@@ -897,14 +920,29 @@ void freeAggregatingFrozenContainer(ContainerHeader* container) {
void runDeallocationHooks(ContainerHeader* container) {
ObjHeader* obj = reinterpret_cast<ObjHeader*>(container + 1);
for (int index = 0; index < container->objectCount(); index++) {
if (obj->has_meta_object()) {
if (KonanNeedDebugInfo && (obj->type_info()->flags_ & TF_LEAK_DETECTOR_CANDIDATE) != 0 && g_checkLeaks) {
// Remove the object from the double-linked list of potentially cyclic objects.
auto* meta = obj->meta_object();
lock(&g_leakCheckerGlobalLock);
// Get previous.
auto* previous = meta->LeakDetector.previous_;
auto* previousMeta = (previous != nullptr) ? previous->meta_object() : nullptr;
auto* next = meta->LeakDetector.next_;
auto* nextMeta = (next != nullptr) ? next->meta_object() : nullptr;
// Remove current.
if (previous != nullptr)
previous->meta_object()->LeakDetector.next_ = next;
if (next != nullptr)
next->meta_object()->LeakDetector.previous_ = previous;
if (obj == g_leakCheckerGlobalList)
g_leakCheckerGlobalList = next;
unlock(&g_leakCheckerGlobalLock);
}
ObjHeader::destroyMetaObject(&obj->typeInfoOrMeta_);
}
obj = reinterpret_cast<ObjHeader*>(
reinterpret_cast<uintptr_t>(obj) + objectSize(obj));
obj = reinterpret_cast<ObjHeader*>(reinterpret_cast<uintptr_t>(obj) + objectSize(obj));
}
}
@@ -1364,8 +1402,8 @@ void collectWhite(MemoryState* state, ContainerHeader* start) {
toVisit.push_front(childContainer);
}
});
runDeallocationHooks(container);
scheduleDestroyContainer(state, container);
runDeallocationHooks(container);
scheduleDestroyContainer(state, container);
}
}
#endif
@@ -1877,6 +1915,18 @@ OBJ_GETTER(allocInstance, const TypeInfo* type_info) {
checkIfGcNeeded(state);
#endif // USE_GC
auto container = ObjectContainer(state, type_info);
ObjHeader* obj = container.GetPlace();
if (KonanNeedDebugInfo && g_checkLeaks && (type_info->flags_ & TF_LEAK_DETECTOR_CANDIDATE) != 0) {
// Add newly allocated object to the double-linked list of potentially cyclic objects.
MetaObjHeader* meta = obj->meta_object();
lock(&g_leakCheckerGlobalLock);
KRef old = g_leakCheckerGlobalList;
g_leakCheckerGlobalList = obj;
meta->LeakDetector.next_ = old;
if (old != nullptr)
old->meta_object()->LeakDetector.previous_ = obj;
unlock(&g_leakCheckerGlobalLock);
}
#if USE_GC
if (Strict) {
rememberNewContainer(container.header());
@@ -1884,7 +1934,7 @@ OBJ_GETTER(allocInstance, const TypeInfo* type_info) {
makeShareable(container.header());
}
#endif // USE_GC
RETURN_OBJ(container.GetPlace());
RETURN_OBJ(obj);
}
template <bool Strict>
@@ -2455,6 +2505,7 @@ void ensureNeverFrozen(ObjHeader* object) {
object->meta_object()->flags_ |= MF_NEVER_FROZEN;
}
// TODO: incorrect, use 3-color scheme.
KBoolean ensureAcyclicAndSet(ObjHeader* where, KInt index, ObjHeader* what) {
RuntimeAssert(where->container() != nullptr && where->container()->frozen(), "Must be used on frozen objects only");
RuntimeAssert(what == nullptr || isPermanentOrFrozen(what),
@@ -2504,6 +2555,99 @@ void shareAny(ObjHeader* obj) {
container->makeShared();
}
OBJ_GETTER0(detectCyclicReferences) {
// Collect rootset, hold references to simplify remaining code.
KRefList rootset;
lock(&g_leakCheckerGlobalLock);
auto* candidate = g_leakCheckerGlobalList;
while (candidate != nullptr) {
addHeapRef(candidate);
rootset.push_back(candidate);
candidate = candidate->meta_object()->LeakDetector.next_;
}
unlock(&g_leakCheckerGlobalLock);
KRefSet cyclic;
KRefSet seen;
KRefDeque toVisit;
for (auto* root: rootset) {
seen.clear();
toVisit.clear();
traverseReferredObjects(root, [&toVisit](ObjHeader* obj) { toVisit.push_front(obj); });
bool seenToRoot = false;
while (!toVisit.empty() && !seenToRoot) {
KRef current = toVisit.front();
toVisit.pop_front();
if (current == root) seenToRoot = true;
// TODO: racy against concurrent mutators.
if (seen.count(current) == 0) {
traverseReferredObjects(current, [&toVisit](ObjHeader* obj) {
toVisit.push_front(obj);
});
seen.insert(current);
}
}
if (seenToRoot) {
cyclic.insert(root);
}
}
int numElements = cyclic.size();
ArrayHeader* result = AllocArrayInstance(theArrayTypeInfo, numElements, OBJ_RESULT)->array();
KRef* place = ArrayAddressOfElementAt(result, 0);
for (auto* it: cyclic) {
UpdateHeapRef(place++, it);
}
for (auto* root: rootset) {
ReleaseHeapRef(root);
}
RETURN_OBJ(result->obj());
}
OBJ_GETTER(findCycle, KRef root) {
KRefSet seen;
KRefListDeque queue;
KRefDeque toVisit;
KRefList path;
traverseReferredObjects(root, [&toVisit](ObjHeader* obj) { toVisit.push_front(obj); });
bool isFound = false;
while (!toVisit.empty() && !isFound) {
KRef current = toVisit.front();
toVisit.pop_front();
// Do DFS path search.
KRefList first;
first.push_back(current);
queue.emplace_back(first);
seen.clear();
while (!queue.empty()) {
KRefList currentPath = queue.back();
queue.pop_back();
KRef node = currentPath[currentPath.size() - 1];
if (node == root) {
isFound = true;
path = currentPath;
break;
}
if (seen.count(node) == 0) {
// TODO: racy against concurrent mutators.
traverseReferredObjects(node, [&queue, &currentPath](ObjHeader* obj) {
KRefList newPath(currentPath);
newPath.push_back(obj);
queue.emplace_back(newPath);
});
seen.insert(node);
}
}
}
ArrayHeader* result = nullptr;
if (isFound) {
result = AllocArrayInstance(theArrayTypeInfo, path.size(), OBJ_RESULT)->array();
KRef* place = ArrayAddressOfElementAt(result, 0);
for (auto* it: path) {
UpdateHeapRef(place++, it);
}
}
RETURN_OBJ(result->obj());
}
} // namespace
MetaObjHeader* ObjHeader::createMetaObject(TypeInfo** location) {
@@ -2535,9 +2679,9 @@ MetaObjHeader* ObjHeader::createMetaObject(TypeInfo** location) {
void ObjHeader::destroyMetaObject(TypeInfo** location) {
MetaObjHeader* meta = clearPointerBits(*(reinterpret_cast<MetaObjHeader**>(location)), OBJECT_TAG_MASK);
*const_cast<const TypeInfo**>(location) = meta->typeInfo_;
if (meta->counter_ != nullptr) {
WeakReferenceCounterClear(meta->counter_);
ZeroHeapRef(&meta->counter_);
if (meta->WeakReference.counter_ != nullptr) {
WeakReferenceCounterClear(meta->WeakReference.counter_);
ZeroHeapRef(&meta->WeakReference.counter_);
}
#ifdef KONAN_OBJC_INTEROP
@@ -2906,6 +3050,15 @@ KBoolean Kotlin_native_internal_GC_getTuneThreshold(KRef) {
#endif
}
OBJ_GETTER(Kotlin_native_internal_GC_detectCycles, KRef) {
if (!KonanNeedDebugInfo || !g_checkLeaks) RETURN_OBJ(nullptr);
RETURN_RESULT_OF0(detectCyclicReferences);
}
OBJ_GETTER(Kotlin_native_internal_GC_findCycle, KRef, KRef root) {
RETURN_RESULT_OF(findCycle, root);
}
KNativePtr CreateStablePointer(KRef any) {
return createStablePointer(any);
}
+20 -9
View File
@@ -315,16 +315,27 @@ ALWAYS_INLINE bool hasPointerBits(T* ptr, unsigned bits) {
struct MetaObjHeader {
// Pointer to the type info. Must be first, to match ArrayHeader and ObjHeader layout.
const TypeInfo* typeInfo_;
// Strong reference to the counter object.
ObjHeader* counter_;
// Container pointer.
ContainerHeader* container_;
#ifdef KONAN_OBJC_INTEROP
void* associatedObject_;
#endif
// Flags for the object state.
int32_t flags_;
// TODO: maybe make it a union for the orthogonal features.
struct {
// Strong reference to the counter object.
ObjHeader* counter_;
} WeakReference;
struct {
// Leak detector's previous list element.
ObjHeader* previous_;
// Leak detector's next list element.
ObjHeader* next_;
} LeakDetector;
};
// Header of every object.
@@ -403,16 +414,16 @@ extern "C" {
returnType name(__VA_ARGS__) RUNTIME_NOTHROW; \
returnType name##Strict(__VA_ARGS__) RUNTIME_NOTHROW; \
returnType name##Relaxed(__VA_ARGS__) RUNTIME_NOTHROW;
#define RETURN_OBJ(value) { ObjHeader* obj = value; \
UpdateReturnRef(OBJ_RESULT, obj); \
return obj; }
#define RETURN_OBJ(value) { ObjHeader* __obj = value; \
UpdateReturnRef(OBJ_RESULT, __obj); \
return __obj; }
#define RETURN_RESULT_OF0(name) { \
ObjHeader* obj = name(OBJ_RESULT); \
return obj; \
ObjHeader* __obj = name(OBJ_RESULT); \
return __obj; \
}
#define RETURN_RESULT_OF(name, ...) { \
ObjHeader* result = name(__VA_ARGS__, OBJ_RESULT); \
return result; \
ObjHeader* __result = name(__VA_ARGS__, OBJ_RESULT); \
return __result; \
}
struct MemoryState;
+2 -1
View File
@@ -57,7 +57,8 @@ enum Konan_TypeFlags {
TF_IMMUTABLE = 1 << 0,
TF_ACYCLIC = 1 << 1,
TF_INTERFACE = 1 << 2,
TF_OBJC_DYNAMIC = 1 << 3
TF_OBJC_DYNAMIC = 1 << 3,
TF_LEAK_DETECTOR_CANDIDATE = 1 << 4
};
// Flags per object instance.
+3 -3
View File
@@ -61,13 +61,13 @@ OBJ_GETTER(Konan_getWeakReferenceImpl, ObjHeader* referred) {
}
#endif // KONAN_OBJC_INTEROP
if (meta->counter_ == nullptr) {
if (meta->WeakReference.counter_ == nullptr) {
ObjHolder counterHolder;
// Cast unneeded, just to emphasize we store an object reference as void*.
ObjHeader* counter = makeWeakReferenceCounter(reinterpret_cast<void*>(referred), counterHolder.slot());
UpdateHeapRefIfNull(&meta->counter_, counter);
UpdateHeapRefIfNull(&meta->WeakReference.counter_, counter);
}
RETURN_OBJ(meta->counter_);
RETURN_OBJ(meta->WeakReference.counter_);
}
// Materialize a weak reference to either null or the real reference.
@@ -7,6 +7,7 @@ package kotlin.native.concurrent
import kotlin.native.internal.ExportTypeInfo
import kotlin.native.internal.Frozen
import kotlin.native.internal.LeakDetectorCandidate
import kotlin.native.internal.NoReorderFields
import kotlin.native.SymbolName
import kotlinx.cinterop.NativePtr
@@ -204,12 +205,22 @@ public class AtomicNativePtr(private var value_: NativePtr) {
private external fun getImpl(): NativePtr
}
private fun idString(value: Any) = "${value.hashCode().toUInt().toString(16)}"
private fun debugString(value: Any?): String {
if (value == null) return "null"
return "${value::class.qualifiedName}: ${idString(value)}"
}
/**
* An atomic reference to a frozen Kotlin object. Can be used in concurrent scenarious
* but frequently shall be of nullable type and be zeroed out (with `compareAndSwap(get(), null)`)
* once no longer needed. Otherwise memory leak could happen.
* but frequently shall be of nullable type and be zeroed out once no longer needed.
* Otherwise memory leak could happen. To detect such leaks [kotlin.native.internal.GC.detectCycles]
* in debug mode could be helpful.
*/
@Frozen
@LeakDetectorCandidate
@NoReorderFields
public class AtomicReference<T>(private var value_: T) {
// A spinlock to fix potential ARC race.
@@ -263,7 +274,8 @@ public class AtomicReference<T>(private var value_: T) {
*
* @return string representation of this object
*/
public override fun toString(): String = "Atomic reference to $value"
public override fun toString(): String =
"${debugString(this)} -> ${debugString(value)}"
// Implementation details.
@SymbolName("Kotlin_AtomicReference_set")
@@ -271,15 +283,16 @@ public class AtomicReference<T>(private var value_: T) {
@SymbolName("Kotlin_AtomicReference_get")
private external fun getImpl(): Any?
}
/**
* An atomic reference to a Kotlin object. Can be used in concurrent scenarious, but must be frozen first,
* otherwise behaves as regular box for the value.
* otherwise behaves as regular box for the value. If frozen, shall be zeroed out once no longer needed.
* Otherwise memory leak could happen. To detect such leaks [kotlin.native.internal.GC.detectCycles]
* in debug mode could be helpful.
*/
@NoReorderFields
@LeakDetectorCandidate
@ExportTypeInfo("theFreezableAtomicReferenceTypeInfo")
public class FreezableAtomicReference<T>(private var value_: T) {
// A spinlock to fix potential ARC race.
@@ -342,7 +355,8 @@ public class FreezableAtomicReference<T>(private var value_: T) {
*
* @return string representation of this object
*/
public override fun toString(): String = "Freezable atomic reference to $value"
public override fun toString(): String =
"${debugString(this)} -> ${debugString(value)}"
// Implementation details.
@SymbolName("Kotlin_AtomicReference_set")
@@ -115,4 +115,10 @@ annotation class Independent
*/
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.BINARY)
@PublishedApi internal annotation class FilterExceptions
@PublishedApi internal annotation class FilterExceptions
/**
* Marks a class whose instances to be added to the list of leak detector candidates.
*/
@Target(AnnotationTarget.CLASS)
@PublishedApi internal annotation class LeakDetectorCandidate
@@ -78,6 +78,24 @@ object GC {
get() = getTuneThreshold()
set(value) = setTuneThreshold(value)
/**
* Detect cyclic references going via atomic references and return list of cycle-inducing objects
* or `null` if the leak detector is not available. Use [Platform.isMemoryLeakCheckerActive] to check
* leak detector availability.
* Note that cycle detector requires reference graph stability, thus it may not work as
* expected or even crash for mutating graphs.
*/
@SymbolName("Kotlin_native_internal_GC_detectCycles")
external fun detectCycles(): Array<Any>?
/**
* Find a reference cycle including from the given object, `null` if no cycles detected.
* Note that cycle detector requires reference graph stability, thus it may not work as
* expected or even crash for mutating graphs.
*/
@SymbolName("Kotlin_native_internal_GC_findCycle")
external fun findCycle(root: Any): Array<Any>?
@SymbolName("Kotlin_native_internal_GC_getThreshold")
private external fun getThreshold(): Int
@@ -174,17 +174,21 @@ internal fun getProgressionLast(start: Long, end: Long, step: Long): Long =
// Called by the debugger.
@ExportForCppRuntime
internal fun KonanObjectToUtf8Array(value: Any?): ByteArray {
val string = when (value) {
is Array<*> -> value.contentToString()
is CharArray -> value.contentToString()
is BooleanArray -> value.contentToString()
is ByteArray -> value.contentToString()
is ShortArray -> value.contentToString()
is IntArray -> value.contentToString()
is LongArray -> value.contentToString()
is FloatArray -> value.contentToString()
is DoubleArray -> value.contentToString()
else -> value.toString()
val string = try {
when (value) {
is Array<*> -> value.contentToString()
is CharArray -> value.contentToString()
is BooleanArray -> value.contentToString()
is ByteArray -> value.contentToString()
is ShortArray -> value.contentToString()
is IntArray -> value.contentToString()
is LongArray -> value.contentToString()
is FloatArray -> value.contentToString()
is DoubleArray -> value.contentToString()
else -> value.toString()
}
} catch (error: Throwable) {
"<Thrown $error when converting to string>"
}
return string.encodeToByteArray()
}