[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/WhyNot135/daScript/master/src/misc/alloc_tracker.cpp [Back]  [Original]

#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

Web Proxy Viewer  |  New URL  |  Original Page