basic legahy heap dumper
diff --git a/kotlin-native/runtime/src/alloc/legacy/cpp/AllocatorImpl.cpp b/kotlin-native/runtime/src/alloc/legacy/cpp/AllocatorImpl.cpp
index fecd919..8297b19 100644
--- a/kotlin-native/runtime/src/alloc/legacy/cpp/AllocatorImpl.cpp
+++ b/kotlin-native/runtime/src/alloc/legacy/cpp/AllocatorImpl.cpp
@@ -6,6 +6,7 @@
 #include "AllocatorImpl.hpp"
 
 #include "ThreadData.hpp"
+#include "ObjectTraversal.hpp"
 
 using namespace kotlin;
 
@@ -72,3 +73,19 @@
     auto* threadData = mm::ThreadRegistry::Instance().CurrentThreadData();
     threadData->allocator().impl().extraObjectDataFactoryThreadQueue().DestroyExtraObjectData(extraObject);
 }
+
+void alloc::Allocator::Impl::dumpHeap() noexcept {
+    graphviz::LogPrinter<kTagGC, logging::Level::kDebug> printer;
+    HeapDump heapDump(printer);
+
+    for (auto node : objectFactory_.LockForIter()) {
+        auto obj = node.GetObjHeader();
+
+        heapDump.object(obj);
+
+        traverseReferredObjects(obj, [&] (ObjHeader* referred){
+            heapDump.reference(obj, referred);
+        });
+    }
+}
+
diff --git a/kotlin-native/runtime/src/alloc/legacy/cpp/AllocatorImpl.hpp b/kotlin-native/runtime/src/alloc/legacy/cpp/AllocatorImpl.hpp
index b1e40ba..809f20b 100644
--- a/kotlin-native/runtime/src/alloc/legacy/cpp/AllocatorImpl.hpp
+++ b/kotlin-native/runtime/src/alloc/legacy/cpp/AllocatorImpl.hpp
@@ -16,6 +16,8 @@
 #include "ObjectFactorySweep.hpp"
 #include "Logging.hpp"
 
+#include "HeapDumper.hpp"
+
 namespace kotlin::alloc {
 
 struct ObjectFactoryTraits {
@@ -44,6 +46,8 @@
     ObjectFactoryImpl& objectFactory() noexcept { return objectFactory_; }
     ExtraObjectDataFactory& extraObjectDataFactory() noexcept { return extraObjectDataFactory_; }
 
+    void dumpHeap() noexcept;
+
 private:
     ObjectFactoryImpl objectFactory_;
     ExtraObjectDataFactory extraObjectDataFactory_;
diff --git a/kotlin-native/runtime/src/alloc/legacy/cpp/HeapDumper.hpp b/kotlin-native/runtime/src/alloc/legacy/cpp/HeapDumper.hpp
new file mode 100644
index 0000000..7911597
--- /dev/null
+++ b/kotlin-native/runtime/src/alloc/legacy/cpp/HeapDumper.hpp
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2010-2024 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.
+ */
+
+#pragma once
+
+#include <sstream>
+
+#include "Graphviz.hpp"
+#include "KString.h"
+
+namespace kotlin::alloc {
+
+template <typename Printer>
+class HeapDump {
+public:
+    explicit HeapDump(const Printer& printer) : printer_(printer) {}
+
+    void object(const ObjHeader* obj) {
+        auto objId = id(obj);
+        auto typeName = name(obj->type_info());
+
+        std::stringstream label;
+        label << objId << "[" << typeName << "]";
+
+        graph_.node(id(obj), label.str());
+
+        if (auto extraObj = mm::ExtraObjectData::Get(obj)) {
+            std::stringstream flags;
+            flags << "{";
+            if (extraObj->getFlag(mm::ExtraObjectData::FLAGS_FROZEN)) flags << "FROZEN,";
+            if (extraObj->getFlag(mm::ExtraObjectData::FLAGS_NEVER_FROZEN)) flags << "NEVER_FROZEN,";
+            if (extraObj->getFlag(mm::ExtraObjectData::FLAGS_IN_FINALIZER_QUEUE)) flags << "IN_FIN_Q,";
+            if (extraObj->getFlag(mm::ExtraObjectData::FLAGS_SWEEPABLE)) flags << "SWEEPABLE,";
+            if (extraObj->getFlag(mm::ExtraObjectData::FLAGS_RELEASE_ON_MAIN_QUEUE)) flags << "RELEASE_ON_MAIN,";
+            if (extraObj->getFlag(mm::ExtraObjectData::FLAGS_FINALIZED)) flags << "FINALIZED,";
+            flags << "}";
+
+
+            std::stringstream extraLabel;
+            extraLabel << id(extraObj) << "[EXTRA(" << id(obj) << ")\\nflags: " << flags.str() << "]";
+
+            graph_.node(id(extraObj), extraLabel.str());
+        };
+    }
+
+    void reference(const ObjHeader* from, const ObjHeader* to) {
+        graph_.edge(id(from), id(to));
+    }
+
+private:
+    template <typename T>
+    static std::string id(T* obj) {
+        std::stringstream stream;
+        stream << obj; // TOOD std::hex?
+        return stream.str();
+    }
+
+    static std::string name(const TypeInfo* type) {
+        if (type == nullptr) return "<unknown>";
+        char* cstr = CreateCStringFromString(type->relativeName_);
+        std::string str = cstr;
+        std::free(cstr);
+        return str;
+    }
+
+    Printer printer_;
+    graphviz::Graph<Printer> graph_{printer_, "Heap", true};
+};
+
+}
diff --git a/kotlin-native/runtime/src/gc/pmcs/cpp/ParallelMarkConcurrentSweep.cpp b/kotlin-native/runtime/src/gc/pmcs/cpp/ParallelMarkConcurrentSweep.cpp
index 34b02a4..f66b7e5 100644
--- a/kotlin-native/runtime/src/gc/pmcs/cpp/ParallelMarkConcurrentSweep.cpp
+++ b/kotlin-native/runtime/src/gc/pmcs/cpp/ParallelMarkConcurrentSweep.cpp
@@ -184,6 +184,8 @@
     allocator_.prepareForGC();
 
 #ifndef CUSTOM_ALLOCATOR
+    allocator_.impl().dumpHeap();
+
     // Taking the locks before the pause is completed. So that any destroying thread
     // would not publish into the global state at an unexpected time.
     std::optional objectFactoryIterable = allocator_.impl().objectFactory().LockForIter();
diff --git a/kotlin-native/runtime/src/mm/cpp/Graphviz.hpp b/kotlin-native/runtime/src/mm/cpp/Graphviz.hpp
new file mode 100644
index 0000000..1e5b75b
--- /dev/null
+++ b/kotlin-native/runtime/src/mm/cpp/Graphviz.hpp
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2010-2024 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.
+ */
+
+#pragma once
+
+#include <sstream>
+
+#include "Logging.hpp"
+#include "Utils.hpp"
+
+namespace kotlin::graphviz  {
+
+template <typename Printer>
+class Graph : private Pinned {
+public:
+    Graph(const Printer& printer, std::string_view name, bool directed = true) : printer_(printer), directed_(directed) {
+        auto kind = directed_ ? "digraph" : "graph";
+        std::stringstream s;
+        s << kind << " " << name << " {";
+        printer_.println(s.str().data());
+        incIdent();
+    }
+
+    ~Graph() {
+        printer_.println("}");
+    }
+
+    void node(std::string_view id, std::string_view label) {
+        std::stringstream s;
+        s << ident() << id << "[label=\"" << label << "\"];";
+        printer_.println(s.str().data());
+    }
+
+    void edge(std::string_view fromId, std::string_view toId) {
+        std::stringstream s;
+        auto edgeOp = directed_ ? "->" : "--";
+        s << ident() << fromId  << edgeOp << toId << ";";
+        printer_.println(s.str().data());
+    }
+private:
+    static constexpr int kIdentStep = 4;
+    void incIdent() {
+        ident_ += kIdentStep;
+    }
+    void decIdent() {
+        ident_ -= kIdentStep;
+    }
+    [[nodiscard]] std::string ident() const {
+        return std::string(ident_, ' ');
+    }
+
+    Printer printer_;
+    const bool directed_;
+    int ident_ = 0;
+};
+
+// FIXME move out of graphviz namespace
+template <logging::Tag kTag, logging::Level kLevel>
+struct LogPrinter {
+    void println(const char* str) {
+        RuntimeLog(kLevel, {kTag}, "%s", str);
+    }
+};
+
+}