11"""Activator for native (C/C++) heap allocation profiling via GOT rewriting.
22
3- This module dlopen's the `libdd_heap_gotter` cdylib (built out-of-band from
4- libdatadog's `libdd-profiling-heap-gotter-ffi`; see `src/native_heap_gotter`)
5- and drives it through a tiny, stable C ABI:
6-
7- bool ddtrace_heap_gotter_install(void); # install + report success
8- bool ddtrace_heap_gotter_is_installed(void); # current install state
9-
10- Calling `install()` patches the process's GOT entries for heap allocation
11- symbols so that Datadog's `ddheap:alloc` USDT probe sites fire on sampled allocations.
12- The Full Host eBPF profiler then attaches uprobes to those sites to collect native allocation stacks.
13-
14- If the cdylib is missing or anything goes wrong loading it, `is_available` is `False`
15- and `install()` is a no-op. Loading this module must never raise.
16-
17- Installation cannot be undone (the patched GOT entries point at functions inside the cdylib),
18- so the library must stay mapped for the life of the process. We keep the `ctypes.CDLL` handle
19- at module scope and never unload it.
20-
21- After a successful `install()`, a child of `fork()` inherits the mapping and the patched GOT.
22- Re-entering `install()` in that child is therefore unnecessary; we skip the native call when
23- this module already recorded a successful arm (Python module state is also inherited). That
24- avoids re-locking upstream's process-global registry mutex. Upstream does not yet implement a
25- `pthread_atfork` child reset, so forking *during* an in-flight `install()`/`update()` can still
26- leave that mutex locked in the child — prefer arming on the main thread, or after fork in the
27- worker (gunicorn/uWSGI-style), and treat mid-install fork as unsafe until libdatadog lands
28- atfork handling.
3+ Dlopens ``libdd_heap_gotter`` (see ``src/native_heap_gotter``) and drives:
4+
5+ bool ddtrace_heap_gotter_install(void);
6+ bool ddtrace_heap_gotter_is_installed(void);
7+ bool ddtrace_heap_gotter_live_heap_enabled(void); # True if built with ddheap:free
8+
9+ ``install()`` patches GOT entries so ``ddheap:alloc`` (and, on live-heap builds,
10+ ``ddheap:free``) USDT sites fire; the Full Host eBPF profiler attaches uprobes.
11+ Nothing is collected or uploaded from Python.
12+
13+ ``live_heap_enabled()`` is a compile-time property of the loaded artifact
14+ (default-on ``live-heap`` feature): True when the cdylib stamps retain flags and
15+ emits ``ddheap:free``. False if the cdylib is missing, alloc-only
16+ (``--no-default-features``), or predates this symbol (bound defensively).
17+
18+ Missing/broken load → ``is_available`` False, ``install()`` no-op; import never
19+ raises. Install is permanent (GOT points into the cdylib), so the CDLL handle
20+ stays mapped for the process lifetime.
21+
22+ After a successful ``install()``, fork children inherit the mapping and patched
23+ GOT; ``_armed`` skips re-entering the native installer. Upstream has no
24+ ``pthread_atfork`` reset for its registry mutex — prefer arming on the main
25+ thread or in the worker after fork; mid-install fork is unsafe.
2926"""
3027
3128from __future__ import annotations
3532import sysconfig
3633
3734
38- # Mirror the ddup/stack modules: importers (notably settings/profiling.py) read
39- # these two attributes to decide whether the feature can run.
35+ # Mirror ddup/stack: settings/profiling.py reads these to gate the feature.
4036is_available : bool = False
4137failure_msg : str = ""
4238
43- _lib : ctypes .CDLL | None = None # kept alive for process lifetime; never dlclose'd
44- # Set when install() has succeeded in this process (inherited across fork).
45- _armed : bool = False
39+ # Compile-time live-heap capability of the loaded artifact (see module docstring).
40+ _live_heap_available : bool = False
41+
42+ _lib : ctypes .CDLL | None = None # process lifetime; never dlclose'd
43+ _armed : bool = False # successful install(); inherited across fork
4644
4745
4846def _library_path () -> str :
49- # The cdylib is staged next to libdd_wrapper in the profiling package and
50- # carries the interpreter EXT_SUFFIX, matching setup.py's naming.
47+ # Staged next to libdd_wrapper with the interpreter EXT_SUFFIX (setup.py).
5148 suffix : str = sysconfig .get_config_var ("EXT_SUFFIX" ) or ".so"
5249 profiling_dir : str = os .path .dirname (os .path .dirname (__file__ ))
5350 return os .path .join (profiling_dir , "libdd_heap_gotter" + suffix )
5451
5552
5653try :
57- # Native heap profiling via the gotter is Linux-only; on every other
58- # platform the underlying library is a no-op, so don't even try to load.
54+ # Linux-only; elsewhere the gotter is a no-op.
5955 sysname = os .uname ().sysname if os .name == "posix" else os .name
6056 if sysname != "Linux" :
6157 raise OSError (f"Native heap gotter is only supported on Linux. Running on { sysname } " )
@@ -64,8 +60,7 @@ def _library_path() -> str:
6460 if not os .path .exists (_path ):
6561 raise FileNotFoundError (_path )
6662
67- # RTLD_GLOBAL so the loaded code is unambiguously resolvable; RTLD_NOW so any
68- # unresolved symbol fails here (fail-closed) rather than at first call.
63+ # RTLD_GLOBAL for resolvability; RTLD_NOW fail-closed on unresolved symbols.
6964 _lib = ctypes .CDLL (_path , mode = ctypes .RTLD_GLOBAL | getattr (os , "RTLD_NOW" , 0 ))
7065
7166 _lib .ddtrace_heap_gotter_install .argtypes = []
@@ -75,18 +70,25 @@ def _library_path() -> str:
7570
7671 is_available = True
7772
73+ # Optional symbol (pre-Phase-2 cdylibs); failure must not disable install().
74+ try :
75+ _lib .ddtrace_heap_gotter_live_heap_enabled .argtypes = []
76+ _lib .ddtrace_heap_gotter_live_heap_enabled .restype = ctypes .c_bool
77+ _live_heap_available = bool (_lib .ddtrace_heap_gotter_live_heap_enabled ())
78+ except Exception :
79+ _live_heap_available = False
80+
7881except Exception as e :
7982 failure_msg = str (e )
8083 _lib = None
8184
8285
8386def install () -> bool :
84- """Install the native heap GOT overrides. Returns True if now installed; False otherwise.
87+ """Install native heap GOT overrides. True if installed; False otherwise.
8588
86- Idempotent at the Python layer: once a call has succeeded, further calls
87- (including in a forked child that inherited ``_armed``) return True without
88- re-entering the native installer. No-op that returns False when the cdylib
89- is unavailable. See the module docstring for fork-safety limits.
89+ Idempotent at the Python layer: after success (including in a fork child that
90+ inherited ``_armed``), further calls return True without re-entering the native
91+ installer. See module docstring for fork-safety limits.
9092 """
9193 global _armed
9294 if not is_available or _lib is None :
@@ -112,3 +114,12 @@ def is_installed() -> bool:
112114 return bool (_lib .ddtrace_heap_gotter_is_installed ())
113115 except Exception :
114116 return False
117+
118+
119+ def live_heap_enabled () -> bool :
120+ """Return whether the loaded cdylib was built with live-heap tracking.
121+
122+ Compile-time property of the artifact (default-on feature). False when the
123+ cdylib is missing, alloc-only, or predates this symbol.
124+ """
125+ return _live_heap_available
0 commit comments