Modules/Callstack/private/StackTrace.cpp
Шурупов Илья Викторович 1ed98b9c32 Make hang reports reliable under a debugger and in input loops
A report could be left empty: sampling thread stacks signals every
thread, and a debugger such as gdb stops the whole process on that
signal before anything is written. Debugger pauses also looked like
hangs, so resuming from a breakpoint produced a false report whose
sampling froze the app again.

The summary (phase, open scopes, input events) and application files
are now written first, before any sampling. Sampling is skipped while a
debugger is attached, and the report says so. When the watchdog's own
100 ms wait takes a second or more, the whole process was paused, and
that time no longer counts towards the stalled phase.

The input loop now counts handled events per phase, so a report shows
when the UI thread kept processing a stream of events without ever
leaving input handling. The watchdog clock is injectable for tests.
2026-10-22 11:57:47 +03:00

268 lines
8.3 KiB
C++

#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();
}
}