[K/N] WIP: A PoC of instrumenting allocation profiler
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/BinaryOptions.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/BinaryOptions.kt
index 0a9c535..06dcb70 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/BinaryOptions.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/BinaryOptions.kt
@@ -88,6 +88,9 @@
val swiftExport by booleanOption()
val genericSafeCasts by booleanOption()
+ val eventTrackerFrequency by stringOption()
+
+ val eventTrackerBacktraceDepth by stringOption()
}
open class BinaryOption<T : Any>(
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/EventTrackerKind.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/EventTrackerKind.kt
new file mode 100644
index 0000000..44bb72d
--- /dev/null
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/EventTrackerKind.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2010-2023 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.
+ */
+package org.jetbrains.kotlin.backend.konan
+
+// Must match `EventKind` in profiler/ProfilerEvents.hpp
+enum class EventTrackerKind(val ord: Int, val defaultBacktraceDepth: Int) {
+ Allocation(0, 2),
+ SafePoint(1, 2),
+ SpecialRef(2, 2),
+ ;
+
+ companion object {
+ fun parse(str: String) = entries.firstOrNull {
+ it.name.equals(str, ignoreCase = true)
+ }
+ }
+}
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt
index 453bf02..6f0ea6b 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt
@@ -158,6 +158,54 @@
default + configured
}
+ val eventTrackerConfigured: Boolean by lazy {
+ configuration.get(BinaryOptions.eventTrackerFrequency) != null
+ }
+
+ val eventTrackerFrequency: Map<EventTrackerKind, Int> by lazy {
+ val default = EventTrackerKind.entries.associateWith { 0 }
+
+ val delimiter = "/" // FIXME consider making "," ?
+
+ val parts = configuration.get(BinaryOptions.eventTrackerFrequency)?.split(delimiter)?.toTypedArray()
+ val configured = parseKeyValuePairs(parts, configuration)?.mapNotNull { (key, value) ->
+ val event = EventTrackerKind.parse(key)
+ val frequency = value.toIntOrNull()
+ if (event == null) {
+ configuration.report(CompilerMessageSeverity.STRONG_WARNING, "Unknown event kind '$key'.")
+ null
+ } else if (frequency == null) {
+ configuration.report(CompilerMessageSeverity.STRONG_WARNING, "Invalid frequency '$value'.")
+ null
+ } else {
+ event to frequency
+ }
+ }?.toMap() ?: default
+ default + configured
+ }
+
+ val eventTrackerBacktraceDepth: Map<EventTrackerKind, Int> by lazy {
+ val default = EventTrackerKind.entries.associateWith { it.defaultBacktraceDepth }
+
+ val delimiter = "/" // FIXME consider making "," ?
+
+ val parts = configuration.get(BinaryOptions.eventTrackerBacktraceDepth)?.split(delimiter)?.toTypedArray()
+ val configured = parseKeyValuePairs(parts, configuration)?.mapNotNull { (key, value) ->
+ val event = EventTrackerKind.parse(key)
+ val frequency = value.toIntOrNull()
+ if (event == null) {
+ configuration.report(CompilerMessageSeverity.STRONG_WARNING, "Unknown event kind '$key'")
+ null
+ } else if (frequency == null) {
+ configuration.report(CompilerMessageSeverity.STRONG_WARNING, "Invalid backtrace depth '$value'")
+ null
+ } else {
+ event to frequency
+ }
+ }?.toMap() ?: return@lazy default
+ default + configured
+ }
+
val suspendFunctionsFromAnyThreadFromObjC: Boolean by lazy {
configuration.get(BinaryOptions.objcExportSuspendFunctionLaunchThreadRestriction) !=
@@ -566,6 +614,7 @@
sanitizer != null -> "with sanitizers enabled"
runtimeLogsEnabled -> "with runtime logs"
checkStateAtExternalCalls -> "with external calls state checker"
+ eventTrackerConfigured -> "with event tracker enabled"
else -> null
}
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/SetupConfiguration.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/SetupConfiguration.kt
index 9c8da29..85ea01a 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/SetupConfiguration.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/SetupConfiguration.kt
@@ -520,7 +520,7 @@
configuration: CompilerConfiguration
): Map<String, String>? = parseKeyValuePairs(arguments.overrideKonanProperties, configuration)
-private fun parseKeyValuePairs(
+fun parseKeyValuePairs(
argumentValue: Array<String>?,
configuration: CompilerConfiguration
): Map<String, String>? = argumentValue?.mapNotNull {
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt
index faaeff4..06b7a73 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt
@@ -3017,10 +3017,22 @@
config.runtimeLogs[it]!!.ord.let { llvm.constInt32(it) }
})
setRuntimeConstGlobal("Kotlin_runtimeLogs", runtimeLogs)
+
+ val eventTrackerFrequency = ConstArray(llvm.int32Type, EventTrackerKind.entries.sortedBy { it.ord }.map {
+ context.config.eventTrackerFrequency[it]!!.let { llvm.constInt32(it) }
+ })
+ setRuntimeConstGlobal("Kotlin_eventTrackerFrequency", eventTrackerFrequency)
+
+ val eventTrackerBacktraceDepth = ConstArray(llvm.int32Type, EventTrackerKind.entries.sortedBy { it.ord }.map {
+ context.config.eventTrackerBacktraceDepth[it]!!.let { llvm.constInt32(it) }
+ })
+ setRuntimeConstGlobal("Kotlin_eventTrackerBacktraceDepth", eventTrackerBacktraceDepth)
+
setRuntimeConstGlobal("Kotlin_freezingEnabled", llvm.constInt32(if (config.freezing.enableFreezeAtRuntime) 1 else 0))
setRuntimeConstGlobal("Kotlin_freezingChecksEnabled", llvm.constInt32(if (config.freezing.enableFreezeChecks) 1 else 0))
setRuntimeConstGlobal("Kotlin_concurrentWeakSweep", llvm.constInt32(if (context.config.concurrentWeakSweep) 1 else 0))
setRuntimeConstGlobal("Kotlin_gcMarkSingleThreaded", llvm.constInt32(if (config.gcMarkSingleThreaded) 1 else 0))
+ setRuntimeConstGlobal("Kotlin_gcMarkSingleThreaded", llvm.constInt32(if (config.gcMarkSingleThreaded) 1 else 0))
return llvmModule
}
diff --git a/kotlin-native/runtime/src/main/cpp/CompilerConstants.hpp b/kotlin-native/runtime/src/main/cpp/CompilerConstants.hpp
index 194deb8..98e1fbc 100644
--- a/kotlin-native/runtime/src/main/cpp/CompilerConstants.hpp
+++ b/kotlin-native/runtime/src/main/cpp/CompilerConstants.hpp
@@ -27,6 +27,8 @@
extern "C" const int32_t Kotlin_disableMmap;
extern "C" const int32_t Kotlin_disableAllocatorOverheadEstimate;
extern "C" const int32_t Kotlin_runtimeLogs[];
+extern "C" const int32_t Kotlin_eventTrackerFrequency[];
+extern "C" const int32_t Kotlin_eventTrackerBacktraceDepth[];
extern "C" const int32_t Kotlin_concurrentWeakSweep;
extern "C" const int32_t Kotlin_gcMarkSingleThreaded;
extern "C" const int32_t Kotlin_freezingEnabled;
@@ -94,6 +96,14 @@
return Kotlin_freezingChecksEnabled != 0;
}
+ALWAYS_INLINE inline const int32_t* eventTrackerFrequency() noexcept {
+ return Kotlin_eventTrackerFrequency;
+}
+
+ALWAYS_INLINE inline const int32_t* eventTrackerBacktraceDepth() noexcept {
+ return Kotlin_eventTrackerBacktraceDepth;
+}
+
ALWAYS_INLINE inline bool concurrentWeakSweep() noexcept {
return Kotlin_concurrentWeakSweep != 0;
}
diff --git a/kotlin-native/runtime/src/main/cpp/Interop.cpp b/kotlin-native/runtime/src/main/cpp/Interop.cpp
index f4359a0..1be348c 100644
--- a/kotlin-native/runtime/src/main/cpp/Interop.cpp
+++ b/kotlin-native/runtime/src/main/cpp/Interop.cpp
@@ -28,7 +28,7 @@
extern "C" {
-KNativePtr Kotlin_Interop_createStablePointer(KRef any) {
+ALWAYS_INLINE KNativePtr Kotlin_Interop_createStablePointer(KRef any) {
KRefSharedHolder* holder = new KRefSharedHolder();
holder->init(any);
return holder;
diff --git a/kotlin-native/runtime/src/main/cpp/KString.cpp b/kotlin-native/runtime/src/main/cpp/KString.cpp
index 6ca13a3..7deee4b 100644
--- a/kotlin-native/runtime/src/main/cpp/KString.cpp
+++ b/kotlin-native/runtime/src/main/cpp/KString.cpp
@@ -479,3 +479,14 @@
} // extern "C"
+
+std::string kotlin::to_string(KConstRef kref) {
+ if (kref == nullptr) return "null";
+ RuntimeAssert(kref->type_info() == theStringTypeInfo, "A Kotlin String expected");
+ KString kstring = kref->array();
+ const KChar* utf16 = CharArrayAddressOfElementAt(kstring, 0);
+ std::string utf8;
+ utf8.reserve(kstring->count_);
+ utf8::unchecked::utf16to8(utf16, utf16 + kstring->count_, back_inserter(utf8));
+ return utf8;
+}
diff --git a/kotlin-native/runtime/src/main/cpp/KString.h b/kotlin-native/runtime/src/main/cpp/KString.h
index 17b0573..a10ee31 100644
--- a/kotlin-native/runtime/src/main/cpp/KString.h
+++ b/kotlin-native/runtime/src/main/cpp/KString.h
@@ -37,6 +37,7 @@
#ifdef __cplusplus
}
+
#endif
template <typename T>
@@ -58,4 +59,9 @@
return middle - (needle < value ? 1 : 0);
}
+
+namespace kotlin {
+std::string to_string(KConstRef kref);
+}
+
#endif // RUNTIME_KSTRING_H
diff --git a/kotlin-native/runtime/src/main/cpp/Runtime.cpp b/kotlin-native/runtime/src/main/cpp/Runtime.cpp
index c90cc6b..2fc0e83 100644
--- a/kotlin-native/runtime/src/main/cpp/Runtime.cpp
+++ b/kotlin-native/runtime/src/main/cpp/Runtime.cpp
@@ -148,6 +148,7 @@
delete state;
WorkerDestroyThreadDataIfNeeded(workerId);
::runtimeState = kInvalidRuntime;
+ RuntimeLogError({ logging::Tag::kLogging }, "Deinit runtime");
}
void Kotlin_deinitRuntimeCallback(void* argument) {
@@ -216,6 +217,7 @@
needsFullShutdown = Kotlin_forceCheckedShutdown() || Kotlin_memoryLeakCheckerEnabled() || Kotlin_cleanersLeakCheckerEnabled();
break;
}
+ needsFullShutdown = true; // FIXME
if (!needsFullShutdown) {
auto lastStatus = std_support::atomic_compare_swap_strong(globalRuntimeStatus, kGlobalRuntimeRunning, kGlobalRuntimeShutdown);
RuntimeAssert(lastStatus == kGlobalRuntimeRunning, "Invalid runtime status for shutdown");
@@ -269,6 +271,7 @@
}
deinitRuntime(runtime, canDestroyRuntime);
+ RuntimeLogError({ logging::Tag::kLogging }, "Shutdown runtime");
}
KInt Konan_Platform_canAccessUnaligned() {
diff --git a/kotlin-native/runtime/src/main/cpp/Utils.hpp b/kotlin-native/runtime/src/main/cpp/Utils.hpp
index 7ace717..a5f5a68 100644
--- a/kotlin-native/runtime/src/main/cpp/Utils.hpp
+++ b/kotlin-native/runtime/src/main/cpp/Utils.hpp
@@ -98,6 +98,11 @@
size_t CombineHash(size_t seed, size_t value);
+template<typename T, typename Hasher = std::hash<T>>
+std::size_t hashOf(const T& x) {
+ return Hasher{}(x);
+}
+
#define ownerOf(type, field, ref) *reinterpret_cast<type*>(reinterpret_cast<char*>(&ref) - offsetof(type, field))
// Returns `true` if the entire `span` is zeroed.
diff --git a/kotlin-native/runtime/src/main/cpp/profiler/IterableHelpers.hpp b/kotlin-native/runtime/src/main/cpp/profiler/IterableHelpers.hpp
new file mode 100644
index 0000000..b216fcc
--- /dev/null
+++ b/kotlin-native/runtime/src/main/cpp/profiler/IterableHelpers.hpp
@@ -0,0 +1,81 @@
+/*
+* 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 <unordered_map>
+#include <unordered_set>
+#include <sstream>
+
+namespace kotlin {
+
+template<typename Iterable, typename KeySelector, typename ValueTransformer>
+auto groupBy(const Iterable& iterable, KeySelector&& selectKey, ValueTransformer&& transformValue) {
+ using OriginalValue = typename Iterable::value_type;
+ using Key = typename std::result_of<KeySelector(const OriginalValue&)>::type;
+ using TransformedValue = typename std::result_of<ValueTransformer(const OriginalValue&)>::type;
+ std::unordered_map<Key, std::vector<TransformedValue>> groups;
+ for (const auto& value: iterable) {
+ auto key = selectKey(value);
+ groups[key].push_back(transformValue(value));
+ }
+ return groups;
+}
+
+template<typename Iterable, typename KeySelector>
+auto groupBy(const Iterable& iterable, KeySelector&& selectKey) {
+ return groupBy(iterable, std::forward<KeySelector>(selectKey), [](const auto& x) { return x; });
+}
+
+template<typename Iterable, typename Associator>
+auto associateWith(const Iterable& iterable, Associator&& assotiation) {
+ using OriginalValue = typename Iterable::value_type;
+ using AssociatedValue = typename std::result_of<Associator(const OriginalValue&)>::type;
+ std::unordered_map<OriginalValue, AssociatedValue> associations;
+ for (const auto& value: iterable) {
+ associations.insert_or_assign(value, assotiation(value));
+ }
+ return associations;
+}
+
+template<typename Iterable, typename Filter>
+auto filterToVector(const Iterable& iterable, Filter&& filter) {
+ using OriginalValue = typename Iterable::value_type;
+ std::vector<OriginalValue> filtered;
+ for (const auto& value: iterable) {
+ if (filter(value)) {
+ filtered.push_back(value);
+ }
+ }
+ return filtered;
+}
+
+template<typename Iterable, typename Map>
+auto mapToVector(const Iterable& iterable, Map&& map) {
+ using OriginalValue = typename Iterable::value_type;
+ using MappedValue = typename std::result_of<Map(const OriginalValue&)>::type;
+ std::vector<MappedValue> mapped;
+ for (const auto& value: iterable) {
+ mapped.push_back(map(value));
+ }
+ return mapped;
+}
+
+template<typename T, typename Ord>
+void sortBy(std::vector<T>& vector, Ord&& ord, bool descending = false) {
+ std::sort(vector.begin(), vector.end(), [ord = std::forward<Ord>(ord), descending](const T& l, const T& r) {
+ auto ordL = ord(l);
+ auto ordR = ord(r);
+ return descending ? ordL > ordR : ordL < ordR;
+ });
+}
+
+template<typename Iterable>
+auto uniq(const Iterable& iterable) {
+ std::unordered_set<typename Iterable::value_type> uniqSet(iterable.begin(), iterable.end());
+ return Iterable(uniqSet.begin(), uniqSet.end());
+}
+
+}
diff --git a/kotlin-native/runtime/src/main/cpp/profiler/ProfileReporter.hpp b/kotlin-native/runtime/src/main/cpp/profiler/ProfileReporter.hpp
new file mode 100644
index 0000000..10aee88
--- /dev/null
+++ b/kotlin-native/runtime/src/main/cpp/profiler/ProfileReporter.hpp
@@ -0,0 +1,140 @@
+/*
+* 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 "Profiler.hpp"
+
+#include "Porting.h"
+#include "StackTrace.hpp"
+
+namespace kotlin::profiler {
+
+template<typename>
+class Profiler;
+
+namespace internal {
+
+template <typename EventTraits>
+class ProfileReporter {
+public:
+ using EventRecord = typename Profiler<EventTraits>::EventRecord;
+
+ explicit ProfileReporter(const Profiler<EventTraits>& profiler) : profiler_(profiler), entryPrinter_(*this) {}
+
+ void report() {
+ {
+ std::stringstream buf;
+ buf << EventTraits::kName << "s sampled with the period of " << EventTraits::samplingFrequencyPeriod()
+ << " and the backtrace depth of " << EventTraits::backtraceDepth()
+ << ".\n";
+ buf << "Total events occurred " << profiler_.totalEventCount_
+ << ", of them recorded " << profiler_.recordedEventCount_
+ << ".\n";
+ auto str = buf.str();
+ konan::consoleErrorUtf8(str.data(), str.size());
+ }
+
+ auto records = mapToVector(profiler_.eventCount_, [](const auto& p) { return p.first; });
+ printRecordEventsOnLevel(records);
+
+ konan::consoleErrorUtf8("\n", 1);
+ }
+
+private:
+ using Event = typename EventRecord::Event;
+ using ContextEntry = typename EventRecord::ContextEntry; // FIXME RecordEntry?
+
+ void printRecordEventsOnLevel(const std::vector<EventRecord>& records, std::size_t level = 0) {
+ auto entriesOnLevel = uniq(mapToVector(records, [&](const auto& r) { return r.entries()[level]; }));
+ auto recordsOfEntry = groupBy(records,
+ [&](const auto& r) { return r.entries()[level]; },
+ [](const auto& r) { return r; });
+
+ auto accumulatedHitsForEntry = associateWith(entriesOnLevel, [&](const auto& entry) {
+ auto& entryRecords = recordsOfEntry[entry];
+ auto hitCounts = mapToVector(entryRecords, [&](const auto& record) { return profiler_.eventCount_.at(record); });
+ return std::accumulate(hitCounts.begin(), hitCounts.end(), std::size_t(0));
+ });
+
+ sortBy(entriesOnLevel, [&](const auto& e) -> std::size_t { return accumulatedHitsForEntry.at(e); }, true);
+
+ for (auto& entry: entriesOnLevel) {
+ printWithSubentries(entry, recordsOfEntry, accumulatedHitsForEntry.at(entry), level);
+ }
+ }
+
+ void printWithSubentries( const ContextEntry& entry,
+ const std::unordered_map<ContextEntry, std::vector<EventRecord>>& recordsOfEntry,
+ std::size_t accumulatedHits, size_t level) {
+
+ entryPrinter_.print(accumulatedHits, entry);
+
+ const auto& subRecords = recordsOfEntry.at(entry);
+ if (level < subRecords[0].entries().size() - 1) {
+ entryPrinter_.incIdent();
+ printRecordEventsOnLevel(subRecords, level + 1);
+ entryPrinter_.decIdent();
+ }
+ }
+
+ class IndentedEntryPrinter {
+ static constexpr auto kIdentStep = 2;
+ public:
+ explicit IndentedEntryPrinter(ProfileReporter& reporter) : reporter_(reporter) {}
+
+ void incIdent() { baseIdent_ += kIdentStep; }
+ void decIdent() { baseIdent_ -= kIdentStep; }
+
+ void print(std::size_t hits, const ContextEntry& entry) {
+ auto buf = std::stringstream{};
+ auto fraction = static_cast<double>(hits) / reporter_.profiler_.recordedEventCount_;
+ buf << "[" << hits << "] (" << fraction * 100 << "%)";
+ auto hitsStr = buf.str();
+
+ buf = std::stringstream{};
+
+ appendSpaces(buf, baseIdent_);
+ buf << hitsStr << " ";
+
+ if (std::holds_alternative<Event>(entry)) {
+ buf << std::get<Event>(entry).toString();
+ } else {
+ std::array backtraceFrames = {std::get<void*>(entry)};
+ auto backtraceLines = GetStackTraceStrings(std_support::span(backtraceFrames));
+ for (std::size_t i = 0; i < backtraceLines.size(); ++i) {
+ if (i > 0) {
+ buf << "\n";
+ appendSpaces(buf, baseIdent_ + hitsStr.size() + 1);
+ }
+ buf << backtraceLines[i];
+ }
+ }
+
+ buf << "\n";
+ auto str = buf.str();
+ konan::consoleErrorUtf8(str.data(), str.size());
+ }
+
+ private:
+ void appendSpaces(std::stringstream& buf, std::size_t count) {
+ for (std::size_t i = 0; i < count; ++i) {
+ buf << " ";
+ }
+ }
+
+ ProfileReporter& reporter_;
+ std::size_t baseIdent_ = 0;
+ };
+
+
+ // FIXME padding
+
+ const Profiler<EventTraits>& profiler_;
+ IndentedEntryPrinter entryPrinter_;
+};
+
+}
+}
diff --git a/kotlin-native/runtime/src/main/cpp/profiler/Profiler.hpp b/kotlin-native/runtime/src/main/cpp/profiler/Profiler.hpp
new file mode 100644
index 0000000..a94f2e6
--- /dev/null
+++ b/kotlin-native/runtime/src/main/cpp/profiler/Profiler.hpp
@@ -0,0 +1,193 @@
+/*
+* 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 <unordered_map>
+#include <unordered_set>
+#include <sstream>
+#include <numeric>
+#include <variant>
+
+#include "IterableHelpers.hpp"
+#include "ProfileReporter.hpp"
+#include "ProfilerEvents.hpp"
+#include "Porting.h"
+#include "Utils.hpp"
+#include "StackTrace.hpp"
+#include "Logging.hpp"
+#include "KString.h"
+
+namespace kotlin::profiler::internal {
+
+template <typename EventTraits>
+struct EventRecord {
+ using Event = typename EventTraits::Event;
+ using Backtrace = StackTrace<>;
+
+ EventRecord(const Event& event, const Backtrace& backtrace) : event_(event), backtrace_(backtrace) {}
+
+ // Either the event itself or a backtrace frame
+ using ContextEntry = std::variant<Event, void*>;
+
+ auto operator==(const EventRecord& other) const noexcept {
+ return event_ == other.event_ && backtrace_ == other.backtrace_;
+ }
+ auto operator!=(const EventRecord& other) const noexcept { return !operator==(other); }
+
+ [[nodiscard]] auto entries() const noexcept {
+ std::vector<ContextEntry> elems;
+ elems.push_back(event_);
+ elems.insert(elems.end(), backtrace_.begin(), backtrace_.end());
+ return elems;
+ }
+
+ auto hash() const noexcept {
+ return CombineHash(hashOf(event_), hashOf(backtrace_));
+ }
+
+private:
+ Event event_;
+ Backtrace backtrace_;
+};
+
+} // namespace kotlin::profiler::internal
+
+template <typename EventTraits>
+struct std::hash<kotlin::profiler::internal::EventRecord<EventTraits>> {
+ std::size_t operator()(const kotlin::profiler::internal::EventRecord<EventTraits>& x) const noexcept {
+ return x.hash();
+ }
+};
+
+namespace kotlin::profiler {
+
+template <typename EventTraits>
+class Profiler : Pinned {
+ friend internal::ProfileReporter<EventTraits>;
+public:
+ using Event = typename EventTraits::Event;
+ using EventRecord = typename internal::EventRecord<EventTraits>;
+ using EventCounts = typename std::unordered_map<EventRecord, std::size_t>;
+
+ class ThreadData : Pinned {
+ friend Profiler;
+ public:
+ explicit ThreadData(Profiler& profiler) : profiler_(profiler) {}
+
+ ~ThreadData() {
+ publish();
+ }
+
+ static auto enabled() noexcept { return EventTraits::enabled(); }
+
+ NO_INLINE void hitImpl(Event event, std::size_t skipFrames = 0) {
+ RuntimeAssert(enabled(), "Registered hit with a disabled profiler");
+
+ if (totalEventCount_++ % EventTraits::samplingFrequencyPeriod() == 0) {
+ // FIXME make it collect exactly the requested amount of frames
+ EventRecord record{event, EventRecord::Backtrace::current(skipFrames + 1, EventTraits::backtraceDepth())};
+ ++eventCount_[record];
+ }
+ }
+
+ void publish() {
+ if (enabled()) {
+ profiler_.aggregate(*this);
+ eventCount_.clear();
+ totalEventCount_ = 0;
+ }
+ }
+
+ private:
+ Profiler& profiler_;
+ std::size_t totalEventCount_ = 0;
+ EventCounts eventCount_{};
+ };
+
+ ~Profiler() {
+ report();
+ }
+
+ void aggregate(const ThreadData& threadData) {
+ std::unique_lock lockGuard(aggregationMutex_);
+ totalEventCount_ += threadData.totalEventCount_;
+ for (const auto& [key, value] : threadData.eventCount_) {
+ eventCount_[key] += value;
+ recordedEventCount_ += value;
+ }
+ }
+
+ void report() const noexcept {
+ if (EventTraits::enabled()) {
+ std::unique_lock lockGuard(aggregationMutex_);
+
+ auto reporter = internal::ProfileReporter<EventTraits>{*this};
+ reporter.report();
+ }
+ }
+
+private:
+ std::size_t totalEventCount_ = 0;
+ std::size_t recordedEventCount_ = 0;
+ EventCounts eventCount_{};
+ mutable std::mutex aggregationMutex_;
+};
+
+
+class Profilers {
+public:
+ class ThreadData {
+ public:
+ explicit ThreadData(Profilers& profilers)
+ : allocationProfilerData_(profilers.allocationProfiler_)
+ , safePointProfilerData_(profilers.safePointProfiler_)
+ , specialRefProfilerData_(profilers.specialRefProfiler_)
+ {}
+
+ auto& allocation() noexcept { return allocationProfilerData_; }
+ auto& safePoint() noexcept { return safePointProfilerData_; }
+ auto& specialRef() noexcept { return specialRefProfilerData_; }
+
+ void publish() {
+ allocationProfilerData_.publish();
+ safePointProfilerData_.publish();
+ specialRefProfilerData_.publish();
+ }
+ private:
+ Profiler<AllocationEventTraits>::ThreadData allocationProfilerData_;
+ Profiler<SafePointEventTraits>::ThreadData safePointProfilerData_;
+ Profiler<SpecialRefEventTraits>::ThreadData specialRefProfilerData_;
+ };
+
+ void report() {
+ allocationProfiler_.report();
+ safePointProfiler_.report();
+ specialRefProfiler_.report();
+ }
+private:
+ Profiler<AllocationEventTraits> allocationProfiler_{};
+ Profiler<SafePointEventTraits> safePointProfiler_{};
+ Profiler<SpecialRefEventTraits> specialRefProfiler_{};
+};
+
+} // namespace kotlin::profiler
+
+#define ProfilerHit0(EventTraits, profilerThreadData, event, ...) \
+ do { \
+ if (EventTraits::enabled()) { \
+ using TDType = typename kotlin::profiler::Profiler<EventTraits>::ThreadData; \
+ static_assert(std::is_same<std::decay<decltype(profilerThreadData)>::type, TDType>::value); \
+ profilerThreadData.hitImpl(event, ##__VA_ARGS__); \
+ } \
+ } while (false)
+
+#define ProfilerHit(profilerThreadData, event, ...) \
+ do { \
+ using ThreadDataType = std::decay<decltype(profilerThreadData)>::type; \
+ if (ThreadDataType::enabled()) { \
+ profilerThreadData.hitImpl(event, ##__VA_ARGS__); \
+ } \
+ } while (false)
diff --git a/kotlin-native/runtime/src/main/cpp/profiler/ProfilerEvents.hpp b/kotlin-native/runtime/src/main/cpp/profiler/ProfilerEvents.hpp
new file mode 100644
index 0000000..d77a62a
--- /dev/null
+++ b/kotlin-native/runtime/src/main/cpp/profiler/ProfilerEvents.hpp
@@ -0,0 +1,126 @@
+/*
+* 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 "KString.h"
+
+namespace kotlin::profiler {
+
+enum class EventKind : int32_t {
+ kAllocation = 0,
+ kSafePoint = 1,
+ kSpecialRef = 2,
+};
+
+namespace internal {
+template<EventKind kEventKind>
+class EventTraits {
+public:
+ ALWAYS_INLINE static bool enabled() noexcept {
+ return samplingFrequencyPeriod() != 0;
+ }
+
+ ALWAYS_INLINE static std::size_t samplingFrequencyPeriod() noexcept {
+ return compiler::eventTrackerFrequency()[static_cast<std::size_t>(kEventKind)];
+ }
+
+ ALWAYS_INLINE static std::size_t backtraceDepth() {
+ return compiler::eventTrackerBacktraceDepth()[static_cast<std::size_t>(kEventKind)];
+ }
+};
+}
+
+class AllocationEventTraits : public internal::EventTraits<EventKind::kAllocation> {
+public:
+ static constexpr auto kName = "Allocation";
+
+ struct AllocationRecord {
+ auto operator==(const AllocationRecord& other) const noexcept {
+ return typeInfo_ == other.typeInfo_ && arrayLength_ == other.arrayLength_;
+ }
+ auto operator!=(const AllocationRecord& other) const noexcept { return !operator==(other); }
+
+ auto toString() const -> std::string {
+ auto pkg = to_string(typeInfo_->packageName_);
+ auto cls = to_string(typeInfo_->relativeName_);
+ auto fqName = pkg.empty() ? cls : pkg + "." + cls;
+ if (typeInfo_->IsArray()) {
+ return fqName + "[" + std::to_string(arrayLength_) +"]";
+ }
+ return fqName;
+ }
+
+ const TypeInfo* typeInfo_;
+ std::size_t arrayLength_ = 0;
+ };
+
+ using Event = AllocationRecord;
+};
+
+class SafePointEventTraits : public internal::EventTraits<EventKind::kSafePoint> {
+public:
+ static constexpr auto kName = "SafePoint";
+
+ struct SafePointHit {
+ auto operator==(const SafePointHit&) const noexcept { return true; }
+ auto operator!=(const SafePointHit&) const noexcept { return false; }
+
+ auto toString() const -> std::string {
+ return "Safe point";
+ }
+ };
+
+ using Event = SafePointHit;
+};
+
+class SpecialRefEventTraits : public internal::EventTraits<EventKind::kSpecialRef> {
+public:
+ static constexpr auto kName = "SpecialRef";
+
+ enum class SpecialRefKind {
+ kStableRef, kWeakRef, kBackRef
+ };
+
+ struct SpecialRefCreation {
+ auto operator==(const SpecialRefCreation& other) const noexcept { return kind_ == other.kind_; }
+ auto operator!=(const SpecialRefCreation& other) const noexcept { return !operator==(other); }
+
+ auto toString() const -> std::string {
+ switch (kind_) {
+ case SpecialRefKind::kStableRef: return "StableRef";
+ case SpecialRefKind::kWeakRef: return "WeakRef";
+ case SpecialRefKind::kBackRef: return "BackRef";
+ }
+ }
+
+ SpecialRefKind kind_;
+ };
+
+ using Event = SpecialRefCreation;
+};
+
+}
+
+template<>
+struct std::hash<kotlin::profiler::AllocationEventTraits::Event> {
+ std::size_t operator()(const kotlin::profiler::AllocationEventTraits::Event& alloc) const noexcept {
+ return kotlin::CombineHash(kotlin::hashOf(alloc.typeInfo_), kotlin::hashOf(alloc.arrayLength_));
+ }
+};
+
+template<>
+struct std::hash<kotlin::profiler::SafePointEventTraits::Event> {
+ std::size_t operator()(const kotlin::profiler::SafePointEventTraits::Event&) const noexcept {
+ return 0;
+ }
+};
+
+template<>
+struct std::hash<kotlin::profiler::SpecialRefEventTraits::Event> {
+ std::size_t operator()(const kotlin::profiler::SpecialRefEventTraits::Event& e) const noexcept {
+ return static_cast<std::size_t>(e.kind_);
+ }
+};
diff --git a/kotlin-native/runtime/src/mm/cpp/GlobalData.hpp b/kotlin-native/runtime/src/mm/cpp/GlobalData.hpp
index 87bcc9e..840f37d 100644
--- a/kotlin-native/runtime/src/mm/cpp/GlobalData.hpp
+++ b/kotlin-native/runtime/src/mm/cpp/GlobalData.hpp
@@ -15,6 +15,7 @@
#include "SpecialRefRegistry.hpp"
#include "ThreadRegistry.hpp"
#include "Utils.hpp"
+#include "profiler/Profiler.hpp"
namespace kotlin {
namespace mm {
@@ -35,6 +36,7 @@
alloc::Allocator& allocator() noexcept { return allocator_; }
gc::GC& gc() noexcept { return gc_; }
AppStateTracking& appStateTracking() noexcept { return appStateTracking_; }
+ profiler::Profilers& profilers() noexcept { return profilers_; }
private:
friend class ManuallyScoped<GlobalData>;
@@ -49,6 +51,7 @@
gcScheduler::GCScheduler gcScheduler_;
alloc::Allocator allocator_;
gc::GC gc_{allocator_, gcScheduler_};
+ profiler::Profilers profilers_;
};
} // namespace mm
diff --git a/kotlin-native/runtime/src/mm/cpp/Memory.cpp b/kotlin-native/runtime/src/mm/cpp/Memory.cpp
index d95cfdd..210c9a0 100644
--- a/kotlin-native/runtime/src/mm/cpp/Memory.cpp
+++ b/kotlin-native/runtime/src/mm/cpp/Memory.cpp
@@ -112,11 +112,13 @@
// the thread registery and waits for threads to suspend or go to the native state.
AssertThreadState(state, ThreadState::kNative);
auto* node = mm::FromMemoryState(state);
+ node->Get()->profilers().publish();
if (destroyRuntime) {
ThreadStateGuard guard(state, ThreadState::kRunnable);
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
// TODO: Why not just destruct `GC` object and its thread data counterpart entirely?
mm::GlobalData::Instance().gc().StopFinalizerThreadIfRunning();
+ mm::GlobalData::Instance().profilers().report();
}
if (!konan::isOnThreadExitNotSetOrAlreadyStarted()) {
// we can clear reference in advance, as Unregister function can't use it anyway
@@ -135,6 +137,7 @@
extern "C" ALWAYS_INLINE RUNTIME_NOTHROW OBJ_GETTER(AllocInstance, const TypeInfo* typeInfo) {
auto* threadData = mm::ThreadRegistry::Instance().CurrentThreadData();
+ ProfilerHit(threadData->profilers().allocation(), {typeInfo});
RETURN_RESULT_OF(mm::AllocateObject, threadData, typeInfo);
}
@@ -143,6 +146,7 @@
ThrowIllegalArgumentException();
}
auto* threadData = mm::ThreadRegistry::Instance().CurrentThreadData();
+ ProfilerHit(threadData->profilers().allocation(), {typeInfo, static_cast<size_t>(elements)});
RETURN_RESULT_OF(mm::AllocateArray, threadData, typeInfo, static_cast<uint32_t>(elements));
}
@@ -500,7 +504,7 @@
return true;
}
-extern "C" RUNTIME_NOTHROW void* CreateStablePointer(ObjHeader* object) {
+extern "C" ALWAYS_INLINE RUNTIME_NOTHROW void* CreateStablePointer(ObjHeader* object) {
if (!object)
return nullptr;
diff --git a/kotlin-native/runtime/src/mm/cpp/MemorySharedRefs.cpp b/kotlin-native/runtime/src/mm/cpp/MemorySharedRefs.cpp
index e5aa66b..a1b477e 100644
--- a/kotlin-native/runtime/src/mm/cpp/MemorySharedRefs.cpp
+++ b/kotlin-native/runtime/src/mm/cpp/MemorySharedRefs.cpp
@@ -19,7 +19,7 @@
obj_ = obj;
}
-void KRefSharedHolder::init(ObjHeader* obj) {
+ALWAYS_INLINE void KRefSharedHolder::init(ObjHeader* obj) {
RuntimeAssert(obj != nullptr, "must not be null");
ref_ = static_cast<mm::RawSpecialRef*>(mm::StableRef::create(obj));
obj_ = obj;
diff --git a/kotlin-native/runtime/src/mm/cpp/SafePoint.cpp b/kotlin-native/runtime/src/mm/cpp/SafePoint.cpp
index a7d5774..4eecf9e 100644
--- a/kotlin-native/runtime/src/mm/cpp/SafePoint.cpp
+++ b/kotlin-native/runtime/src/mm/cpp/SafePoint.cpp
@@ -127,6 +127,7 @@
ALWAYS_INLINE void mm::safePoint(std::memory_order fastPathOrder) noexcept {
AssertThreadState(ThreadState::kRunnable);
+ ProfilerHit(mm::ThreadRegistry::Instance().CurrentThreadData()->profilers().safePoint(), {});
auto action = safePointAction.load(fastPathOrder);
if (__builtin_expect(action != nullptr, false)) {
slowPath();
@@ -135,6 +136,7 @@
ALWAYS_INLINE void mm::safePoint(mm::ThreadData& threadData, std::memory_order fastPathOrder) noexcept {
AssertThreadState(&threadData, ThreadState::kRunnable);
+ ProfilerHit(threadData.profilers().safePoint(), {});
auto action = safePointAction.load(fastPathOrder);
if (__builtin_expect(action != nullptr, false)) {
slowPath(threadData);
diff --git a/kotlin-native/runtime/src/mm/cpp/SpecialRefRegistry.cpp b/kotlin-native/runtime/src/mm/cpp/SpecialRefRegistry.cpp
index 3296b90..d5445c4 100644
--- a/kotlin-native/runtime/src/mm/cpp/SpecialRefRegistry.cpp
+++ b/kotlin-native/runtime/src/mm/cpp/SpecialRefRegistry.cpp
@@ -8,6 +8,7 @@
#include "GlobalData.hpp"
#include "MemoryPrivate.hpp"
#include "ObjCBackRef.hpp"
+#include "profiler/Profiler.hpp"
#include "StableRef.hpp"
#include "ThreadData.hpp"
#include "ThreadState.hpp"
@@ -15,15 +16,18 @@
using namespace kotlin;
-mm::StableRef mm::SpecialRefRegistry::ThreadQueue::createStableRef(ObjHeader* object) noexcept {
+ALWAYS_INLINE mm::StableRef mm::SpecialRefRegistry::ThreadQueue::createStableRef(ObjHeader* object) noexcept {
+ ProfilerHit(mm::ThreadRegistry::Instance().CurrentThreadData()->profilers().specialRef(), {profiler::SpecialRefEventTraits::SpecialRefKind::kStableRef});
return mm::StableRef(registerNode(object, 1, true).asRaw());
}
-mm::WeakRef mm::SpecialRefRegistry::ThreadQueue::createWeakRef(ObjHeader* object) noexcept {
+ALWAYS_INLINE mm::WeakRef mm::SpecialRefRegistry::ThreadQueue::createWeakRef(ObjHeader* object) noexcept {
+ ProfilerHit(mm::ThreadRegistry::Instance().CurrentThreadData()->profilers().specialRef(), {profiler::SpecialRefEventTraits::SpecialRefKind::kWeakRef});
return mm::WeakRef(registerNode(object, 0, false).asRaw());
}
-mm::ObjCBackRef mm::SpecialRefRegistry::ThreadQueue::createObjCBackRef(ObjHeader* object) noexcept {
+ALWAYS_INLINE mm::ObjCBackRef mm::SpecialRefRegistry::ThreadQueue::createObjCBackRef(ObjHeader* object) noexcept {
+ ProfilerHit(mm::ThreadRegistry::Instance().CurrentThreadData()->profilers().specialRef(), {profiler::SpecialRefEventTraits::SpecialRefKind::kBackRef});
return mm::ObjCBackRef(registerNode(object, 1, false).asRaw());
}
diff --git a/kotlin-native/runtime/src/mm/cpp/StableRef.cpp b/kotlin-native/runtime/src/mm/cpp/StableRef.cpp
index 593dc91..8230724 100644
--- a/kotlin-native/runtime/src/mm/cpp/StableRef.cpp
+++ b/kotlin-native/runtime/src/mm/cpp/StableRef.cpp
@@ -13,7 +13,7 @@
using namespace kotlin;
// static
-mm::StableRef mm::StableRef::create(ObjHeader* obj) noexcept {
+ALWAYS_INLINE mm::StableRef mm::StableRef::create(ObjHeader* obj) noexcept {
RuntimeAssert(obj != nullptr, "Creating StableRef for null object");
return mm::ThreadRegistry::Instance().CurrentThreadData()->specialRefRegistry().createStableRef(obj);
}
diff --git a/kotlin-native/runtime/src/mm/cpp/ThreadData.hpp b/kotlin-native/runtime/src/mm/cpp/ThreadData.hpp
index 3dee380..06356b8 100644
--- a/kotlin-native/runtime/src/mm/cpp/ThreadData.hpp
+++ b/kotlin-native/runtime/src/mm/cpp/ThreadData.hpp
@@ -34,9 +34,13 @@
gcScheduler_(GlobalData::Instance().gcScheduler(), *this),
allocator_(GlobalData::Instance().allocator()),
gc_(GlobalData::Instance().gc(), *this),
- suspensionData_(ThreadState::kNative, *this) {}
+ suspensionData_(ThreadState::kNative, *this),
+ profilers_(GlobalData::Instance().profilers())
+ {}
- ~ThreadData() = default;
+ ~ThreadData() {
+ RuntimeLogError({ logging::Tag::kLogging }, "Thread deinit");
+ }
int threadId() const noexcept { return threadId_; }
@@ -62,6 +66,8 @@
ThreadSuspensionData& suspensionData() { return suspensionData_; }
+ auto& profilers() { return profilers_; }
+
void Publish() noexcept {
// TODO: These use separate locks, which is inefficient.
globalsThreadQueue_.Publish();
@@ -85,6 +91,7 @@
gc::GC::ThreadData gc_;
std::vector<std::pair<ObjHeader**, ObjHeader*>> initializingSingletons_;
ThreadSuspensionData suspensionData_;
+ profiler::Profilers::ThreadData profilers_;
};
} // namespace mm