Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/dasHV/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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/$<CONFIG>/include")
TARGET_INCLUDE_DIRECTORIES(${lib} SYSTEM PRIVATE "${DAS_HV_DIR}/hv/$<CONFIG>/include")
TARGET_LINK_LIBRARIES(${lib} PRIVATE ${HV_LIBRARIES} ${OPENSSL_LIBRARIES_FILES})
ADD_DEPENDENCIES(${lib} LIBHV)
SETUP_CPP11(${lib})
Expand Down
4 changes: 4 additions & 0 deletions modules/dasHV/src/dasHV.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "dasHV.h"

#include <hv/hlog.h>
#include <hv/hasync.h>

IMPLEMENT_EXTERNAL_TYPE_FACTORY(WebSocketClient,hv::WebSocketClient)
IMPLEMENT_EXTERNAL_TYPE_FACTORY(WebSocketServer,hv::WebSocketServer)
Expand Down Expand Up @@ -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;
Expand Down
32 changes: 32 additions & 0 deletions src/misc/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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 <module> <offset>` 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.
16 changes: 13 additions & 3 deletions src/misc/alloc_tracker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,21 @@
#include <dlfcn.h>
#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)
#pragma warning(push)
#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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
Expand Down Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions utils/daslang-live/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
4 changes: 2 additions & 2 deletions utils/daslang/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading