Callstack: in-process stack capture and thread sampling #5
3 changed files with 334 additions and 91 deletions
|
|
@ -55,8 +55,7 @@ CallStackCapture::CallStackCapture() {
|
||||||
mBuffLen = STACKS_LENGTH;
|
mBuffLen = STACKS_LENGTH;
|
||||||
mBuff = (CallStack*) malloc(mBuffLen * sizeof(CallStack));
|
mBuff = (CallStack*) malloc(mBuffLen * sizeof(CallStack));
|
||||||
|
|
||||||
mErrorSnapshot.frames[0] = (alni) & errorNoSpaceLeft;
|
mErrorSnapshot.frames[0] = (alni) &errorNoSpaceLeft;
|
||||||
|
|
||||||
|
|
||||||
platformInit();
|
platformInit();
|
||||||
|
|
||||||
|
|
@ -110,103 +109,38 @@ CallStackCapture::~CallStackCapture() {
|
||||||
|
|
||||||
// ---------------------------------- Platform Depended ---------------------------------- //
|
// ---------------------------------- Platform Depended ---------------------------------- //
|
||||||
|
|
||||||
#if defined(ENV_OS_LINUX)
|
#if defined(ENV_OS_LINUX) || defined(ENV_OS_ANDROID)
|
||||||
|
|
||||||
|
#include "StackTrace.hpp"
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <cxxabi.h>
|
|
||||||
#include <execinfo.h>
|
|
||||||
#include <malloc.h>
|
|
||||||
|
|
||||||
void CallStackCapture::platformInit() {
|
namespace {
|
||||||
|
void copyTail(char* out, const std::string& text) {
|
||||||
|
const size_t length = text.size();
|
||||||
|
const size_t start =
|
||||||
|
length > CallStackCapture::MAX_DEBUG_INFO_LEN ? length - CallStackCapture::MAX_DEBUG_INFO_LEN : 0;
|
||||||
|
std::strcpy(out, text.c_str() + start);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CallStackCapture::platformDeinit() {
|
void CallStackCapture::platformInit() {}
|
||||||
}
|
|
||||||
|
void CallStackCapture::platformDeinit() {}
|
||||||
|
|
||||||
void CallStackCapture::platformWriteStackTrace(CallStack* stack) {
|
void CallStackCapture::platformWriteStackTrace(CallStack* stack) {
|
||||||
auto depth = backtrace((void**) stack->frames, (int) MAX_CALL_DEPTH_CAPTURE - 1);
|
uintptr_t frames[MAX_CALL_DEPTH_CAPTURE];
|
||||||
|
const size_t depth = StackTrace::captureCallingThread(frames, MAX_CALL_DEPTH_CAPTURE - 1);
|
||||||
|
for (size_t index = 0; index < depth; index++)
|
||||||
|
stack->frames[index] = (FramePointer) frames[index];
|
||||||
stack->frames[depth] = 0;
|
stack->frames[depth] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void getGetSourceFromBinaryAddress(const char* binary, const char* address, char* file, ualni* line) {
|
|
||||||
static char buff[1024];
|
|
||||||
|
|
||||||
snprintf(buff, sizeof(buff), "addr2line -e %s -a %s", binary, address);
|
|
||||||
FILE* pipe = popen(buff, "r");
|
|
||||||
if (pipe) {
|
|
||||||
fgets(buff, sizeof(buff), pipe);
|
|
||||||
fgets(buff, sizeof(buff), pipe);
|
|
||||||
pclose(pipe);
|
|
||||||
char* linePtr = strchr(buff, ':');
|
|
||||||
|
|
||||||
printf("%s\n", buff);
|
|
||||||
|
|
||||||
if (linePtr != nullptr && buff[0] != '?' && buff[1] != '?' && buff[2] != ':') {
|
|
||||||
*linePtr = '\0';
|
|
||||||
auto sourceLen = std::strlen(buff);
|
|
||||||
std::strcpy(file, buff + ((sourceLen > CallStackCapture::MAX_DEBUG_INFO_LEN) ? (sourceLen - CallStackCapture::MAX_DEBUG_INFO_LEN) : 0));
|
|
||||||
*line = strtoul(linePtr + 1, nullptr, 10);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::strcpy(file, "unresolved");
|
|
||||||
*line = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void getDemangledName(const char* func, char* out) {
|
|
||||||
int status;
|
|
||||||
size_t funcDemangledSize = CallStackCapture::MAX_DEBUG_INFO_LEN;
|
|
||||||
char* funcDemangled = (char*) malloc(funcDemangledSize);
|
|
||||||
char* ret = abi::__cxa_demangle(func, funcDemangled, &funcDemangledSize, &status);
|
|
||||||
if (status == 0) {
|
|
||||||
funcDemangled = ret;
|
|
||||||
auto funcLen = std::strlen(funcDemangled);
|
|
||||||
std::strcpy(out, funcDemangled + ((funcLen > CallStackCapture::MAX_DEBUG_INFO_LEN) ? (funcLen - CallStackCapture::MAX_DEBUG_INFO_LEN) : 0));
|
|
||||||
free(ret);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto funcLen = std::strlen(func);
|
|
||||||
std::strcpy(out, func + ((funcLen > CallStackCapture::MAX_DEBUG_INFO_LEN) ? (funcLen - CallStackCapture::MAX_DEBUG_INFO_LEN) : 0));
|
|
||||||
free(funcDemangled);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CallStackCapture::platformWriteDebugSymbols(FramePointer frame, DebugSymbols* out) {
|
void CallStackCapture::platformWriteDebugSymbols(FramePointer frame, DebugSymbols* out) {
|
||||||
void* addrList[1] = { (void*) frame };
|
const FrameSymbol symbol = StackTrace::symbolize((uintptr_t) frame);
|
||||||
auto symbolsArray = backtrace_symbols(addrList, 1);
|
copyTail(out->file, symbol.module.empty() ? std::string("unresolved") : symbol.module);
|
||||||
|
copyTail(out->function, symbol.function.empty() ? std::string("unresolved") : symbol.function);
|
||||||
// 'bin(fun+addr)'
|
out->line = symbol.line;
|
||||||
char* bin = *symbolsArray;
|
|
||||||
char* func = nullptr;
|
|
||||||
char* offset = nullptr;
|
|
||||||
|
|
||||||
// 'bin fun+addr'
|
|
||||||
for (char* p = bin; *p; ++p) {
|
|
||||||
if (*p == '(') {
|
|
||||||
*p = 0;
|
|
||||||
func = p + 1;
|
|
||||||
} else if (*p == '+') {
|
|
||||||
offset = p;
|
|
||||||
} else if (*p == ')' && offset) {
|
|
||||||
*p = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (func && offset) {
|
|
||||||
getGetSourceFromBinaryAddress(bin, func, out->file, &out->line);
|
|
||||||
|
|
||||||
if (offset != func) {
|
|
||||||
*offset = 0;
|
|
||||||
getDemangledName(func, out->function);
|
|
||||||
} else {
|
|
||||||
std::strcpy(out->function, "unresolved");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
std::strcpy(out->file, "unresolved");
|
|
||||||
std::strcpy(out->function, "unresolved");
|
|
||||||
}
|
|
||||||
|
|
||||||
free(symbolsArray);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CallStackCapture::printSnapshot(const CallStack* snapshot) {
|
void CallStackCapture::printSnapshot(const CallStack* snapshot) {
|
||||||
|
|
|
||||||
268
Callstack/private/StackTrace.cpp
Normal file
268
Callstack/private/StackTrace.cpp
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
#include "StackTrace.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#if defined(__linux__) || defined(__ANDROID__)
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <chrono>
|
||||||
|
#include <csignal>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cxxabi.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <mutex>
|
||||||
|
#include <sys/syscall.h>
|
||||||
|
#include <thread>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <unwind.h>
|
||||||
|
|
||||||
|
namespace tp {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct UnwindState {
|
||||||
|
uintptr_t* frames;
|
||||||
|
size_t count;
|
||||||
|
size_t capacity;
|
||||||
|
};
|
||||||
|
|
||||||
|
_Unwind_Reason_Code collectFrame(_Unwind_Context* context, void* argument) {
|
||||||
|
auto* state = static_cast<UnwindState*>(argument);
|
||||||
|
const uintptr_t address = (uintptr_t) _Unwind_GetIP(context);
|
||||||
|
if (address == 0) return _URC_NO_REASON;
|
||||||
|
if (state->count >= state->capacity) return _URC_END_OF_STACK;
|
||||||
|
state->frames[state->count++] = address;
|
||||||
|
return _URC_NO_REASON;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SampleSlot {
|
||||||
|
std::atomic<int> tid{ 0 };
|
||||||
|
std::atomic<size_t> depth{ 0 };
|
||||||
|
std::atomic<bool> answered{ false };
|
||||||
|
uintptr_t frames[StackTrace::kMaxFrames] = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr size_t kMaxSampledThreads = 256;
|
||||||
|
|
||||||
|
struct SampleBoard {
|
||||||
|
SampleSlot slots[kMaxSampledThreads];
|
||||||
|
std::atomic<size_t> used{ 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
SampleBoard& sampleBoard() {
|
||||||
|
static SampleBoard shared;
|
||||||
|
return shared;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::mutex& samplingMutex() {
|
||||||
|
static std::mutex shared;
|
||||||
|
return shared;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sampleSignal() { return SIGRTMIN + 3; }
|
||||||
|
|
||||||
|
int currentTid() { return (int) syscall(SYS_gettid); }
|
||||||
|
|
||||||
|
SampleSlot* slotOf(int tid) {
|
||||||
|
SampleBoard& board = sampleBoard();
|
||||||
|
const size_t used = std::min(board.used.load(std::memory_order_acquire), kMaxSampledThreads);
|
||||||
|
for (size_t index = 0; index < used; index++)
|
||||||
|
if (board.slots[index].tid.load(std::memory_order_relaxed) == tid) return &board.slots[index];
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void answerSample(int, siginfo_t*, void*) {
|
||||||
|
const int savedErrno = errno;
|
||||||
|
if (SampleSlot* slot = slotOf(currentTid())) {
|
||||||
|
slot->depth.store(
|
||||||
|
StackTrace::captureCallingThread(slot->frames, StackTrace::kMaxFrames), std::memory_order_relaxed
|
||||||
|
);
|
||||||
|
slot->answered.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
|
errno = savedErrno;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string firstLine(const std::filesystem::path& path) {
|
||||||
|
std::ifstream file(path);
|
||||||
|
std::string line;
|
||||||
|
std::getline(file, line);
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string schedulerState(const std::filesystem::path& task) {
|
||||||
|
std::ifstream file(task / "status");
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(file, line))
|
||||||
|
if (line.rfind("State:", 0) == 0) return line.substr(line.find_first_not_of(" \t", 6));
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int> processThreadIds() {
|
||||||
|
std::vector<int> tids;
|
||||||
|
std::error_code error;
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator("/proc/self/task", error)) {
|
||||||
|
const std::string name = entry.path().filename().string();
|
||||||
|
if (!name.empty() && std::all_of(name.begin(), name.end(), [](char c) {
|
||||||
|
return std::isdigit((unsigned char) c);
|
||||||
|
}))
|
||||||
|
tids.push_back(std::stoi(name));
|
||||||
|
}
|
||||||
|
std::sort(tids.begin(), tids.end());
|
||||||
|
return tids;
|
||||||
|
}
|
||||||
|
|
||||||
|
void armSlots(const std::vector<int>& tids) {
|
||||||
|
SampleBoard& board = sampleBoard();
|
||||||
|
for (size_t index = 0; index < tids.size(); index++) {
|
||||||
|
SampleSlot& slot = board.slots[index];
|
||||||
|
slot.answered.store(false, std::memory_order_relaxed);
|
||||||
|
slot.depth.store(0, std::memory_order_relaxed);
|
||||||
|
slot.tid.store(tids[index], std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
board.used.store(tids.size(), std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
void waitForAnswers(size_t count, double timeoutMs) {
|
||||||
|
SampleBoard& board = sampleBoard();
|
||||||
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::duration<double, std::milli>(timeoutMs);
|
||||||
|
const auto allAnswered = [&]() {
|
||||||
|
for (size_t index = 0; index < count; index++)
|
||||||
|
if (!board.slots[index].answered.load(std::memory_order_acquire)) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
while (!allAnswered() && std::chrono::steady_clock::now() < deadline)
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadSample collect(const SampleSlot& slot, int tid) {
|
||||||
|
const std::filesystem::path task = "/proc/self/task/" + std::to_string(tid);
|
||||||
|
ThreadSample sample;
|
||||||
|
sample.tid = tid;
|
||||||
|
sample.name = firstLine(task / "comm");
|
||||||
|
sample.state = schedulerState(task);
|
||||||
|
sample.waitChannel = firstLine(task / "wchan");
|
||||||
|
sample.answered = slot.answered.load(std::memory_order_acquire);
|
||||||
|
if (sample.answered)
|
||||||
|
sample.frames.assign(slot.frames, slot.frames + std::min(slot.depth.load(), StackTrace::kMaxFrames));
|
||||||
|
return sample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t StackTrace::captureCallingThread(uintptr_t* frames, size_t capacity) {
|
||||||
|
UnwindState state{ frames, 0, capacity };
|
||||||
|
_Unwind_Backtrace(collectFrame, &state);
|
||||||
|
return state.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameSymbol StackTrace::symbolize(uintptr_t address) {
|
||||||
|
FrameSymbol symbol;
|
||||||
|
Dl_info info{};
|
||||||
|
if (!dladdr((void*) (address - 1), &info)) return symbol;
|
||||||
|
if (info.dli_fname) symbol.module = std::filesystem::path(info.dli_fname).filename().string();
|
||||||
|
symbol.moduleOffset = address - (uintptr_t) info.dli_fbase;
|
||||||
|
if (!info.dli_sname) return symbol;
|
||||||
|
int status = 0;
|
||||||
|
char* demangled = abi::__cxa_demangle(info.dli_sname, nullptr, nullptr, &status);
|
||||||
|
symbol.function = status == 0 && demangled ? demangled : info.dli_sname;
|
||||||
|
std::free(demangled);
|
||||||
|
return symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StackTrace::canSampleThreads() { return true; }
|
||||||
|
|
||||||
|
bool StackTrace::isDebuggerAttached() {
|
||||||
|
std::ifstream status("/proc/self/status");
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(status, line))
|
||||||
|
if (line.rfind("TracerPid:", 0) == 0) return std::strtol(line.c_str() + 10, nullptr, 10) != 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StackTrace::prepareThreadSampling() {
|
||||||
|
static std::once_flag prepared;
|
||||||
|
std::call_once(prepared, []() {
|
||||||
|
struct sigaction action{};
|
||||||
|
action.sa_sigaction = answerSample;
|
||||||
|
action.sa_flags = SA_SIGINFO | SA_RESTART;
|
||||||
|
sigemptyset(&action.sa_mask);
|
||||||
|
sigaction(sampleSignal(), &action, nullptr);
|
||||||
|
uintptr_t warmup[4];
|
||||||
|
captureCallingThread(warmup, 4);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ThreadSample> StackTrace::sampleAllThreads(double answerTimeoutMs) {
|
||||||
|
prepareThreadSampling();
|
||||||
|
std::lock_guard lock(samplingMutex());
|
||||||
|
std::vector<int> tids = processThreadIds();
|
||||||
|
tids.erase(std::remove(tids.begin(), tids.end(), currentTid()), tids.end());
|
||||||
|
if (tids.size() > kMaxSampledThreads) tids.resize(kMaxSampledThreads);
|
||||||
|
|
||||||
|
armSlots(tids);
|
||||||
|
for (int tid : tids)
|
||||||
|
syscall(SYS_tgkill, getpid(), tid, sampleSignal());
|
||||||
|
waitForAnswers(tids.size(), answerTimeoutMs);
|
||||||
|
|
||||||
|
std::vector<ThreadSample> samples;
|
||||||
|
for (size_t index = 0; index < tids.size(); index++)
|
||||||
|
samples.push_back(collect(sampleBoard().slots[index], tids[index]));
|
||||||
|
sampleBoard().used.store(0, std::memory_order_release);
|
||||||
|
return samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
int StackTrace::callingThreadId() { return currentTid(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#elif defined(_WIN32)
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
namespace tp {
|
||||||
|
|
||||||
|
size_t StackTrace::captureCallingThread(uintptr_t* frames, size_t capacity) {
|
||||||
|
return (size_t) CaptureStackBackTrace(1, (DWORD) std::min<size_t>(capacity, 62), (void**) frames, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameSymbol StackTrace::symbolize(uintptr_t address) {
|
||||||
|
FrameSymbol symbol;
|
||||||
|
HMODULE module = nullptr;
|
||||||
|
if (!GetModuleHandleExA(
|
||||||
|
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||||
|
(LPCSTR) address,
|
||||||
|
&module
|
||||||
|
))
|
||||||
|
return symbol;
|
||||||
|
char path[MAX_PATH] = {};
|
||||||
|
GetModuleFileNameA(module, path, MAX_PATH);
|
||||||
|
const std::string full = path;
|
||||||
|
symbol.module = full.substr(full.find_last_of("\\/") + 1);
|
||||||
|
symbol.moduleOffset = address - (uintptr_t) module;
|
||||||
|
return symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StackTrace::canSampleThreads() { return false; }
|
||||||
|
bool StackTrace::isDebuggerAttached() { return IsDebuggerPresent() != 0; }
|
||||||
|
void StackTrace::prepareThreadSampling() {}
|
||||||
|
std::vector<ThreadSample> StackTrace::sampleAllThreads(double) { return {}; }
|
||||||
|
int StackTrace::callingThreadId() { return (int) GetCurrentThreadId(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace tp {
|
||||||
|
|
||||||
|
std::string StackTrace::describe(uintptr_t address) {
|
||||||
|
const FrameSymbol symbol = symbolize(address);
|
||||||
|
std::ostringstream text;
|
||||||
|
text << "0x" << std::hex << address;
|
||||||
|
if (!symbol.module.empty()) text << " " << symbol.module << "+0x" << symbol.moduleOffset;
|
||||||
|
text << std::dec;
|
||||||
|
if (!symbol.function.empty()) text << " " << symbol.function;
|
||||||
|
if (!symbol.file.empty()) text << " " << symbol.file << ":" << symbol.line;
|
||||||
|
return text.str();
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Callstack/public/StackTrace.hpp
Normal file
41
Callstack/public/StackTrace.hpp
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace tp {
|
||||||
|
|
||||||
|
struct FrameSymbol {
|
||||||
|
std::string module;
|
||||||
|
uintptr_t moduleOffset = 0;
|
||||||
|
std::string function;
|
||||||
|
std::string file;
|
||||||
|
size_t line = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ThreadSample {
|
||||||
|
int tid = 0;
|
||||||
|
std::string name;
|
||||||
|
std::string state;
|
||||||
|
std::string waitChannel;
|
||||||
|
bool answered = false;
|
||||||
|
std::vector<uintptr_t> frames;
|
||||||
|
};
|
||||||
|
|
||||||
|
class StackTrace {
|
||||||
|
public:
|
||||||
|
static constexpr size_t kMaxFrames = 64;
|
||||||
|
|
||||||
|
static size_t captureCallingThread(uintptr_t* frames, size_t capacity);
|
||||||
|
[[nodiscard]] static FrameSymbol symbolize(uintptr_t address);
|
||||||
|
[[nodiscard]] static std::string describe(uintptr_t address);
|
||||||
|
|
||||||
|
[[nodiscard]] static bool canSampleThreads();
|
||||||
|
[[nodiscard]] static bool isDebuggerAttached();
|
||||||
|
static void prepareThreadSampling();
|
||||||
|
[[nodiscard]] static std::vector<ThreadSample> sampleAllThreads(double answerTimeoutMs = 200);
|
||||||
|
[[nodiscard]] static int callingThreadId();
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue