feat(agc): write the worker sampler's stacks as collapsed stacks
APS5_SAMPLE_WORKER=<file> now also writes <file stem>.collapsed.txt: one "frame;...;frame count" line per distinct stack of queue worker 0, root first, frames as module+0xRVA, which speedscope.app and flamegraph.pl open. Walks go up to 64 frames, and nothing allocates while the worker is suspended. The libraries are built with -fno-asynchronous-unwind-tables, so most of their functions have no .pdata entry, only a DWARF record in .ehfram, and a walk ended at the first such frame. The worker's walk now steps those frames through their record, with the CIE/FDE decoding and the call frame instructions of libc's unwinder, which move from Unwind.cpp into its header unchanged. A module is indexed after the worker resumes, the first time a walk meets it, and every register the step restores is read inside the worker's stack. The process sampler (APS5_SAMPLE_THREADS) keeps its 12 frames through .pdata. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
682ef0c174
commit
428963d3b3
|
|
@ -274,6 +274,17 @@ if(BUILD_TESTING)
|
|||
set_tests_properties(agc_profile_output PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_stack_walk_tests tests/StackWalk.cpp)
|
||||
target_include_directories(agc_stack_walk_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_stack_walk_tests PRIVATE libc)
|
||||
configure_windows_unwind(agc_stack_walk_tests)
|
||||
add_test(NAME agc_stack_walk COMMAND agc_stack_walk_tests)
|
||||
set_tests_properties(agc_stack_walk PROPERTIES TIMEOUT 20)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_stack_walk PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,155 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_STACKWALK_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_STACKWALK_HPP
|
||||
|
||||
#include "prx/libc/include/exceptions/Unwind.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
class DwarfFrames {
|
||||
public:
|
||||
using Byte = LibcUnwind::Byte;
|
||||
using Word = LibcUnwind::Word;
|
||||
using Registers = std::array<Word, 17>;
|
||||
enum class Step { Stepped, NoRecord, Failed };
|
||||
|
||||
DwarfFrames(Word base, Word end, const Byte* frames, const Byte* framesEnd) : base(base), end(end) {
|
||||
for (const Byte* p = frames; p != nullptr && framesEnd - p >= 8;) {
|
||||
const Byte* record = p;
|
||||
const auto length = LibcUnwind::Read<std::uint32_t>(p);
|
||||
if (length == 0) continue;
|
||||
if (length == 0xffffffff || length < 4 || static_cast<Word>(framesEnd - p) < length) break;
|
||||
const Byte* next = p + length;
|
||||
if (LibcUnwind::Read<std::uint32_t>(p) != 0) {
|
||||
LibcUnwind::Frame frame{};
|
||||
const Byte* cursor = nullptr;
|
||||
const char* augmentation = nullptr;
|
||||
Byte lsdaEncoding = 0;
|
||||
if (LibcUnwind::DecodeRange(frame, LibcUnwind::Lookup{0, record}, cursor, augmentation, lsdaEncoding)) index.emplace_back(frame.start, record);
|
||||
}
|
||||
p = next;
|
||||
}
|
||||
std::sort(index.begin(), index.end());
|
||||
}
|
||||
|
||||
Word Base() const {
|
||||
return base;
|
||||
}
|
||||
|
||||
bool Contains(Word pc) const {
|
||||
return pc >= base && pc < end;
|
||||
}
|
||||
|
||||
bool Covers(Word pc) const {
|
||||
LibcUnwind::Frame frame{};
|
||||
return find(pc, frame) == Step::Stepped;
|
||||
}
|
||||
|
||||
template<typename TRead>
|
||||
Step Unwind(Registers& registers, bool leaf, TRead&& read) const {
|
||||
const Word pc = registers[16] - (leaf ? 0 : 1);
|
||||
LibcUnwind::Frame frame{};
|
||||
if (const auto found = find(pc, frame); found != Step::Stepped) return found;
|
||||
LibcUnwind::Rules initial;
|
||||
if (!LibcUnwind::Instructions(frame.cieBegin, frame.cieEnd, frame, ~Word(0), initial, {})) return Step::Failed;
|
||||
if constexpr (LibcUnwind::RuleCount > 32) {
|
||||
if (frame.returnRegister == 32) {
|
||||
initial.registers[16] = initial.registers[32];
|
||||
initial.registers[32] = {};
|
||||
frame.returnRegister = 16;
|
||||
}
|
||||
}
|
||||
LibcUnwind::Rules rules = initial;
|
||||
if (!LibcUnwind::Instructions(frame.begin, frame.end, frame, pc, rules, initial)) return Step::Failed;
|
||||
if (rules.cfaExpression != nullptr || rules.cfaRegister >= 16 || frame.returnRegister >= 17) return Step::Failed;
|
||||
const Word cfa = registers[rules.cfaRegister] + rules.cfaOffset;
|
||||
if (cfa <= registers[7]) return Step::Failed;
|
||||
Registers next = registers;
|
||||
for (unsigned column = 0; column < next.size(); ++column) {
|
||||
const auto& rule = rules.registers[column];
|
||||
if (rule.kind == 0) continue;
|
||||
if (rule.kind == 1) {
|
||||
if (!read(cfa + rule.value, next[column])) return Step::Failed;
|
||||
} else if (rule.kind == 2 && rule.value >= 0 && rule.value < 17) {
|
||||
next[column] = registers[rule.value];
|
||||
} else if (rule.kind == 4) {
|
||||
next[column] = cfa + rule.value;
|
||||
} else if (rule.kind == 5) {
|
||||
next[column] = 0;
|
||||
} else {
|
||||
return Step::Failed;
|
||||
}
|
||||
}
|
||||
next[7] = cfa;
|
||||
next[16] = next[frame.returnRegister];
|
||||
if (next[16] == 0 || (next[7] == registers[7] && next[16] == registers[16])) return Step::Failed;
|
||||
registers = next;
|
||||
return Step::Stepped;
|
||||
}
|
||||
|
||||
private:
|
||||
Step find(Word pc, LibcUnwind::Frame& frame) const {
|
||||
const auto found = std::upper_bound(index.begin(), index.end(), pc, [](Word value, const auto& entry) { return value < entry.first; });
|
||||
if (found == index.begin()) return Step::NoRecord;
|
||||
const Byte* p = nullptr;
|
||||
const char* augmentation = nullptr;
|
||||
Byte lsdaEncoding = 0;
|
||||
if (!LibcUnwind::DecodeRange(frame, LibcUnwind::Lookup{pc, std::prev(found)->second}, p, augmentation, lsdaEncoding)) return Step::Failed;
|
||||
if (pc < frame.start || pc - frame.start >= frame.length) return Step::NoRecord;
|
||||
if (*augmentation == 'z') {
|
||||
const auto size = LibcUnwind::Uleb(p);
|
||||
p += size;
|
||||
}
|
||||
frame.begin = p;
|
||||
return Step::Stepped;
|
||||
}
|
||||
|
||||
Word base;
|
||||
Word end;
|
||||
std::vector<std::pair<Word, const Byte*>> index;
|
||||
};
|
||||
|
||||
class CollapsedStacks {
|
||||
public:
|
||||
void Add(std::span<const std::uint64_t> leafFirst, bool truncated) {
|
||||
std::vector<std::uint64_t> stack(leafFirst.begin(), leafFirst.end());
|
||||
if (truncated) {
|
||||
stack.push_back(0);
|
||||
++truncatedStacks;
|
||||
}
|
||||
++stacks[std::move(stack)];
|
||||
}
|
||||
|
||||
std::uint64_t Truncated() const {
|
||||
return truncatedStacks;
|
||||
}
|
||||
|
||||
template<typename TName>
|
||||
std::string Format(TName&& name) const {
|
||||
std::string output;
|
||||
for (const auto& [stack, count] : stacks) {
|
||||
for (auto frame = stack.rbegin(); frame != stack.rend(); ++frame) {
|
||||
if (frame != stack.rbegin()) output += ';';
|
||||
output += *frame == 0 ? std::string("[truncated]") : name(*frame);
|
||||
}
|
||||
output += ' ' + std::to_string(count) + '\n';
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::vector<std::uint64_t>, std::uint64_t> stacks;
|
||||
std::uint64_t truncatedStacks = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -3,9 +3,11 @@
|
|||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#ifdef _WIN32
|
||||
#include "prx/libSceAgcDriver/Execution/include/StackWalk.hpp"
|
||||
#include <windows.h>
|
||||
#include <tlhelp32.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
|
|
@ -29,43 +31,134 @@ std::jthread workerSamplerThread;
|
|||
// Debug aid: APS5_SAMPLE_WORKER=<file> samples the calling thread every millisecond and, every 20 s,
|
||||
// writes "module+offset self inclusive" lines to <file> for offline symbolization with nm.
|
||||
struct Sampler {
|
||||
static constexpr int ProcessDepth = 12;
|
||||
static constexpr int WorkerDepth = 64;
|
||||
|
||||
HANDLE target = nullptr;
|
||||
std::string path;
|
||||
int depth = ProcessDepth;
|
||||
bool keepStacks = false;
|
||||
std::uint64_t stackLow = 0;
|
||||
std::uint64_t stackHigh = 0;
|
||||
std::vector<DwarfFrames> modules;
|
||||
std::uint64_t pendingModule = 0;
|
||||
std::mutex mutex;
|
||||
std::map<std::uint64_t, std::pair<std::uint64_t, std::uint64_t>> counts;
|
||||
CollapsedStacks stacks;
|
||||
std::uint64_t samples = 0;
|
||||
std::int64_t suspendedTicks = 0;
|
||||
std::int64_t longestSuspendedTicks = 0;
|
||||
|
||||
~Sampler() noexcept(false) {
|
||||
if (target != nullptr && !CloseHandle(target)) throw std::system_error(GetLastError(), std::system_category(), "Closing sampler thread handle");
|
||||
}
|
||||
|
||||
void sample() {
|
||||
CONTEXT context{};
|
||||
context.ContextFlags = CONTEXT_FULL;
|
||||
if (SuspendThread(target) == static_cast<DWORD>(-1)) return;
|
||||
const bool captured = GetThreadContext(target, &context) != 0;
|
||||
std::vector<std::uint64_t> frames;
|
||||
if (captured) {
|
||||
// Unwinding reads the target's stack while it is suspended; frames without unwind data
|
||||
// (JIT/guest code) end the walk.
|
||||
for (int depth = 0; depth < 12 && context.Rip != 0; ++depth) {
|
||||
frames.push_back(context.Rip);
|
||||
DWORD64 imageBase = 0;
|
||||
auto* entry = RtlLookupFunctionEntry(context.Rip, &imageBase, nullptr);
|
||||
if (entry == nullptr) break;
|
||||
bool readStack(std::uint64_t address, std::uint64_t& value) const {
|
||||
if (address < stackLow || address > stackHigh - sizeof(value)) return false;
|
||||
std::memcpy(&value, reinterpret_cast<const void*>(address), sizeof(value));
|
||||
return true;
|
||||
}
|
||||
|
||||
const DwarfFrames* moduleOf(std::uint64_t pc) const {
|
||||
const auto found = std::upper_bound(modules.begin(), modules.end(), pc, [](std::uint64_t value, const DwarfFrames& module) { return value < module.Base(); });
|
||||
return found != modules.begin() && std::prev(found)->Contains(pc) ? &*std::prev(found) : nullptr;
|
||||
}
|
||||
|
||||
bool hasUnwindData(std::uint64_t pc) const {
|
||||
DWORD64 imageBase = 0;
|
||||
const auto* module = moduleOf(pc - 1);
|
||||
return RtlLookupFunctionEntry(pc, &imageBase, nullptr) != nullptr || (module != nullptr && module->Covers(pc - 1));
|
||||
}
|
||||
|
||||
DwarfFrames::Step stepDwarf(CONTEXT& context, bool leaf) {
|
||||
const auto* module = moduleOf(context.Rip - (leaf ? 0 : 1));
|
||||
if (module == nullptr) {
|
||||
MEMORY_BASIC_INFORMATION memory{};
|
||||
if (VirtualQuery(reinterpret_cast<LPCVOID>(context.Rip), &memory, sizeof(memory)) != 0 && memory.Type == MEM_IMAGE) pendingModule = reinterpret_cast<std::uint64_t>(memory.AllocationBase);
|
||||
return DwarfFrames::Step::Failed;
|
||||
}
|
||||
DwarfFrames::Registers registers{context.Rax, context.Rdx, context.Rcx, context.Rbx, context.Rsi, context.Rdi, context.Rbp, context.Rsp, context.R8, context.R9, context.R10, context.R11, context.R12, context.R13, context.R14, context.R15, context.Rip};
|
||||
const auto step = module->Unwind(registers, leaf, [&](std::uint64_t address, std::uint64_t& value) { return readStack(address, value); });
|
||||
if (step != DwarfFrames::Step::Stepped) return step;
|
||||
DWORD64* columns[] = {&context.Rax, &context.Rdx, &context.Rcx, &context.Rbx, &context.Rsi, &context.Rdi, &context.Rbp, &context.Rsp, &context.R8, &context.R9, &context.R10, &context.R11, &context.R12, &context.R13, &context.R14, &context.R15, &context.Rip};
|
||||
for (std::size_t column = 0; column < registers.size(); ++column) *columns[column] = registers[column];
|
||||
return step;
|
||||
}
|
||||
|
||||
int walk(CONTEXT& context, std::array<std::uint64_t, WorkerDepth>& frames) {
|
||||
const bool dwarf = stackHigh != 0 && context.Rsp >= stackLow && context.Rsp < stackHigh;
|
||||
int count = 0;
|
||||
while (count < depth && context.Rip != 0) {
|
||||
frames[count++] = context.Rip;
|
||||
DWORD64 imageBase = 0;
|
||||
if (auto* entry = RtlLookupFunctionEntry(context.Rip, &imageBase, nullptr)) {
|
||||
PVOID handler = nullptr;
|
||||
DWORD64 establisher = 0;
|
||||
RtlVirtualUnwind(UNW_FLAG_NHANDLER, imageBase, context.Rip, entry, &context, &handler, &establisher, nullptr);
|
||||
continue;
|
||||
}
|
||||
if (!dwarf) break;
|
||||
const auto step = stepDwarf(context, count == 1);
|
||||
if (step == DwarfFrames::Step::Stepped) continue;
|
||||
std::uint64_t returnAddress = 0;
|
||||
if (step == DwarfFrames::Step::Failed || count != 1 || !readStack(context.Rsp, returnAddress) || !hasUnwindData(returnAddress)) break;
|
||||
context.Rip = returnAddress;
|
||||
context.Rsp += sizeof(returnAddress);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void indexModule() {
|
||||
const auto base = std::exchange(pendingModule, 0);
|
||||
if (moduleOf(base) != nullptr) return;
|
||||
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
|
||||
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(base + dos->e_lfanew);
|
||||
const auto* sections = IMAGE_FIRST_SECTION(nt);
|
||||
const DwarfFrames::Byte* frames = nullptr;
|
||||
const DwarfFrames::Byte* framesEnd = nullptr;
|
||||
for (unsigned index = 0; index < nt->FileHeader.NumberOfSections; ++index) {
|
||||
if (std::memcmp(sections[index].Name, ".ehfram", 8) != 0) continue;
|
||||
frames = reinterpret_cast<const DwarfFrames::Byte*>(base + sections[index].VirtualAddress);
|
||||
framesEnd = frames + sections[index].Misc.VirtualSize;
|
||||
}
|
||||
const auto at = std::upper_bound(modules.begin(), modules.end(), base, [](std::uint64_t value, const DwarfFrames& module) { return value < module.Base(); });
|
||||
modules.emplace(at, base, base + nt->OptionalHeader.SizeOfImage, frames, framesEnd);
|
||||
}
|
||||
|
||||
void sample() {
|
||||
CONTEXT context{};
|
||||
context.ContextFlags = CONTEXT_FULL;
|
||||
std::array<std::uint64_t, WorkerDepth> frames;
|
||||
int count = 0;
|
||||
LARGE_INTEGER suspended{};
|
||||
LARGE_INTEGER resumed{};
|
||||
QueryPerformanceCounter(&suspended);
|
||||
if (SuspendThread(target) == static_cast<DWORD>(-1)) return;
|
||||
if (GetThreadContext(target, &context) != 0) count = walk(context, frames);
|
||||
ResumeThread(target);
|
||||
QueryPerformanceCounter(&resumed);
|
||||
if (pendingModule != 0) indexModule();
|
||||
std::lock_guard lock(mutex);
|
||||
++samples;
|
||||
for (std::size_t i = 0; i < frames.size(); ++i) {
|
||||
auto& count = counts[frames[i]];
|
||||
if (i == 0) ++count.first;
|
||||
++count.second;
|
||||
suspendedTicks += resumed.QuadPart - suspended.QuadPart;
|
||||
longestSuspendedTicks = std::max(longestSuspendedTicks, resumed.QuadPart - suspended.QuadPart);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto& entry = counts[frames[i]];
|
||||
if (i == 0) ++entry.first;
|
||||
++entry.second;
|
||||
}
|
||||
if (keepStacks && count != 0) stacks.Add({frames.data(), static_cast<std::size_t>(count)}, context.Rip != 0);
|
||||
}
|
||||
|
||||
static std::string frameName(std::uint64_t address, std::map<std::uint64_t, std::string>& loaded) {
|
||||
HMODULE module = nullptr;
|
||||
char name[MAX_PATH] = "?";
|
||||
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast<LPCSTR>(address), &module)) GetModuleFileNameA(module, name, sizeof(name));
|
||||
const char* base = std::strrchr(name, '\\');
|
||||
if (module != nullptr) loaded.emplace(reinterpret_cast<std::uint64_t>(module), base ? base + 1 : name);
|
||||
char frame[MAX_PATH + 32];
|
||||
std::snprintf(frame, sizeof(frame), "%s+0x%llx", base ? base + 1 : name, static_cast<unsigned long long>(address - reinterpret_cast<std::uint64_t>(module)));
|
||||
return frame;
|
||||
}
|
||||
|
||||
void write() {
|
||||
|
|
@ -81,6 +174,34 @@ struct Sampler {
|
|||
std::fprintf(file, "%s 0x%llx %llu %llu\n", base ? base + 1 : name, static_cast<unsigned long long>(address - reinterpret_cast<std::uint64_t>(module)), static_cast<unsigned long long>(count.first), static_cast<unsigned long long>(count.second));
|
||||
}
|
||||
std::fclose(file);
|
||||
if (keepStacks) writeStacks();
|
||||
}
|
||||
|
||||
void writeStacks() {
|
||||
const auto dot = path.rfind('.');
|
||||
const auto slash = path.find_last_of("\\/");
|
||||
const auto stem = dot != std::string::npos && (slash == std::string::npos || dot > slash) ? path.substr(0, dot) : path;
|
||||
const auto collapsed = stem + ".collapsed.txt";
|
||||
const auto temporary = collapsed + ".tmp";
|
||||
std::map<std::uint64_t, std::string> names;
|
||||
std::map<std::uint64_t, std::string> loaded;
|
||||
for (const auto& [address, count] : counts) names.emplace(address, frameName(address, loaded));
|
||||
std::FILE* file = std::fopen(temporary.c_str(), "w");
|
||||
if (file == nullptr) return;
|
||||
LARGE_INTEGER frequency{};
|
||||
QueryPerformanceFrequency(&frequency);
|
||||
const double tickUs = 1e6 / static_cast<double>(frequency.QuadPart);
|
||||
std::fprintf(file, "# samples=%llu depth=%d truncated=%llu suspended_us_mean=%.1f suspended_us_max=%.1f\n", static_cast<unsigned long long>(samples), depth, static_cast<unsigned long long>(stacks.Truncated()),
|
||||
samples != 0 ? static_cast<double>(suspendedTicks) * tickUs / static_cast<double>(samples) : 0.0, static_cast<double>(longestSuspendedTicks) * tickUs);
|
||||
for (const auto& [base, name] : loaded) {
|
||||
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
|
||||
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(base + dos->e_lfanew);
|
||||
std::fprintf(file, "# module %s 0x%lx 0x%lx\n", name.c_str(), static_cast<unsigned long>(nt->OptionalHeader.SizeOfImage), static_cast<unsigned long>(nt->FileHeader.TimeDateStamp));
|
||||
}
|
||||
const auto text = stacks.Format([&](std::uint64_t address) { return names.at(address); });
|
||||
const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size();
|
||||
if (std::fclose(file) != 0 || !written) return;
|
||||
MoveFileExA(temporary.c_str(), collapsed.c_str(), MOVEFILE_REPLACE_EXISTING);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -172,6 +293,14 @@ void StartWorkerSampler() {
|
|||
if (path == nullptr) return;
|
||||
auto sampler = std::make_unique<Sampler>();
|
||||
sampler->path = path;
|
||||
sampler->depth = Sampler::WorkerDepth;
|
||||
sampler->keepStacks = true;
|
||||
const auto* block = reinterpret_cast<const NT_TIB*>(NtCurrentTeb());
|
||||
MEMORY_BASIC_INFORMATION stack{};
|
||||
if (VirtualQuery(block->StackLimit, &stack, sizeof(stack)) != 0) {
|
||||
sampler->stackLow = reinterpret_cast<std::uint64_t>(stack.AllocationBase);
|
||||
sampler->stackHigh = reinterpret_cast<std::uint64_t>(block->StackBase);
|
||||
}
|
||||
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &sampler->target, THREAD_ALL_ACCESS, FALSE, 0)) throw std::system_error(GetLastError(), std::system_category(), "Duplicating sampler thread handle");
|
||||
workerSamplerThread = std::jthread([sampler = std::move(sampler)](std::stop_token token) {
|
||||
auto flushed = std::chrono::steady_clock::now();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/StackWalk.hpp"
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::CollapsedStacks;
|
||||
using AgcDriver::DwarfFrames;
|
||||
using Byte = DwarfFrames::Byte;
|
||||
using Word = DwarfFrames::Word;
|
||||
using Step = DwarfFrames::Step;
|
||||
|
||||
constexpr std::size_t FunctionOffset = 0x100;
|
||||
constexpr std::size_t FunctionSize = 0x40;
|
||||
|
||||
void Check(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
class Section {
|
||||
public:
|
||||
explicit Section(Byte returnColumn) : bytes(0x200, 0) {
|
||||
const std::size_t cie = append32(0);
|
||||
append32(0);
|
||||
append({1, 'z', 'R', 0, 1, 0x78, returnColumn, 1, 0x1b, 0x0c, 7, 8, static_cast<Byte>(0x80 | returnColumn), 1});
|
||||
patch32(cie, static_cast<std::uint32_t>(cursor - cie - 4));
|
||||
const std::size_t fde = append32(0);
|
||||
append32(static_cast<std::uint32_t>(cursor - cie));
|
||||
append32(static_cast<std::uint32_t>(Function() - Address(cursor)));
|
||||
append32(FunctionSize);
|
||||
append({0, 0x41, 0x0e, 0x10, 0x86, 0x02, 0x43, 0x0d, 0x06, 0, 0});
|
||||
patch32(fde, static_cast<std::uint32_t>(cursor - fde - 4));
|
||||
end = cursor;
|
||||
append32(0);
|
||||
}
|
||||
|
||||
Word Function() const {
|
||||
return Address(FunctionOffset);
|
||||
}
|
||||
|
||||
DwarfFrames Frames() const {
|
||||
return DwarfFrames(Address(0), Address(bytes.size()), bytes.data(), bytes.data() + end + 4);
|
||||
}
|
||||
|
||||
private:
|
||||
Word Address(std::size_t offset) const {
|
||||
return reinterpret_cast<Word>(bytes.data() + offset);
|
||||
}
|
||||
|
||||
std::size_t append32(std::uint32_t value) {
|
||||
const auto at = cursor;
|
||||
std::memcpy(bytes.data() + cursor, &value, 4);
|
||||
cursor += 4;
|
||||
return at;
|
||||
}
|
||||
|
||||
void append(std::initializer_list<Byte> values) {
|
||||
for (const auto value : values) bytes[cursor++] = value;
|
||||
}
|
||||
|
||||
void patch32(std::size_t at, std::uint32_t value) {
|
||||
std::memcpy(bytes.data() + at, &value, 4);
|
||||
}
|
||||
|
||||
std::vector<Byte> bytes;
|
||||
std::size_t cursor = 0;
|
||||
std::size_t end = 0;
|
||||
};
|
||||
|
||||
struct Stack {
|
||||
std::array<Word, 64> words{};
|
||||
|
||||
Word At(std::size_t index) const {
|
||||
return reinterpret_cast<Word>(&words[index]);
|
||||
}
|
||||
|
||||
auto Reader() const {
|
||||
return [this](Word address, Word& value) {
|
||||
if (address < At(0) || address > At(words.size() - 1)) return false;
|
||||
std::memcpy(&value, reinterpret_cast<const void*>(address), sizeof(value));
|
||||
return true;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
DwarfFrames::Registers Registers(Word pc, Word sp, Word fp) {
|
||||
DwarfFrames::Registers registers{};
|
||||
registers[16] = pc;
|
||||
registers[7] = sp;
|
||||
registers[6] = fp;
|
||||
return registers;
|
||||
}
|
||||
|
||||
void Prologue() {
|
||||
const Section section(16);
|
||||
const auto frames = section.Frames();
|
||||
const auto function = section.Function();
|
||||
Stack stack;
|
||||
stack.words[32] = 0x5555;
|
||||
stack.words[33] = 0x1234;
|
||||
|
||||
auto registers = Registers(function, stack.At(33), 0x9999);
|
||||
Check(frames.Unwind(registers, true, stack.Reader()) == Step::Stepped, "the entry of a function did not step");
|
||||
Check(registers[16] == 0x1234 && registers[7] == stack.At(34) && registers[6] == 0x9999, "the entry of a function stepped to the wrong frame");
|
||||
|
||||
registers = Registers(function + 2, stack.At(32), 0x9999);
|
||||
Check(frames.Unwind(registers, true, stack.Reader()) == Step::Stepped, "the frame after the push did not step");
|
||||
Check(registers[16] == 0x1234 && registers[7] == stack.At(34) && registers[6] == 0x5555, "the frame after the push did not restore the frame pointer");
|
||||
|
||||
registers = Registers(function + 0x20, stack.At(8), stack.At(32));
|
||||
Check(frames.Unwind(registers, false, stack.Reader()) == Step::Stepped, "the body of a function did not step");
|
||||
Check(registers[16] == 0x1234 && registers[7] == stack.At(34) && registers[6] == 0x5555, "the body of a function did not step through the frame pointer");
|
||||
}
|
||||
|
||||
void Outside() {
|
||||
const Section section(16);
|
||||
const auto frames = section.Frames();
|
||||
const auto function = section.Function();
|
||||
Stack stack;
|
||||
auto registers = Registers(function + FunctionSize, stack.At(8), 0);
|
||||
Check(frames.Unwind(registers, true, stack.Reader()) == Step::NoRecord, "a pc after the function had a record");
|
||||
registers = Registers(function - 1, stack.At(8), 0);
|
||||
Check(frames.Unwind(registers, true, stack.Reader()) == Step::NoRecord, "a pc before the function had a record");
|
||||
Check(frames.Covers(function + 0x10) && !frames.Covers(function + FunctionSize), "the record covers the wrong range");
|
||||
registers = Registers(function, stack.At(63) + 4, 0);
|
||||
const auto before = registers;
|
||||
Check(frames.Unwind(registers, true, stack.Reader()) == Step::Failed && registers == before, "a read outside the stack was not refused");
|
||||
Check(DwarfFrames(0, 0, nullptr, nullptr).Unwind(registers, true, stack.Reader()) == Step::NoRecord, "a module without records stepped");
|
||||
}
|
||||
|
||||
void ReturnColumn32() {
|
||||
const Section section(32);
|
||||
const auto frames = section.Frames();
|
||||
Stack stack;
|
||||
stack.words[10] = 0x4321;
|
||||
auto registers = Registers(section.Function(), stack.At(10), 0);
|
||||
#ifdef _WIN32
|
||||
Check(frames.Unwind(registers, true, stack.Reader()) == Step::Stepped && registers[16] == 0x4321 && registers[7] == stack.At(11), "the MinGW return column was not read");
|
||||
#else
|
||||
Check(frames.Unwind(registers, true, stack.Reader()) == Step::NoRecord, "a return column above 16 was accepted");
|
||||
#endif
|
||||
}
|
||||
|
||||
void Collapsed() {
|
||||
CollapsedStacks stacks;
|
||||
const std::uint64_t full[] = {0x10, 0x20, 0x30};
|
||||
const std::uint64_t cut[] = {0x10, 0x40};
|
||||
stacks.Add(full, false);
|
||||
stacks.Add(full, false);
|
||||
stacks.Add(cut, true);
|
||||
const auto text = stacks.Format([](std::uint64_t address) {
|
||||
char name[32];
|
||||
std::snprintf(name, sizeof(name), "m+0x%llx", static_cast<unsigned long long>(address));
|
||||
return std::string(name);
|
||||
});
|
||||
Check(text == "m+0x30;m+0x20;m+0x10 2\n[truncated];m+0x40;m+0x10 1\n", "collapsed stacks changed format");
|
||||
Check(stacks.Truncated() == 1, "the truncated walk was not counted");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
Prologue();
|
||||
Outside();
|
||||
ReturnColumn32();
|
||||
Collapsed();
|
||||
std::cout << "stack walk tests passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -95,6 +95,135 @@ inline unsigned EncodingSize(Byte encoding) {
|
|||
default: std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
struct Lookup { Word pc; const Byte* fde {}; Word text {}; Word data {}; };
|
||||
|
||||
struct Frame {
|
||||
const Byte* cieBegin {};
|
||||
const Byte* cieEnd {};
|
||||
const Byte* begin {};
|
||||
const Byte* end {};
|
||||
Word start {}, length {}, personality {}, lsda {};
|
||||
Word codeAlign {};
|
||||
std::intptr_t dataAlign {};
|
||||
unsigned returnRegister {};
|
||||
bool signal {};
|
||||
#ifdef _WIN32
|
||||
bool foreign {};
|
||||
_Unwind_Context caller {};
|
||||
#endif
|
||||
};
|
||||
|
||||
inline bool DecodeRange(Frame& frame, const Lookup& query, const Byte*& p, const char*& augmentation, Byte& lsdaEncoding) {
|
||||
if (!query.fde) return false;
|
||||
p = query.fde;
|
||||
auto length = Read<std::uint32_t>(p);
|
||||
if (!length || length == 0xffffffff) return false;
|
||||
frame.end = p + length;
|
||||
const Byte* ciePointer = p;
|
||||
auto cieOffset = Read<std::uint32_t>(p);
|
||||
const Byte* cie = ciePointer - cieOffset;
|
||||
const Byte* c = cie;
|
||||
auto cieLength = Read<std::uint32_t>(c);
|
||||
frame.cieEnd = c + cieLength;
|
||||
if (Read<std::uint32_t>(c) != 0) return false;
|
||||
Byte version = *c++;
|
||||
if (version != 1 && version != 3 && version != 4) return false;
|
||||
augmentation = reinterpret_cast<const char*>(c);
|
||||
while (*c++) {}
|
||||
if (version == 4 && (*c++ != sizeof(Word) || *c++ != 0)) return false;
|
||||
frame.codeAlign = Uleb(c);
|
||||
frame.dataAlign = Sleb(c);
|
||||
frame.returnRegister = version == 1 ? *c++ : Uleb(c);
|
||||
#ifdef _WIN32
|
||||
if (frame.returnRegister >= 17 && frame.returnRegister != 32) return false;
|
||||
#else
|
||||
if (frame.returnRegister >= 17) return false;
|
||||
#endif
|
||||
Byte pointerEncoding = 0;
|
||||
lsdaEncoding = 255;
|
||||
if (*augmentation == 'z') {
|
||||
Word size = Uleb(c);
|
||||
const Byte* end = c + size;
|
||||
for (const char* a = augmentation + 1; *a; ++a) {
|
||||
switch (*a) {
|
||||
case 'R': pointerEncoding = *c++; break;
|
||||
case 'L': lsdaEncoding = *c++; break;
|
||||
case 'P': { Byte encoding = *c++; frame.personality = Encoded(c, encoding, query.data, 0, query.text); break; }
|
||||
case 'S': frame.signal = true; break;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
c = end;
|
||||
} else if (*augmentation) return false;
|
||||
frame.cieBegin = c;
|
||||
frame.start = Encoded(p, pointerEncoding, query.data, 0, query.text);
|
||||
frame.length = Encoded(p, pointerEncoding & 15);
|
||||
return true;
|
||||
}
|
||||
|
||||
struct Rule { unsigned kind {}; std::intptr_t value {}; const Byte* expression {}; };
|
||||
#ifdef _WIN32
|
||||
constexpr unsigned RuleCount = 33;
|
||||
#else
|
||||
constexpr unsigned RuleCount = 17;
|
||||
#endif
|
||||
|
||||
struct Rules { Rule registers[RuleCount] {}; unsigned cfaRegister {7}; std::intptr_t cfaOffset {}; const Byte* cfaExpression {}; };
|
||||
|
||||
inline bool Instructions(const Byte* p, const Byte* end, const Frame& frame, Word target, Rules& state, const Rules& initial) {
|
||||
Word location = frame.start;
|
||||
Rules saved[16];
|
||||
unsigned depth = 0;
|
||||
while (p < end && location <= target) {
|
||||
Byte opcode = *p++;
|
||||
unsigned reg;
|
||||
if ((opcode & 192) == 64) { location += (opcode & 63) * frame.codeAlign; continue; }
|
||||
if ((opcode & 192) == 128) {
|
||||
reg = opcode & 63;
|
||||
auto offset = Uleb(p) * frame.dataAlign;
|
||||
if (reg < RuleCount) state.registers[reg] = {1, static_cast<std::intptr_t>(offset)};
|
||||
continue;
|
||||
}
|
||||
if ((opcode & 192) == 192) {
|
||||
reg = opcode & 63;
|
||||
if (reg < RuleCount) state.registers[reg] = initial.registers[reg];
|
||||
continue;
|
||||
}
|
||||
switch (opcode) {
|
||||
case 0: break;
|
||||
case 1: location = Read<Word>(p); break;
|
||||
case 2: location += Read<Byte>(p) * frame.codeAlign; break;
|
||||
case 3: location += Read<std::uint16_t>(p) * frame.codeAlign; break;
|
||||
case 4: location += Read<std::uint32_t>(p) * frame.codeAlign; break;
|
||||
case 5: case 17: case 20: case 21: {
|
||||
reg = Uleb(p);
|
||||
auto offset = (opcode == 17 || opcode == 21 ? Sleb(p) : std::intptr_t(Uleb(p))) * frame.dataAlign;
|
||||
if (reg < RuleCount) state.registers[reg] = {unsigned(opcode >= 20 ? 4 : 1), offset};
|
||||
break;
|
||||
}
|
||||
case 6: reg = Uleb(p); if (reg < RuleCount) state.registers[reg] = initial.registers[reg]; break;
|
||||
case 7: case 8: reg = Uleb(p); if (reg < RuleCount) state.registers[reg] = {unsigned(opcode == 7 ? 5 : 0)}; break;
|
||||
case 9: { reg = Uleb(p); auto other = Uleb(p); if (reg < RuleCount) state.registers[reg] = {2, std::intptr_t(other)}; break; }
|
||||
case 10: if (depth == 16) return false; saved[depth++] = state; break;
|
||||
case 11: if (!depth) return false; state = saved[--depth]; break;
|
||||
case 12: case 18:
|
||||
state.cfaExpression = nullptr; state.cfaRegister = Uleb(p);
|
||||
state.cfaOffset = opcode == 18 ? Sleb(p) * frame.dataAlign : Uleb(p); break;
|
||||
case 13: state.cfaRegister = Uleb(p); break;
|
||||
case 14: case 19: state.cfaOffset = opcode == 19 ? Sleb(p) * frame.dataAlign : Uleb(p); break;
|
||||
case 15: { state.cfaExpression = p; auto size = Uleb(p); p += size; break; }
|
||||
case 16: case 22: {
|
||||
reg = Uleb(p); const Byte* expr = p; auto size = Uleb(p); p += size;
|
||||
if (reg < RuleCount) state.registers[reg] = {unsigned(opcode == 16 ? 3 : 6), 0, expr};
|
||||
break;
|
||||
}
|
||||
case 46: Uleb(p); break;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
|
|
|||
|
|
@ -42,8 +42,6 @@ _Unwind_Reason_Code CallPersonality(Word personality, _Unwind_Action actions, _U
|
|||
return __gcc_personality_v0_nid_postfix(1, actions, exception->exception_class, exception, context);
|
||||
return (actions & _UA_SEARCH_PHASE) ? _URC_FATAL_PHASE1_ERROR : _URC_FATAL_PHASE2_ERROR;
|
||||
}
|
||||
struct Lookup { Word pc; const Byte* fde {}; Word text {}; Word data {}; };
|
||||
|
||||
#if defined(__linux__) || defined(__APPLE__)
|
||||
int FindFrame(dl_phdr_info* info, std::size_t, void* argument) {
|
||||
auto& query = *static_cast<Lookup*>(argument);
|
||||
|
|
@ -80,70 +78,6 @@ int FindFrame(dl_phdr_info* info, std::size_t, void* argument) {
|
|||
|
||||
#endif
|
||||
|
||||
struct Frame {
|
||||
const Byte* cieBegin {};
|
||||
const Byte* cieEnd {};
|
||||
const Byte* begin {};
|
||||
const Byte* end {};
|
||||
Word start {}, length {}, personality {}, lsda {};
|
||||
Word codeAlign {};
|
||||
std::intptr_t dataAlign {};
|
||||
unsigned returnRegister {};
|
||||
bool signal {};
|
||||
#ifdef _WIN32
|
||||
bool foreign {};
|
||||
_Unwind_Context caller {};
|
||||
#endif
|
||||
};
|
||||
|
||||
bool DecodeRange(Frame& frame, const Lookup& query, const Byte*& p, const char*& augmentation, Byte& lsdaEncoding) {
|
||||
if (!query.fde) return false;
|
||||
p = query.fde;
|
||||
auto length = Read<std::uint32_t>(p);
|
||||
if (!length || length == 0xffffffff) return false;
|
||||
frame.end = p + length;
|
||||
const Byte* ciePointer = p;
|
||||
auto cieOffset = Read<std::uint32_t>(p);
|
||||
const Byte* cie = ciePointer - cieOffset;
|
||||
const Byte* c = cie;
|
||||
auto cieLength = Read<std::uint32_t>(c);
|
||||
frame.cieEnd = c + cieLength;
|
||||
if (Read<std::uint32_t>(c) != 0) return false;
|
||||
Byte version = *c++;
|
||||
if (version != 1 && version != 3 && version != 4) return false;
|
||||
augmentation = reinterpret_cast<const char*>(c);
|
||||
while (*c++) {}
|
||||
if (version == 4 && (*c++ != sizeof(Word) || *c++ != 0)) return false;
|
||||
frame.codeAlign = Uleb(c);
|
||||
frame.dataAlign = Sleb(c);
|
||||
frame.returnRegister = version == 1 ? *c++ : Uleb(c);
|
||||
#ifdef _WIN32
|
||||
if (frame.returnRegister >= 17 && frame.returnRegister != 32) return false;
|
||||
#else
|
||||
if (frame.returnRegister >= 17) return false;
|
||||
#endif
|
||||
Byte pointerEncoding = 0;
|
||||
lsdaEncoding = 255;
|
||||
if (*augmentation == 'z') {
|
||||
Word size = Uleb(c);
|
||||
const Byte* end = c + size;
|
||||
for (const char* a = augmentation + 1; *a; ++a) {
|
||||
switch (*a) {
|
||||
case 'R': pointerEncoding = *c++; break;
|
||||
case 'L': lsdaEncoding = *c++; break;
|
||||
case 'P': { Byte encoding = *c++; frame.personality = Encoded(c, encoding, query.data, 0, query.text); break; }
|
||||
case 'S': frame.signal = true; break;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
c = end;
|
||||
} else if (*augmentation) return false;
|
||||
frame.cieBegin = c;
|
||||
frame.start = Encoded(p, pointerEncoding, query.data, 0, query.text);
|
||||
frame.length = Encoded(p, pointerEncoding & 15);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DecodeCandidate(_Unwind_Context& context, Frame& frame, const Lookup& query) {
|
||||
const Byte* p;
|
||||
const char* augmentation;
|
||||
|
|
@ -305,69 +239,6 @@ bool DecodeFrame(_Unwind_Context& context, Frame& frame) {
|
|||
#endif
|
||||
}
|
||||
|
||||
struct Rule { unsigned kind {}; std::intptr_t value {}; const Byte* expression {}; };
|
||||
#ifdef _WIN32
|
||||
constexpr unsigned RuleCount = 33;
|
||||
#else
|
||||
constexpr unsigned RuleCount = 17;
|
||||
#endif
|
||||
|
||||
struct Rules { Rule registers[RuleCount] {}; unsigned cfaRegister {7}; std::intptr_t cfaOffset {}; const Byte* cfaExpression {}; };
|
||||
|
||||
bool Instructions(const Byte* p, const Byte* end, const Frame& frame, Word target, Rules& state, const Rules& initial) {
|
||||
Word location = frame.start;
|
||||
Rules saved[16];
|
||||
unsigned depth = 0;
|
||||
while (p < end && location <= target) {
|
||||
Byte opcode = *p++;
|
||||
unsigned reg;
|
||||
if ((opcode & 192) == 64) { location += (opcode & 63) * frame.codeAlign; continue; }
|
||||
if ((opcode & 192) == 128) {
|
||||
reg = opcode & 63;
|
||||
auto offset = Uleb(p) * frame.dataAlign;
|
||||
if (reg < RuleCount) state.registers[reg] = {1, static_cast<std::intptr_t>(offset)};
|
||||
continue;
|
||||
}
|
||||
if ((opcode & 192) == 192) {
|
||||
reg = opcode & 63;
|
||||
if (reg < RuleCount) state.registers[reg] = initial.registers[reg];
|
||||
continue;
|
||||
}
|
||||
switch (opcode) {
|
||||
case 0: break;
|
||||
case 1: location = Read<Word>(p); break;
|
||||
case 2: location += Read<Byte>(p) * frame.codeAlign; break;
|
||||
case 3: location += Read<std::uint16_t>(p) * frame.codeAlign; break;
|
||||
case 4: location += Read<std::uint32_t>(p) * frame.codeAlign; break;
|
||||
case 5: case 17: case 20: case 21: {
|
||||
reg = Uleb(p);
|
||||
auto offset = (opcode == 17 || opcode == 21 ? Sleb(p) : std::intptr_t(Uleb(p))) * frame.dataAlign;
|
||||
if (reg < RuleCount) state.registers[reg] = {unsigned(opcode >= 20 ? 4 : 1), offset};
|
||||
break;
|
||||
}
|
||||
case 6: reg = Uleb(p); if (reg < RuleCount) state.registers[reg] = initial.registers[reg]; break;
|
||||
case 7: case 8: reg = Uleb(p); if (reg < RuleCount) state.registers[reg] = {unsigned(opcode == 7 ? 5 : 0)}; break;
|
||||
case 9: { reg = Uleb(p); auto other = Uleb(p); if (reg < RuleCount) state.registers[reg] = {2, std::intptr_t(other)}; break; }
|
||||
case 10: if (depth == 16) return false; saved[depth++] = state; break;
|
||||
case 11: if (!depth) return false; state = saved[--depth]; break;
|
||||
case 12: case 18:
|
||||
state.cfaExpression = nullptr; state.cfaRegister = Uleb(p);
|
||||
state.cfaOffset = opcode == 18 ? Sleb(p) * frame.dataAlign : Uleb(p); break;
|
||||
case 13: state.cfaRegister = Uleb(p); break;
|
||||
case 14: case 19: state.cfaOffset = opcode == 19 ? Sleb(p) * frame.dataAlign : Uleb(p); break;
|
||||
case 15: { state.cfaExpression = p; auto size = Uleb(p); p += size; break; }
|
||||
case 16: case 22: {
|
||||
reg = Uleb(p); const Byte* expr = p; auto size = Uleb(p); p += size;
|
||||
if (reg < RuleCount) state.registers[reg] = {unsigned(opcode == 16 ? 3 : 6), 0, expr};
|
||||
break;
|
||||
}
|
||||
case 46: Uleb(p); break;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Expression(const Byte* p, const _Unwind_Context& context, Word cfa, Word& result) {
|
||||
Word length = Uleb(p);
|
||||
const Byte* end = p + length;
|
||||
|
|
|
|||
|
|
@ -114,6 +114,12 @@ Each invocation creates one pipeline. With no `--cache-in`, its application pipe
|
|||
|
||||
Set `APS5_PIPELINE_STATS=1` to capture and print driver statistics for each newly created graphics or compute pipeline. This requires `VK_KHR_pipeline_executable_properties` and `pipelineExecutableInfo`; an unsupported device fails with an error. Statistic names and units are driver-specific. Capturing statistics can increase pipeline compilation cost. The setting is disabled by default.
|
||||
|
||||
## Worker sampler
|
||||
|
||||
Set `APS5_SAMPLE_WORKER=<file>` to sample queue worker 0 every millisecond (in practice every timer tick). Every 20 s and at exit it writes `<file>`, one `module offset self inclusive` line per sampled address, and `<file stem>.collapsed.txt`, the worker's whole stacks in the collapsed format that speedscope.app and flamegraph.pl read: one `frame;...;frame count` line per distinct stack, root first, each frame as `module+0xRVA`. A walk that ends before the thread's root starts with `[truncated]`. The `#` lines give the samples, the walk depth (64 frames), the truncated walks, the mean and longest time the worker was suspended, and the size and PE timestamp of each module, to check the binary a file is symbolized against. A frame with `.pdata` is stepped by `RtlVirtualUnwind`; any other frame by its DWARF record in `.ehfram`, the unwind data the libraries are built with. Those steps read only inside the worker's stack, and a module is indexed after the worker resumes, the first time a walk meets it.
|
||||
|
||||
`APS5_SAMPLE_THREADS=<file>` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to `<file>`, 12 frames deep and through `.pdata` only. Both samplers exist only on Windows; the addresses are symbolized offline, for example with `nm` on the `.prx`.
|
||||
|
||||
## Driver thread priority
|
||||
|
||||
Set `APS5_THREAD_PRIORITY=high` to raise the AGC driver's queue workers and the video output presenter above the title's threads. On Linux the threads get nice `APS5_THREAD_NICE` (default -10) through rtkit; on Windows they get `THREAD_PRIORITY_HIGHEST`. `APS5_THREAD_PRIORITY=rt` asks rtkit for real-time priority `APS5_THREAD_RT` (default 10, at most rtkit's `MaxRealtimePriority`) under rtkit's policy, `SCHED_RR` by default; on Windows it sets `THREAD_PRIORITY_TIME_CRITICAL`. rtkit only grants it when the process's hard `RLIMIT_RTTIME` is at most its `RTTimeUSecMax` (200000 us by default), so a hard limit above `APS5_THREAD_RTTIME_US` (default 200000, capped at `RTTimeUSecMax`) is lowered to it and the soft limit to at most half of it. The hard limit cannot be raised again and stays lowered for the whole process even if rtkit then refuses; see [technical debt](TechnicalDebt.md#functional). There is no fallback between the modes: when rtkit or `SetThreadPriority` refuses, a queue worker reports the refusal as a driver failure, which the next submission, wait or shutdown rethrows, and the presenter's start throws it. Each raised thread logs its priority once as `[thread-priority]`. The setting is `off` by default; a value other than `off`, `high` or `rt` is reported the same way as a refusal.
|
||||
|
|
|
|||
Loading…
Reference in New Issue