#include "daScript/misc/platform.h"
#if DAS_TRACK_ALLOC
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "daScript/misc/lexer_alloc_track.h"
#if defined(_MSC_VER)
#include
#include
#pragma comment(lib, "dbghelp.lib")
#elif defined(__linux__) || defined(__APPLE__)
#include
#include
#include
#endif
// init_seg(lib) registers our atexit handler before any user-level static
// ctor handler ends up at the bottom of the LIFO stack, fires after all
// user static dtors so their allocations don't show as leaks.
#if defined(_MSC_VER)
#pragma warning(push)
#pragma warning(disable: 4073)
#pragma init_seg(lib)
#pragma warning(pop)
#endif
namespace das {
static constexpr int kFrames = 16;
struct AllocInfo {
size_t size;
int frameCount;
void * frames[kFrames];
};
struct Entry {
void * key;
AllocInfo info;
};
static void * const kTombstone = reinterpret_cast(uintptr_t(-1));
// Open-addressing void* map using std::malloc/std::free so our own storage
// doesn't re-enter the hooks.
struct LeakMap {
Entry * entries = nullptr;
size_t capacity = 0;
size_t live = 0;
size_t filled = 0; // live + tombstones (rehash threshold)
void init() {
capacity = 4096;
entries = static_cast(std::calloc(capacity, sizeof(Entry)));
}
static size_t mix(void *p) {
uintptr_t x = reinterpret_cast(p);
x ^= x >> 33;
x *= 0xff51afd7ed558ccdULL;
x ^= x >> 33;
x *= 0xc4ceb9fe1a85ec53ULL;
x ^= x >> 33;
return size_t(x);
}
size_t find(void *key) const {
size_t mask = capacity - 1;
size_t i = mix(key) & mask;
size_t first_tomb = SIZE_MAX;
while (true) {
void *k = entries[i].key;
if (k == nullptr) return (first_tomb != SIZE_MAX) ? first_tomb : i;
if (k == key) return i;
if (k == kTombstone && first_tomb == SIZE_MAX) first_tomb = i;
i = (i + 1) & mask;
}
}
void rehash(size_t new_cap) {
Entry *old_entries = entries;
size_t old_cap = capacity;
entries = static_cast(std::calloc(new_cap, sizeof(Entry)));
capacity = new_cap;
size_t mask = new_cap - 1;
for (size_t j = 0; j < old_cap; ++j) {
void *k = old_entries[j].key;
if (k == nullptr || k == kTombstone) continue;
size_t i = mix(k) & mask;
while (entries[i].key != nullptr) i = (i + 1) & mask;
entries[i] = old_entries[j];
}
std::free(old_entries);
filled = live;
}
void insert(void *key, const AllocInfo &info) {
if (!entries) init();
if ((filled + 1) * 4 >= capacity * 3) rehash(capacity * 2);
size_t i = find(key);
void *k = entries[i].key;
entries[i].key = key;
entries[i].info = info;
if (k == nullptr) { ++filled; ++live; }
else if (k == kTombstone) { ++live; }
}
void erase(void *key) {
if (!entries || !live) return;
size_t i = find(key);
if (entries[i].key == key) {
entries[i].key = kTombstone;
--live;
}
}
};
// Placement-new into static storage: never destructed, so track_free during
// static teardown stays safe.
static LeakMap & getMap() {
alignas(LeakMap) static unsigned char storage[sizeof(LeakMap)];
static LeakMap *m = ::new (storage) LeakMap();
return *m;
}
static std::mutex & getMutex() {
alignas(std::mutex) static unsigned char storage[sizeof(std::mutex)];
static std::mutex *m = ::new (storage) std::mutex();
return *m;
}
static std::atomic g_armed{false};
static std::atomic g_orphan_free{0};
static thread_local bool tl_inside = false;
struct ReentryGuard {
bool prev;
ReentryGuard() : prev(tl_inside) { tl_inside = true; }
~ReentryGuard() { tl_inside = prev; }
};
#if defined(_MSC_VER) && defined(_M_X64)
// In alloc_tracker_fast_stack.cpp cached .pdata unwinder, drop-in for
// CaptureStackBackTrace. Default skipFrames=2 hides itself + the tracker hook.
unsigned das_fast_stack_capture(void **stack, unsigned maxFrames, int skipFrames = 2) noexcept;
#endif
// skipFrames/skip = 2: drop the capture function and the tracker hook so the
// visible top frame is the caller (operator new wrapper, etc).
static int capture_stack(void **frames, int max_frames) noexcept {
#if defined(_MSC_VER) && defined(_M_X64)
return (int)das_fast_stack_capture(frames, (unsigned)max_frames, 2);
#elif defined(_MSC_VER)
return (int)CaptureStackBackTrace(1, (DWORD)max_frames, frames, nullptr);
#elif defined(__linux__) || defined(__APPLE__)
void * raw[64];
int n = max_frames + 2 > 64 ? 64 : max_frames + 2;
int got = backtrace(raw, n);
int skip = got > 2 ? 2 : got;
int out = got - skip;
if (out > max_frames) out = max_frames;
for (int i = 0; i < out; ++i) frames[i] = raw[skip + i];
return out;
#else
(void)frames; (void)max_frames;
return 0;
#endif
}
// noinline so the skipFrames count above reliably drops this frame.
#if defined(_MSC_VER)
__declspec(noinline)
#else
__attribute__((noinline))
#endif
void track_alloc_hook(void *p, size_t sz) noexcept {
if (!g_armed.load(std::memory_order_relaxed)) return;
if (!p || tl_inside) return;
ReentryGuard g;
AllocInfo info;
info.size = sz;
info.frameCount = capture_stack(info.frames, kFrames);
std::lock_guard lock(getMutex());
getMap().insert(p, info);
}
void track_free_hook(void *p) noexcept {
if (!g_armed.load(std::memory_order_relaxed)) return;
if (!p || tl_inside) return;
ReentryGuard g;
lexer_track_free(reinterpret_cast(p));
std::lock_guard lock(getMutex());
LeakMap &m = getMap();
if (m.entries && m.live) {
size_t i = m.find(p);
if (m.entries[i].key == p) {
m.entries[i].key = kTombstone;
--m.live;
return;
}
}
// Allocated pre-arm or freed cross-DLL counted for diagnostics.
g_orphan_free.fetch_add(1, std::memory_order_relaxed);
}
void arm_alloc_tracking() noexcept {
g_armed.store(true, std::memory_order_release);
}
AllocTrackerInternalGuard::AllocTrackerInternalGuard() noexcept : prev(tl_inside) {
tl_inside = true;
}
AllocTrackerInternalGuard::~AllocTrackerInternalGuard() noexcept {
tl_inside = prev;
}
// ------------------------- Symbolization -------------------------
#if defined(_MSC_VER)
static bool g_symInitialized = false;
static void init_symbols() {
if (g_symInitialized) return;
// FAIL_CRITICAL_ERRORS + NO_PROMPTS: never pop dialogs.
// OMAP_FIND_NEAREST: BBT-optimized binaries.
// fInvadeProcess=TRUE + DEFERRED_LOADS: enumerate now, map PDBs on first use.
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS |
SYMOPT_OMAP_FIND_NEAREST | SYMOPT_FAIL_CRITICAL_ERRORS |
SYMOPT_NO_PROMPTS | SYMOPT_INCLUDE_32BIT_MODULES);
if (SymInitialize(GetCurrentProcess(), nullptr, TRUE)) {
g_symInitialized = true;
SymRefreshModuleList(GetCurrentProcess());
}
}
// Suppress symbol name when SymFromAddr falls back to a distant match
// 64KB is generous for one function; typical codegen funcs are under 4KB.
static constexpr DWORD64 kMaxTrustedSymbolOffset = 0x10000;
static void print_module_fallback(FILE *out, HANDLE proc, void *addr, DWORD64 addr64) {
IMAGEHLP_MODULE64 modInfo;
memset(&modInfo, 0, sizeof(modInfo));
modInfo.SizeOfStruct = sizeof(modInfo);
if (SymGetModuleInfo64(proc, addr64, &modInfo)) {
DWORD64 moduleOffset = addr64 - modInfo.BaseOfImage;
fprintf(out, " %p %s+0x%llx\n",
addr, modInfo.ModuleName, (unsigned long long)moduleOffset);
} else {
fprintf(out, " %p ?\n", addr);
}
}
static void print_frame(FILE *out, void *addr) {
HANDLE proc = GetCurrentProcess();
DWORD64 addr64 = (DWORD64)addr;
char symbuf[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(char)];
SYMBOL_INFO *sym = (SYMBOL_INFO*)symbuf;
sym->SizeOfStruct = sizeof(SYMBOL_INFO);
sym->MaxNameLen = MAX_SYM_NAME;
DWORD64 disp = 0;
bool haveSym = SymFromAddr(proc, addr64, &disp, sym) != FALSE;
if (!haveSym || disp >= kMaxTrustedSymbolOffset) {
// Distant symbol match is likely misattributed module+offset is more useful.
print_module_fallback(out, proc, addr, addr64);
return;
}
const char *name = sym->Name;
IMAGEHLP_LINE64 line;
memset(&line, 0, sizeof(line));
line.SizeOfStruct = sizeof(line);
DWORD lineDisp = 0;
if (SymGetLineFromAddr64(proc, addr64, &lineDisp, &line)) {
fprintf(out, " %p %s+0x%llx %s:%lu\n",
addr, name, (unsigned long long)disp, line.FileName, (unsigned long)line.LineNumber);
} else {
fprintf(out, " %p %s+0x%llx\n",
addr, name, (unsigned long long)disp);
}
}
#else
static void init_symbols() {}
static void print_frame(FILE *out, void *addr) {
#if defined(__linux__) || defined(__APPLE__)
Dl_info info;
if (dladdr(addr, &info) && info.dli_sname) {
int status = 0;
char *demangled = abi::__cxa_demangle(info.dli_sname, nullptr, nullptr, &status);
const char *name = (status == 0 && demangled) ? demangled : info.dli_sname;
uintptr_t offset = (uintptr_t)addr - (uintptr_t)info.dli_saddr;
fprintf(out, " %p %s+0x%lx (%s)\n",
addr, name, (unsigned long)offset, info.dli_fname ? info.dli_fname : "?");
std::free(demangled);
} else {
fprintf(out, " %p ?\n", addr);
}
#else
fprintf(out, " %p\n", addr);
#endif
}
#endif
// ------------------------- Dump -------------------------
struct Group {
uint64_t hash;
size_t count;
size_t totalBytes;
size_t minSize;
size_t maxSize;
void * samplePtr;
int frameCount;
void * frames[kFrames];
};
static uint64_t hash_frames(void * const *frames, int n) {
uint64_t h = 0xcbf29ce484222325ULL;
for (int i = 0; i < n; ++i) {
h ^= (uint64_t)(uintptr_t)frames[i];
h *= 0x100000001b3ULL;
}
return h;
}
static void format_with_commas(char *buf, size_t buflen, uint64_t v) {
char tmp[32];
int n = snprintf(tmp, sizeof(tmp), "%llu", (unsigned long long)v);
int out = 0;
for (int i = 0; i < n && out < (int)buflen - 1; ++i) {
int remaining = n - i;
if (i > 0 && (remaining % 3) == 0) {
if (out < (int)buflen - 1) buf[out++] = ',';
}
buf[out++] = tmp[i];
}
buf[out] = 0;
}
size_t dump_alloc_leaks(FILE *out) {
// Disarm so fprintf/Sym*/etc don't churn the map during the dump.
bool was_armed = g_armed.exchange(false);
(void)was_armed;
tl_inside = true;
auto t0 = std::chrono::steady_clock::now();
// Snapshot under lock; std::malloc keeps storage out of the tracker.
Group *groups = nullptr;
size_t groupCount = 0;
size_t groupCap = 0;
size_t totalLeaks = 0;
size_t totalBytes = 0;
size_t largestSingle = 0;
bool groupsExhausted = false;
{
std::lock_guard lock(getMutex());
LeakMap &m = getMap();
for (size_t i = 0; i < m.capacity; ++i) {
void *k = m.entries[i].key;
if (k == nullptr || k == kTombstone) continue;
const AllocInfo &info = m.entries[i].info;
++totalLeaks;
totalBytes += info.size;
if (info.size > largestSingle) largestSingle = info.size;
if (groupsExhausted) continue;
uint64_t h = hash_frames(info.frames, info.frameCount);
Group *g = nullptr;
for (size_t j = 0; j < groupCount; ++j) {
if (groups[j].hash == h) { g = &groups[j]; break; }
}
if (!g) {
if (groupCount == groupCap) {
size_t newCap = groupCap ? groupCap * 2 : 64;
Group *resized = (Group*)std::realloc(groups, newCap * sizeof(Group));
if (!resized) {
// OOM mid-shutdown: keep counting totals, drop new sites.
groupsExhausted = true;
continue;
}
groups = resized;
groupCap = newCap;
}
g = &groups[groupCount++];
g->hash = h;
g->count = 0;
g->totalBytes = 0;
g->minSize = SIZE_MAX;
g->maxSize = 0;
g->samplePtr = k;
g->frameCount = info.frameCount;
memcpy(g->frames, info.frames, info.frameCount * sizeof(void*));
}
g->count += 1;
g->totalBytes += info.size;
if (info.size < g->minSize) g->minSize = info.size;
if (info.size > g->maxSize) g->maxSize = info.size;
}
}
// Sort: totalBytes desc, count desc, hash for stability. Insertion sort
// groupCount is typically small.
for (size_t i = 1; i < groupCount; ++i) {
Group key = groups[i];
size_t j = i;
while (j > 0) {
const Group &a = groups[j - 1];
bool before = (a.totalBytes < key.totalBytes) ||
(a.totalBytes == key.totalBytes && a.count < key.count) ||
(a.totalBytes == key.totalBytes && a.count == key.count && a.hash < key.hash);
if (!before) break;
groups[j] = groups[j - 1];
--j;
}
groups[j] = key;
}
if (totalLeaks == 0) {
std::free(groups);
tl_inside = false;
return 0;
}
char b1[32], b2[32], b3[32];
format_with_commas(b1, sizeof(b1), totalLeaks);
format_with_commas(b2, sizeof(b2), totalBytes);
format_with_commas(b3, sizeof(b3), largestSingle);
fprintf(out, "\n=== daslang C++ heap leak report ===\n");
fprintf(out, "Total live allocations: %s\n", b1);
fprintf(out, "Total bytes: %s\n", b2);
fprintf(out, "Distinct leak sites: %zu\n", groupCount);
fprintf(out, "Largest single alloc: %s bytes\n", b3);
uint64_t orphan = g_orphan_free.load(std::memory_order_relaxed);
if (orphan) {
fprintf(out, "Orphan frees (pre-arm or cross-module): %llu\n",
(unsigned long long)orphan);
}
if (groupsExhausted) {
fprintf(out, "WARNING: realloc() failed during grouping; per-site detail truncated.\n"
" Totals above are accurate; only the leak-site breakdown is partial.\n");
}
if (groupCount > 0) {
init_symbols();
for (size_t i = 0; i < groupCount; ++i) {
const Group &g = groups[i];
uint64_t mean = g.count ? g.totalBytes / g.count : 0;
char bc[32], bt[32], bmn[32], bmx[32], bavg[32];
format_with_commas(bc, sizeof(bc), g.count);
format_with_commas(bt, sizeof(bt), g.totalBytes);
format_with_commas(bmn, sizeof(bmn), g.minSize);
format_with_commas(bmx, sizeof(bmx), g.maxSize);
format_with_commas(bavg,sizeof(bavg),mean);
fprintf(out, "\n--- Leak site #%zu: %s allocs, %s bytes (min %s, max %s, mean %s) ---\n",
i + 1, bc, bt, bmn, bmx, bavg);
fprintf(out, " sample ptr: %p\n", g.samplePtr);
for (int j = 0; j < g.frameCount; ++j) {
print_frame(out, g.frames[j]);
}
}
}
auto t1 = std::chrono::steady_clock::now();
double secs = std::chrono::duration(t1 - t0).count();
fprintf(out, "\n=== End report (%s leaks, %zu sites, symbolized in %.2fs) ===\n",
b1, groupCount, secs);
fflush(out);
std::free(groups);
tl_inside = false;
// intentionally leave disarmed
return totalLeaks;
}
// ------------------------- Atexit registration -------------------------
// Idempotence guard against explicit main.cpp dump call + atexit firing.
static std::atomic g_dumped{false};
// In ast_module.cpp: ++Module::Initialize, --Module::Shutdown. Nonzero at
// exit means cleanup was bypassed (e.g. dastest's fio::exit), so live
// allocations may still be owned and the dump is suppressed.
extern std::atomic g_envTotal;
static void dump_alloc_leaks_atexit() {
if (g_dumped.exchange(true, std::memory_order_acq_rel)) return;
int pending = g_envTotal.load(std::memory_order_relaxed);
if (pending > 0) {
fprintf(stderr,
"\n=== daslang C++ heap leak report SKIPPED ===\n"
"Module::Shutdown() was not called (%d environment(s) still active).\n"
"Likely cause: the process exited via exit()/abort() bypassing cleanup.\n"
"Live allocations are not reported because they may still be owned.\n",
pending);
fflush(stderr);
return;
}
uint64_t leaked = dump_alloc_leaks(stderr);
// The lexer's per-NAME-token strings are a bounded compile-time
// retention, not a runtime leak; let embedders opt out of this dump.
#if !defined(DAS_DONT_REPORT_LEXER_LEAKS)
dump_lexer_string_leaks(stderr);
#endif
if (leaked > 0) {
// Surface leaks as non-zero exit so CI / scripts can detect.
// _Exit skips remaining atexit handlers (already dumped what we need).
fflush(stderr);
std::_Exit(1);
}
}
struct RegisterLeakDumpAtExit {
RegisterLeakDumpAtExit() noexcept { std::atexit(&dump_alloc_leaks_atexit); }
};
static RegisterLeakDumpAtExit g_register_leak_dump_atexit;
} // namespace das
#endif // DAS_TRACK_ALLOC