diff --git a/modules/dasHV/CMakeLists.txt b/modules/dasHV/CMakeLists.txt index d389bcb4a0..1cedd6e1d0 100644 --- a/modules/dasHV/CMakeLists.txt +++ b/modules/dasHV/CMakeLists.txt @@ -214,7 +214,7 @@ IF ((NOT DAS_HV_INCLUDED) AND ((NOT ${DAS_HV_DISABLED}) OR (NOT DEFINED DAS_HV_D ADD_MODULE_CPP(HV) ADD_MODULE_LIB(libDasModuleHV dasModuleHV ${DAS_HV_MODULE_SRC} ${DAS_HV_MODULE_PLATFORM_SRC}) MACRO(SETUP_HV lib) - TARGET_INCLUDE_DIRECTORIES(${lib} PRIVATE "${DAS_HV_DIR}/hv/$/include") + TARGET_INCLUDE_DIRECTORIES(${lib} SYSTEM PRIVATE "${DAS_HV_DIR}/hv/$/include") TARGET_LINK_LIBRARIES(${lib} PRIVATE ${HV_LIBRARIES} ${OPENSSL_LIBRARIES_FILES}) ADD_DEPENDENCIES(${lib} LIBHV) SETUP_CPP11(${lib}) diff --git a/modules/dasHV/src/dasHV.cpp b/modules/dasHV/src/dasHV.cpp index e689b9f2d6..a62fa4a90f 100644 --- a/modules/dasHV/src/dasHV.cpp +++ b/modules/dasHV/src/dasHV.cpp @@ -8,6 +8,7 @@ #include "dasHV.h" #include +#include IMPLEMENT_EXTERNAL_TYPE_FACTORY(WebSocketClient,hv::WebSocketClient) IMPLEMENT_EXTERNAL_TYPE_FACTORY(WebSocketServer,hv::WebSocketServer) @@ -1701,6 +1702,9 @@ class Module_HV : public Module { ->args({"server","writer"}); } + ~Module_HV() { + hv::async::cleanup(); + } virtual ModuleAotType aotRequire ( TextWriter & tw ) const override { tw << "#include \"../modules/dasHV/src/aot_hv.h\"\n"; return ModuleAotType::cpp; diff --git a/src/misc/ARCHITECTURE.md b/src/misc/ARCHITECTURE.md index 1fd73bf1e3..652944e70d 100644 --- a/src/misc/ARCHITECTURE.md +++ b/src/misc/ARCHITECTURE.md @@ -6,6 +6,9 @@ - `sysos.cpp` - the per-platform core-count probes `job_que.cpp` calls. - `network.cpp` - the single-client TCP `Server` the DAP debugger and `daslib/network` sit on, and the two helpers every socket error passes through. +- `alloc_tracker.cpp` - the RelWithDebInfo C++ heap leak tracker: the live-allocation map, the + exit-time report, and the per-frame symbolizer. `alloc_tracker_overrides.cpp` beside it carries + the global `operator new`/`delete` that feed it, compiled into every binary and shared module. The knobs are bound to daslang in `src/builtin/module_builtin_jobque.cpp`; each knob's caller contract is stated on its declaration in `include/daScript/misc/job_que.h`. @@ -63,3 +66,32 @@ a disconnected client would retry forever while holding the debug-agent context tick that notices the closed socket could never run. `REVIEW.das` beside this file fails a `network.cpp` that reads `errno` outside `last_socket_error()`, or names a would-block code outside `socket_would_block()`. + +## 6. The leak dump runs last, so a static dtor's free is not a leak + +`alloc_tracker.cpp` reports from an `atexit` handler, and heap that some other static destructor +is about to free is indistinguishable from a leak while that destructor has not run. The handler +therefore has to be registered FIRST, because `atexit` runs LIFO: `#pragma init_seg(lib)` puts the +registrar's constructor ahead of every user-level static on MSVC, and +`__attribute__((init_priority(101)))` does the same everywhere else. Losing that ordering does not +lose a leak, it invents one - every process-lifetime cache in the runtime (the dasbind late-bind +map, the dynamic-module registries, the JIT parallel-emit job vector) is freed by a static dtor or +by `Module::Shutdown`, so a dump that runs before them reports the whole set. A toolchain with +neither mechanism keeps the old ordering and over-reports; nothing else breaks. + +What makes running last SAFE is that the tracker owns no destructible state: `getMap` and +`getMutex` placement-new into static storage and are never destructed, so a `track_free_hook` +arriving during static teardown - after the dump, at any point - still has a live map to tombstone +into. + +## 7. A frame prints at the best tier that resolved, never as nothing + +A frame falls back rather than vanishing: the symbol name plus its offset when the platform +resolver found one, else the module plus the frame's offset from its load base, else `?`. That +middle tier is what carries a POSIX build - `dladdr` reads `.dynsym` only, so every static and +hidden-visibility function in the runtime resolves to no symbol, and a report that printed `?` for +those would hide most of its own stacks behind manual base-address arithmetic. With the module and +offset in hand, `addr2line -f -C -e ` is the whole recovery. The MSVC arm carries +one tier the POSIX arm does not: it distrusts a symbol whose offset exceeds +`kMaxTrustedSymbolOffset`, because `SymFromAddr` answers with a distant neighbour where `dladdr` +answers with nothing. diff --git a/src/misc/alloc_tracker.cpp b/src/misc/alloc_tracker.cpp index 32dd7491ee..bc13c7567d 100644 --- a/src/misc/alloc_tracker.cpp +++ b/src/misc/alloc_tracker.cpp @@ -24,7 +24,7 @@ #include #endif -// init_seg(lib) registers our atexit handler before any user-level static +// init_seg(lib) / init_priority put 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) @@ -32,6 +32,13 @@ #pragma warning(disable: 4073) #pragma init_seg(lib) #pragma warning(pop) +#elif defined(__has_attribute) + #if __has_attribute(init_priority) + #define DAS_LEAK_DUMP_EARLY_INIT __attribute__((init_priority(101))) + #endif +#endif +#ifndef DAS_LEAK_DUMP_EARLY_INIT + #define DAS_LEAK_DUMP_EARLY_INIT #endif namespace das { @@ -296,7 +303,7 @@ static void init_symbols() {} static void print_frame(FILE *out, void *addr) { #if defined(__linux__) || defined(__APPLE__) - Dl_info info; + Dl_info info = {}; if (dladdr(addr, &info) && info.dli_sname) { int status = 0; char *demangled = abi::__cxa_demangle(info.dli_sname, nullptr, nullptr, &status); @@ -305,6 +312,9 @@ static void print_frame(FILE *out, void *addr) { fprintf(out, " %p %s+0x%lx (%s)\n", addr, name, (unsigned long)offset, info.dli_fname ? info.dli_fname : "?"); std::free(demangled); + } else if (info.dli_fbase) { + fprintf(out, " %p %s+0x%lx\n", addr, info.dli_fname ? info.dli_fname : "?", + (unsigned long)((uintptr_t)addr - (uintptr_t)info.dli_fbase)); } else { fprintf(out, " %p ?\n", addr); } @@ -530,7 +540,7 @@ static void dump_alloc_leaks_atexit() { struct RegisterLeakDumpAtExit { RegisterLeakDumpAtExit() noexcept { std::atexit(&dump_alloc_leaks_atexit); } }; -static RegisterLeakDumpAtExit g_register_leak_dump_atexit; +static RegisterLeakDumpAtExit g_register_leak_dump_atexit DAS_LEAK_DUMP_EARLY_INIT; } // namespace das diff --git a/utils/daslang-live/main.cpp b/utils/daslang-live/main.cpp index 8ffce2c695..539fcb8c41 100644 --- a/utils/daslang-live/main.cpp +++ b/utils/daslang-live/main.cpp @@ -1031,8 +1031,8 @@ int main(int argc, char * argv[]) { if ( dumpLeaks ) { JobStatus::DumpJobQueLeaks(); } - // das::dump_alloc_leaks is registered as an atexit handler via init_seg(lib), - // so it fires after all static destructors — cleaner than dumping here. + // das::dump_alloc_leaks registers itself as the FIRST atexit handler, so it + // fires after all static destructors — cleaner than dumping here. release_single_instance(); return result; } diff --git a/utils/daslang/main.cpp b/utils/daslang/main.cpp index 7ab98fd4ea..d39d70d95c 100644 --- a/utils/daslang/main.cpp +++ b/utils/daslang/main.cpp @@ -1125,8 +1125,8 @@ int MAIN_FUNC_NAME ( int argc, char * argv[] ) { if ( dumpLeaks ) { JobStatus::DumpJobQueLeaks(); } - // das::dump_alloc_leaks is registered as an atexit handler via init_seg(lib), - // so it fires after all static destructors — cleaner than dumping here. + // das::dump_alloc_leaks registers itself as the FIRST atexit handler, so it + // fires after all static destructors — cleaner than dumping here. if ( g_smart_ptr_total!=0 ) { // The exit is unconditional but the explanation used to sit inside `if (dumpLeaks)`, so // `-no-dump-leaks` turned this into a bare exit(1) -- indistinguishable from a script that