6.9. Memory Leak Detection — Diagnostics Cheat Sheet
daslang ships six distinct leak-detection mechanisms, each narrow in scope. This page is the umbrella: which one to pick, how to invoke it, and how to read the output. Each mechanism has its own in-depth page or skill file linked at the end of the section.
6.9.1. At a glance
# |
Mechanism |
Scope |
Invoke |
|---|---|---|---|
1 |
daslang heap, per-allocation with captured call stack |
|
|
2 |
Context heap + string heap, all blocks alive at exit |
|
|
3 |
AST nodes outliving compile or execution |
automatic (every run) |
|
4 |
one specific |
|
|
5 |
threading primitives with manual refcount |
|
|
6 |
HandleRegistry (dasHV) |
value-sized handles (WebSocket client/server/channel) |
automatic |
6.9.2. Picking the right tool
If you see multiple reports at exit, fix in this order: #3 (gc_node) first (any survivor indicates an ownership bug that can cascade), then #5 (threading primitives), then #1 or #2 (heap), then #4 (smart_ptr) and #6 (dasHV handles) if relevant.
daslang run exits 0 but prints
GC COMPILE LEAK/GC APP LEAK— jump to 3. gc_node leak detection (automatic).You need the call stack for every leaked block in a daslang program — use 1. daslang leak profiler (--das-profiler-leaks).
You want a quick per-block survey (sizes, ids, daslang source locations) — use 2. C++ heap report (-track-allocations -heap-report).
You already know an object id from a prior dump and want a debug break on every refcount bump — use 4. Smart-pointer tracking (--track-smart-ptr <hexId>).
You use
daslib/jobque/daslib/jobque_boost/ channels / streams and the exit banner listsJobStatus/Channel/LockBoxsurvivors — jump to 5. JobStatus / Channel / LockBox tracker.Long-running dasHV server, suspected WebSocket-client handle leak — see 6. HandleRegistry (dasHV handle objects).
6.9.3. 1. daslang leak profiler (--das-profiler-leaks)
Records every live daslang-side heap allocation with the full daslang call stack, and dumps them on context destroy sorted by size.
daslang --das-profiler --das-profiler-leaks path/to/script.das
Sample output:
=== Memory leaks in context '<unnamed>' (3 allocations, 0x2b0 bytes) ===
[leak] size=0x200 bytes
at builtin`push`4379756157886752001 daslib/builtin.das:117
at make_big_leak examples/leak_smoke.das:14
at main examples/leak_smoke.das:37
Sizes are in hex. Stack frames are in leaf-first order, matching panic().
The file:line portion is standalone so the VSCode terminal auto-links
to the source line. Multi-context programs produce one block per live
context at shutdown.
Full guide: Profiler — Runtime Profiling.
6.9.4. 2. C++ heap report (-track-allocations -heap-report)
Enables per-block origin tracking at runtime and prints each Context’s heap and string-heap state at exit.
daslang -track-allocations -heap-report path/to/script.das
Note
These two flags use a single leading dash, not two — the CLI is
historically inconsistent about leading-dash count across flags.
--track-smart-ptr, --track-job-status, and
--das-profiler-leaks all use two.
Sample output with the default linear heap (one line per chunk):
--- heap report ---
2b0202057 944 of 65536
--- string heap report ---
2b0191a05 52 of 65536
Columns: chunk address, bytes allocated, chunk size. Linear heap reset is bulk — you don’t see individual blocks here.
With options persistent_heap = true (or
-Dpersistent_heap=true as a policy), the persistent allocator reports
individual blocks (big stuff) and slab occupancy (decks):
--- heap report ---
big stuff:
size pointer id
512 0x2069eeae630 1 array
48 0x2069ece4c20 2 new [[ ]] D:/script.das:22:12
128 0x2069f1f4fa0 3 array
bytes per location:
48 D:/script.das:22:12
Rows: size in bytes, pointer, per-heap sequential id, an optional
comment stamped by the runtime helper (array, new [[ ]], table,
etc.), and the LineInfo of the call site if available. The
“bytes per location” section aggregates totals by file:line.
Build-time gate. Tracking is compiled into every build by default via
DAS_TRACK_ALLOCATIONS=1 in include/daScript/misc/platform.h. Shipping
builds can set -DDAS_TRACK_ALLOCATIONS=0 to dead-code-eliminate the
infrastructure. The runtime flag (-track-allocations) is what turns
tracking on in a normal build — without it the heap still reports
“decks” occupancy but “big stuff” blocks don’t carry id/comment/
file:line.
Compare with #1. #1 shows every live block with a full call stack; #2 shows every live block with a single LineInfo but also shows slab occupancy. Use #1 when you need “how did this get allocated”, #2 when you want “what’s alive and roughly from where”.
6.9.5. 3. gc_node leak detection (automatic)
gc_node is the ownership mechanism for AST types: TypeDecl,
Expression, Function, Structure, Enumeration, Variable,
MakeFieldDecl, MakeStruct, and their subclasses. Every allocation
links into a per-thread GC root list; leaks are any survivors at compile or
app exit.
No flag needed — daslang.exe and daslang-live.exe automatically
check gc_root::gc_get_thread_root().gc_count at two points and dump any
survivors:
``GC COMPILE LEAK: N gc_node(s) after compile`` — the compilation pass finished but some AST nodes from that pass are still referenced.
``GC APP LEAK: N gc_node(s) after execution`` —
mainreturned but runtime code allocated AST nodes (usually viaclone_type/new TypeDecl/qmacroin daslang) that nothing cleaned up.
Sample output:
GC APP LEAK: 3 gc_node(s) after execution
gc_root 0x7ff...: count=3
node 0x7ff...: id=1234 type=TypeDecl magic=0xDA5C0001
node 0x7ff...: id=1235 type=Expression magic=0xDA5C0002
Narrow it down with the env var. Pick an id from the report and rerun
under a debugger with DAS_GC_BREAK_ON_ID set:
DAS_GC_BREAK_ON_ID=1234 daslang path/to/script.das
The gc_node constructor calls os_debug_break() when it allocates a
node whose id matches — you get the full C++ + daslang stack at the
creation site.
Common fix. daslang tools/utilities that build AST nodes at runtime need
ast_gc_guard() { ... } around the scope. See the gc_migration skill
for the full ownership story.
6.9.6. 4. Smart-pointer tracking (--track-smart-ptr <hexId>)
ptr_ref_count is the base class for daslang’s refcounted smart pointers
(Context, Program, FileAccess, compiler analyses, etc.). Each
instance has a unique ref_count_id and links into a global list
(ref_count_head).
At exit daslang.exe calls ptr_ref_count::DumpTrackPtr() which
lists every survivor:
0x7ffee1301000 (rc=2, id=5a) Context main_ctx
0x7ffee1301100 (rc=1, id=5b) Program
total 2 tracked pointers
Pick an id from there and rerun with --track-smart-ptr <hexId>:
daslang --track-smart-ptr 0x5a path/to/script.das
addRef, delRef, and the destructor on that specific id call
os_debug_break(). Attach a debugger (or --das-wait-debugger) to
collect stack traces for each refcount bump. This makes it easy to find the
site that’s holding on too long.
A second static (ref_count_track_destructor) breaks only on the
destructor call. It is not exposed as a CLI flag; set it from the debugger
if you need to distinguish “who destructed it” from “who bumped it”.
6.9.7. 5. JobStatus / Channel / LockBox tracker
Threading primitives (JobStatus and subclasses Channel and
LockBox, plus the Feature value-type) have their own manual
refcount system. DumpJobQueLeaks() runs automatically at exit and
lists survivors with subtype and created at source location.
daslang --track-job-status <id> path/to/script.das
The --track-job-status <id> flag traces every addRef / releaseRef on one
specific object with the source location of each call. The workflow is
essentially identical to #4 but specialized for these threading types —
they have their own exit dump format and their own narrower trace output.
Full workflow (refcount accounting, shutdown order, lockbox fill/grab/join
lifecycle, capture-macro hidden refs): see
skills/jobque_debugging.md in the source tree.
6.9.8. 6. HandleRegistry (dasHV handle objects)
dasHV exposes C++ objects such as hv::WebSocketClient,
hv::WebSocketServer, and hv::WebSocketChannel to daslang as
value-sized Handle<T> integers backed by
HandleRegistry<T>::instance() — a generation-tagged slot table owning
a std::shared_ptr<T> per handle. The registry is defined in
include/daScript/misc/handle_registry.h.
Automatic dump at process exit. handleRegistry_dumpAll() runs
inside Module::Shutdown(dumpLeaks) in both daslang.exe and
daslang-live.exe, in the window between the module destructor loop
(which drains job threads via Module_JobQue::~Module_JobQue) and
the DynamicModuleInfo teardown that unloads shared modules. That
ordering is deliberate: before the window, live job threads legitimately
hold handles; after it, the dumpHandleLeaks<T> function pointers
registered from shared-module DLLs are dangling. Every handle type
registered via addHandleAnnotation<T> auto-registers a per-type dump
callback that walks HandleRegistry<T>::instance() and reports any
live handles at that moment.
Sample output.
Handle<WebSocketClient> idx=3 gen=1 (rc=1)
Handle<WebSocketServer> idx=0 gen=9 (rc=1)
total 1 leaked handles of type WebSocketClient
total 1 leaked handles of type WebSocketServer
Columns are the slot index, generation counter (rolls on release and
reacquire), and shared_ptr::use_count() at dump time. rc=1 means
the registry is the sole owner — a classic forgotten-release bug.
rc>1 means another strong reference is keeping the object alive;
look for a forgotten capture.
How type names are resolved. The dumper calls typeName<T>::name(),
which is specialized by MAKE_EXTERNAL_TYPE_FACTORY(Name, hv::Name) in
modules/dasHV/src/dasHV.h. Adding a new handle type without
typeName<T> is a compile-time error on the first
addHandleAnnotation<T> call — by design.
Disabled modules cost nothing. When DAS_HV_DISABLED=ON the module
TUs are not compiled; no callbacks get registered; the dump iterates an
empty hooks vector.
Advisory, not fatal. The dump prints but does not change exit code
(matches DumpJobQueLeaks precedent). Only ptr_ref_count leaks
trigger exit(1).
Silencing all three exit-time dumps. Pass --no-dump-leaks to
daslang.exe or daslang-live.exe and the JobStatus, HandleRegistry,
and smart_ptr TextPrinter dumps all become quiet. The exit(1) on a
smart_ptr leak is preserved — it is a failure signal, not diagnostic
noise. Default is on.
Manual query. Still useful for in-process programmatic inspection — for example, a long-running server that wants to log its own handle census periodically:
auto & reg = HandleRegistry<hv::WebSocketClient>::instance();
if ( auto n = reg.live_count() ) {
reg.for_each_live([](Handle<hv::WebSocketClient> h, auto & p){
// log h.value, p.use_count(), p.get()
});
}
6.9.9. See also
Profiler — Runtime Profiling — the performance profiler + the daslang leak profiler in detail.
HTTP and WebSocket library (libhv) — dasHV module reference.
skills/memory_leak_detection.md— the compact version of this guide intended as a Claude reference.skills/jobque_debugging.md— full workflow for mechanism #5.skills/gc_migration.md— background for mechanism #3 and theast_gc_guardhelper.