Collect stacktraces without allocating kotlin objects.

This commit is contained in:
Alexander Shabalin
2021-07-12 08:49:33 +00:00
committed by Space
parent fca89c2118
commit 274dabb61d
4 changed files with 145 additions and 85 deletions
+31 -1
View File
@@ -22,6 +22,7 @@
#include <type_traits> #include <type_traits>
#include "KAssert.h" #include "KAssert.h"
#include "KString.h"
#include "StackTrace.hpp" #include "StackTrace.hpp"
#include "Memory.h" #include "Memory.h"
#include "Natives.h" #include "Natives.h"
@@ -40,8 +41,37 @@ KInt Kotlin_Any_hashCode(KConstRef thiz) {
return reinterpret_cast<uintptr_t>(thiz); return reinterpret_cast<uintptr_t>(thiz);
} }
NO_INLINE OBJ_GETTER0(Kotlin_getCurrentStackTrace) {
KStdVector<void*> stackTrace;
{
// Don't use `kotlin::CallWithThreadState` to avoid messing up callstack.
kotlin::ThreadStateGuard guard(kotlin::ThreadState::kNative);
// Skip this function and primary `Throwable` constructor.
stackTrace = kotlin::GetCurrentStackTrace(2);
}
ObjHolder resultHolder;
ObjHeader* result = AllocArrayInstance(theNativePtrArrayTypeInfo, stackTrace.size(), resultHolder.slot());
for (size_t index = 0; index < stackTrace.size(); ++index) {
Kotlin_NativePtrArray_set(result, index, stackTrace[index]);
}
RETURN_OBJ(result);
}
OBJ_GETTER(Kotlin_getStackTraceStrings, KConstRef stackTrace) { OBJ_GETTER(Kotlin_getStackTraceStrings, KConstRef stackTrace) {
RETURN_RESULT_OF(kotlin::GetStackTraceStrings, stackTrace); const KNativePtr* array = PrimitiveArrayAddressOfElementAt<KNativePtr>(stackTrace->array(), 0);
size_t size = stackTrace->array()->count_;
auto stackTraceStrings = kotlin::CallWithThreadState<kotlin::ThreadState::kNative>(kotlin::GetStackTraceStrings, array, size);
ObjHolder resultHolder;
ObjHeader* strings = AllocArrayInstance(theArrayTypeInfo, stackTraceStrings.size(), resultHolder.slot());
for (size_t index = 0; index < stackTraceStrings.size(); ++index) {
ObjHolder holder;
CreateStringFromCString(stackTraceStrings[index].c_str(), holder.slot());
UpdateHeapRef(ArrayAddressOfElementAt(strings->array(), index), holder.obj());
}
RETURN_OBJ(strings);
} }
// TODO: consider handling it with compiler magic instead. // TODO: consider handling it with compiler magic instead.
@@ -17,10 +17,9 @@
#include "Common.h" #include "Common.h"
#include "ExecFormat.h" #include "ExecFormat.h"
#include "Memory.h" #include "Porting.h"
#include "KString.h"
#include "Natives.h"
#include "SourceInfo.h" #include "SourceInfo.h"
#include "Types.h"
#include "utf8.h" #include "utf8.h"
@@ -30,23 +29,18 @@ namespace {
#if USE_GCC_UNWIND #if USE_GCC_UNWIND
struct Backtrace { struct Backtrace {
Backtrace(int count, int skip) : index(0), skipCount(skip) { Backtrace(int count, int skip) : skipCount(skip) {
uint32_t size = count - skipCount; uint32_t size = count - skipCount;
if (size < 0) { if (size < 0) {
size = 0; size = 0;
} }
auto result = AllocArrayInstance(theNativePtrArrayTypeInfo, size, arrayHolder.slot()); array.reserve(size);
// TODO: throw cached OOME?
RuntimeCheck(result != nullptr, "Cannot create backtrace array");
} }
void setNextElement(_Unwind_Ptr element) { Kotlin_NativePtrArray_set(obj(), index++, (KNativePtr)element); } void setNextElement(_Unwind_Ptr element) { array.push_back(reinterpret_cast<void*>(element)); }
ObjHeader* obj() { return arrayHolder.obj(); }
int index;
int skipCount; int skipCount;
ObjHolder arrayHolder; KStdVector<void*> array;
}; };
_Unwind_Reason_Code depthCountCallback(struct _Unwind_Context* context, void* arg) { _Unwind_Reason_Code depthCountCallback(struct _Unwind_Context* context, void* arg) {
@@ -67,9 +61,6 @@ _Unwind_Reason_Code unwindCallback(struct _Unwind_Context* context, void* arg) {
#else #else
_Unwind_Ptr address = _Unwind_GetIP(context); _Unwind_Ptr address = _Unwind_GetIP(context);
#endif #endif
// We run the unwinding process in the native thread state. But setting a next element
// requires writing to a Kotlin array which must be performed in the runnable thread state.
kotlin::ThreadStateGuard guard(kotlin::ThreadState::kRunnable);
backtrace->setNextElement(address); backtrace->setNextElement(address);
return _URC_NO_REASON; return _URC_NO_REASON;
@@ -79,9 +70,8 @@ _Unwind_Reason_Code unwindCallback(struct _Unwind_Context* context, void* arg) {
THREAD_LOCAL_VARIABLE bool disallowSourceInfo = false; THREAD_LOCAL_VARIABLE bool disallowSourceInfo = false;
#if !KONAN_NO_BACKTRACE && !USE_GCC_UNWIND #if !KONAN_NO_BACKTRACE && !USE_GCC_UNWIND
SourceInfo getSourceInfo(KConstRef stackTrace, int32_t index) { SourceInfo getSourceInfo(void* symbol) {
return disallowSourceInfo ? SourceInfo{.fileName = nullptr, .lineNumber = -1, .column = -1} return disallowSourceInfo ? SourceInfo{.fileName = nullptr, .lineNumber = -1, .column = -1} : Kotlin_getSourceInfo(symbol);
: Kotlin_getSourceInfo(*PrimitiveArrayAddressOfElementAt<KNativePtr>(stackTrace->array(), index));
} }
#endif #endif
@@ -89,70 +79,65 @@ SourceInfo getSourceInfo(KConstRef stackTrace, int32_t index) {
// TODO: this implementation is just a hack, e.g. the result is inexact; // TODO: this implementation is just a hack, e.g. the result is inexact;
// however it is better to have an inexact stacktrace than not to have any. // however it is better to have an inexact stacktrace than not to have any.
extern "C" NO_INLINE OBJ_GETTER0(Kotlin_getCurrentStackTrace) { NO_INLINE KStdVector<void*> kotlin::GetCurrentStackTrace(int extraSkipFrames) noexcept {
#if KONAN_NO_BACKTRACE #if KONAN_NO_BACKTRACE
return AllocArrayInstance(theNativePtrArrayTypeInfo, 0, OBJ_RESULT); return {};
#else #else
// Skips first 2 elements as irrelevant: this function and primary Throwable constructor. // Skips this function frame + anything asked by the caller.
constexpr int kSkipFrames = 2; const int kSkipFrames = 1 + extraSkipFrames;
#if USE_GCC_UNWIND #if USE_GCC_UNWIND
int depth = 0; int depth = 0;
CallWithThreadState<ThreadState::kNative>(_Unwind_Backtrace, depthCountCallback, static_cast<void*>(&depth)); _Unwind_Backtrace(depthCountCallback, static_cast<void*>(&depth));
Backtrace result(depth, kSkipFrames); Backtrace result(depth, kSkipFrames);
if (result.obj()->array()->count_ > 0) { if (result.array.capacity() > 0) {
CallWithThreadState<ThreadState::kNative>(_Unwind_Backtrace, unwindCallback, static_cast<void*>(&result)); _Unwind_Backtrace(unwindCallback, static_cast<void*>(&result));
} }
RETURN_OBJ(result.obj()); return std::move(result.array);
#else #else
const int maxSize = 32; const int maxSize = 32;
void* buffer[maxSize]; void* buffer[maxSize];
int size = kotlin::CallWithThreadState<kotlin::ThreadState::kNative>(backtrace, buffer, maxSize); int size = backtrace(buffer, maxSize);
if (size < kSkipFrames) return AllocArrayInstance(theNativePtrArrayTypeInfo, 0, OBJ_RESULT); if (size < kSkipFrames) return {};
ObjHolder resultHolder; KStdVector<void*> result;
ObjHeader* result = AllocArrayInstance(theNativePtrArrayTypeInfo, size - kSkipFrames, resultHolder.slot()); result.reserve(size - kSkipFrames);
for (int index = kSkipFrames; index < size; ++index) { for (int index = kSkipFrames; index < size; ++index) {
Kotlin_NativePtrArray_set(result, index - kSkipFrames, buffer[index]); result.push_back(buffer[index]);
} }
RETURN_OBJ(result); return result;
#endif #endif
#endif // !KONAN_NO_BACKTRACE #endif // !KONAN_NO_BACKTRACE
} }
OBJ_GETTER(kotlin::GetStackTraceStrings, KConstRef stackTrace) { KStdVector<KStdString> kotlin::GetStackTraceStrings(void* const* stackTrace, size_t stackTraceSize) noexcept {
#if KONAN_NO_BACKTRACE #if KONAN_NO_BACKTRACE
ObjHeader* result = AllocArrayInstance(theArrayTypeInfo, 1, OBJ_RESULT); KStdVector<KStdString> strings;
ObjHolder holder; strings.push_back("<UNIMPLEMENTED>");
CreateStringFromCString("<UNIMPLEMENTED>", holder.slot()); return strings;
UpdateHeapRef(ArrayAddressOfElementAt(result->array(), 0), holder.obj());
return result;
#else #else
int32_t size = static_cast<int32_t>(stackTrace->array()->count_); KStdVector<KStdString> strings;
ObjHolder resultHolder; strings.reserve(stackTraceSize);
ObjHeader* strings = AllocArrayInstance(theArrayTypeInfo, size, resultHolder.slot());
#if USE_GCC_UNWIND #if USE_GCC_UNWIND
for (int32_t index = 0; index < size; ++index) { for (size_t index = 0; index < stackTraceSize; ++index) {
KNativePtr address = Kotlin_NativePtrArray_get(stackTrace, index); KNativePtr address = stackTrace[index];
char symbol[512]; char symbol[512];
if (!CallWithThreadState<ThreadState::kNative>(AddressToSymbol, (const void*)address, symbol, sizeof(symbol))) { if (!AddressToSymbol(address, symbol, sizeof(symbol))) {
// Make empty string: // Make empty string:
symbol[0] = '\0'; symbol[0] = '\0';
} }
char line[512]; char line[512];
konan::snprintf(line, sizeof(line) - 1, "%s (%p)", symbol, (void*)(intptr_t)address); konan::snprintf(line, sizeof(line) - 1, "%s (%p)", symbol, (void*)(intptr_t)address);
ObjHolder holder; strings.push_back(line);
CreateStringFromCString(line, holder.slot());
UpdateHeapRef(ArrayAddressOfElementAt(strings->array(), index), holder.obj());
} }
#else #else
if (size > 0) { if (stackTraceSize > 0) {
char** symbols = CallWithThreadState<ThreadState::kNative>( char** symbols = backtrace_symbols(stackTrace, static_cast<int>(stackTraceSize));
backtrace_symbols, PrimitiveArrayAddressOfElementAt<KNativePtr>(stackTrace->array(), 0), size);
RuntimeCheck(symbols != nullptr, "Not enough memory to retrieve the stacktrace"); RuntimeCheck(symbols != nullptr, "Not enough memory to retrieve the stacktrace");
for (int32_t index = 0; index < size; ++index) { for (size_t index = 0; index < stackTraceSize; ++index) {
auto sourceInfo = CallWithThreadState<ThreadState::kNative>(getSourceInfo, stackTrace, index); KNativePtr address = stackTrace[index];
auto sourceInfo = getSourceInfo(address);
const char* symbol = symbols[index]; const char* symbol = symbols[index];
const char* result; const char* result;
char line[1024]; char line[1024];
@@ -167,15 +152,13 @@ OBJ_GETTER(kotlin::GetStackTraceStrings, KConstRef stackTrace) {
} else { } else {
result = symbol; result = symbol;
} }
ObjHolder holder; strings.push_back(result);
CreateStringFromCString(result, holder.slot());
UpdateHeapRef(ArrayAddressOfElementAt(strings->array(), index), holder.obj());
} }
// Not konan::free. Used to free memory allocated in backtrace_symbols where malloc is used. // Not konan::free. Used to free memory allocated in backtrace_symbols where malloc is used.
free(symbols); free(symbols);
} }
#endif #endif
RETURN_OBJ(strings); return strings;
#endif // !KONAN_NO_BACKTRACE #endif // !KONAN_NO_BACKTRACE
} }
@@ -183,23 +166,23 @@ void kotlin::DisallowSourceInfo() {
disallowSourceInfo = true; disallowSourceInfo = true;
} }
void kotlin::PrintStackTraceStderr() { NO_INLINE void kotlin::PrintStackTraceStderr() {
// NOTE: This might be called from both runnable and native states (including in uninitialized runtime)
// TODO: This is intended for runtime use. Try to avoid memory allocations and signal unsafe functions. // TODO: This is intended for runtime use. Try to avoid memory allocations and signal unsafe functions.
kotlin::ThreadStateGuard guard(kotlin::ThreadState::kRunnable, true); // TODO: This might have to go into `GetCurrentStackTrace`, but this changes the generated stacktrace for
// `Throwable`.
ObjHolder stackTrace; #if KONAN_WINDOWS
Kotlin_getCurrentStackTrace(stackTrace.slot()); // Skip this function and `_Unwind_Backtrace`.
ObjHolder stackTraceStrings; constexpr int kSkipFrames = 2;
kotlin::GetStackTraceStrings(stackTrace.obj(), stackTraceStrings.slot()); #else
ArrayHeader* stackTraceStringsArray = stackTraceStrings.obj()->array(); // Skip this function.
for (uint32_t i = 0; i < stackTraceStringsArray->count_; ++i) { constexpr int kSkipFrames = 1;
ArrayHeader* symbol = (*ArrayAddressOfElementAt(stackTraceStringsArray, i))->array(); #endif
auto* utf16 = CharArrayAddressOfElementAt(symbol, 0); auto stackTrace = GetCurrentStackTrace(kSkipFrames);
KStdString utf8; auto stackTraceStrings = GetStackTraceStrings(stackTrace.data(), stackTrace.size());
utf8::with_replacement::utf16to8(utf16, utf16 + symbol->count_, std::back_inserter(utf8)); for (auto& frame : stackTraceStrings) {
kotlin::ThreadStateGuard guard(kotlin::ThreadState::kNative); konan::consoleErrorUtf8(frame.c_str(), frame.size());
konan::consoleErrorUtf8(utf8.c_str(), utf8.size());
konan::consoleErrorf("\n"); konan::consoleErrorf("\n");
} }
} }
@@ -8,7 +8,13 @@
namespace kotlin { namespace kotlin {
OBJ_GETTER(GetStackTraceStrings, KConstRef stackTrace); // TODO: Instead of KStd* provide allocator-customizable versions, to allow stack memory allocation.
// TODO: Model API as in upcoming https://en.cppreference.com/w/cpp/utility/basic_stacktrace
KStdVector<void*> GetCurrentStackTrace(int extraSkipFrames) noexcept;
// TODO: This is asking for a span.
KStdVector<KStdString> GetStackTraceStrings(void* const* stackTrace, size_t stackTraceSize) noexcept;
// It's not always safe to extract SourceInfo during unhandled exception termination. // It's not always safe to extract SourceInfo during unhandled exception termination.
void DisallowSourceInfo(); void DisallowSourceInfo();
@@ -16,6 +22,3 @@ void DisallowSourceInfo();
void PrintStackTraceStderr(); void PrintStackTraceStderr();
} // namespace kotlin } // namespace kotlin
// Returns current stacktrace as Array<String>.
extern "C" OBJ_GETTER0(Kotlin_getCurrentStackTrace);
@@ -5,6 +5,8 @@
#include "StackTrace.hpp" #include "StackTrace.hpp"
#include <signal.h>
#include "gmock/gmock.h" #include "gmock/gmock.h"
#include "gtest/gtest.h" #include "gtest/gtest.h"
@@ -16,21 +18,63 @@ using namespace kotlin;
namespace { namespace {
NO_INLINE void AbortWithStackTrace() { NO_INLINE KStdVector<void*> GetStackTrace1(int skipFrames) {
return GetCurrentStackTrace(skipFrames);
}
NO_INLINE KStdVector<void*> GetStackTrace2(int skipFrames) {
return GetStackTrace1(skipFrames);
}
NO_INLINE void AbortWithStackTrace(int) {
PrintStackTraceStderr(); PrintStackTraceStderr();
konan::abort(); konan::abort();
} }
} // namespace } // namespace
TEST(StackTraceTest, StackTrace) {
// TODO: Consider incorporating extra skipping to `GetCurrentStackTrace` on windows.
#if KONAN_WINDOWS
constexpr int kSkip = 1;
#else
constexpr int kSkip = 0;
#endif
auto stackTrace = GetStackTrace2(kSkip);
auto symbolicStackTrace = GetStackTraceStrings(stackTrace.data(), stackTrace.size());
ASSERT_GT(symbolicStackTrace.size(), 0ul);
EXPECT_THAT(symbolicStackTrace[0], testing::HasSubstr("GetStackTrace1"));
}
TEST(StackTraceTest, StackTraceWithSkip) {
// TODO: Consider incorporating extra skipping to `GetCurrentStackTrace` on windows.
#if KONAN_WINDOWS
constexpr int kSkip = 2;
#else
constexpr int kSkip = 1;
#endif
auto stackTrace = GetStackTrace2(kSkip);
auto symbolicStackTrace = GetStackTraceStrings(stackTrace.data(), stackTrace.size());
ASSERT_GT(symbolicStackTrace.size(), 0ul);
EXPECT_THAT(symbolicStackTrace[0], testing::HasSubstr("GetStackTrace2"));
}
TEST(StackTraceDeathTest, PrintStackTrace) { TEST(StackTraceDeathTest, PrintStackTrace) {
EXPECT_DEATH( EXPECT_DEATH(
{ kotlin::RunInNewThread(AbortWithStackTrace); }, { AbortWithStackTrace(0); },
#if KONAN_WINDOWS testing::AllOf(
// TODO: Fix Windows to match other platforms. testing::HasSubstr("AbortWithStackTrace"), testing::HasSubstr("StackTraceDeathTest_PrintStackTrace_Test"),
testing::AllOf(testing::HasSubstr("AbortWithStackTrace"), testing::HasSubstr("PrintStackTraceStderr")) testing::Not(testing::HasSubstr("PrintStackTraceStderr"))));
#else }
testing::AllOf(testing::HasSubstr("AbortWithStackTrace"), testing::Not(testing::HasSubstr("PrintStackTraceStderr")))
#endif TEST(StackTraceDeathTest, PrintStackTraceInSignalHandler) {
); EXPECT_DEATH(
{
signal(SIGINT, &AbortWithStackTrace);
raise(SIGINT);
},
testing::AllOf(
testing::HasSubstr("AbortWithStackTrace"),
testing::HasSubstr("StackTraceDeathTest_PrintStackTraceInSignalHandler_Test"),
testing::Not(testing::HasSubstr("PrintStackTraceStderr"))));
} }