|
| 1 | +"""Activator for native (C/C++) heap allocation profiling via GOT rewriting. |
| 2 | +
|
| 3 | +This module dlopen's libdatadog's ``libdd-profiling-heap-gotter-ffi`` cdylib |
| 4 | +(staged as ``liblibdd_profiling_heap_gotter_ffi<EXT_SUFFIX>.so``; see |
| 5 | +``src/native_heap_gotter/``) and drives it through the shared libdatadog C ABI: |
| 6 | +
|
| 7 | + ddog_VoidResult ddog_heap_gotter_install(void); |
| 8 | + bool ddog_heap_gotter_is_installed(void); |
| 9 | +
|
| 10 | +Calling ``install()`` patches the process's GOT entries for heap allocation |
| 11 | +symbols so that Datadog's ``ddheap:alloc`` (Phase 1: allocation-only) USDT probe |
| 12 | +sites fire on sampled allocations. The OpenTelemetry eBPF profiler or Datadog |
| 13 | +Host Profiler then attaches uprobes to those sites to collect native allocation |
| 14 | +flamegraphs. There is nothing to collect or upload from the Python side — this |
| 15 | +only *arms* the probes. |
| 16 | +
|
| 17 | +Fail-closed by design: if the cdylib is missing (the default, since it only |
| 18 | +ships when built with ``DD_PROFILING_NATIVE_HEAP_BUILD=1``) or anything goes |
| 19 | +wrong loading it, ``is_available`` is ``False`` and ``install()`` is a no-op |
| 20 | +returning ``False``. Loading this module must never raise. |
| 21 | +
|
| 22 | +Permanence: installation cannot be undone (the patched GOT entries point at |
| 23 | +functions inside the cdylib), so the library must stay mapped for the life of |
| 24 | +the process. We keep the ``ctypes.CDLL`` handle at module scope and never unload |
| 25 | +it. After ``fork()`` the child inherits both the mapping and the patched GOT, so |
| 26 | +a re-install in the child is a harmless idempotent no-op. |
| 27 | +""" |
| 28 | + |
| 29 | +from __future__ import annotations |
| 30 | + |
| 31 | +import ctypes |
| 32 | +import os |
| 33 | +import sysconfig |
| 34 | + |
| 35 | + |
| 36 | +# Upstream libdatadog FFI artifact base name (double-`lib` prefix). |
| 37 | +_LIBRARY_BASENAME = "liblibdd_profiling_heap_gotter_ffi" |
| 38 | + |
| 39 | +# Mirror cbindgen tags for ddog_VoidResult (common.h). |
| 40 | +DDOG_VOID_RESULT_OK = 0 |
| 41 | +DDOG_VOID_RESULT_ERR = 1 |
| 42 | + |
| 43 | + |
| 44 | +class _DdogVecU8(ctypes.Structure): |
| 45 | + _fields_ = [ |
| 46 | + ("ptr", ctypes.c_void_p), |
| 47 | + ("len", ctypes.c_size_t), |
| 48 | + ("capacity", ctypes.c_size_t), |
| 49 | + ] |
| 50 | + |
| 51 | + |
| 52 | +class _DdogError(ctypes.Structure): |
| 53 | + _fields_ = [("message", _DdogVecU8)] |
| 54 | + |
| 55 | + |
| 56 | +class _DdogVoidResult(ctypes.Structure): |
| 57 | + _fields_ = [ |
| 58 | + ("tag", ctypes.c_uint32), |
| 59 | + ("err", _DdogError), |
| 60 | + ] |
| 61 | + |
| 62 | + |
| 63 | +# Mirror the ddup/stack modules: importers (notably settings/profiling.py) read |
| 64 | +# these two attributes to decide whether the feature can run. |
| 65 | +is_available: bool = False |
| 66 | +failure_msg: str = "" |
| 67 | + |
| 68 | +_lib: ctypes.CDLL | None = None # kept alive for process lifetime; never dlclose'd |
| 69 | + |
| 70 | + |
| 71 | +def _library_path() -> str: |
| 72 | + suffix: str = sysconfig.get_config_var("EXT_SUFFIX") or ".so" |
| 73 | + profiling_dir: str = os.path.dirname(os.path.dirname(__file__)) |
| 74 | + return os.path.join(profiling_dir, _LIBRARY_BASENAME + suffix) |
| 75 | + |
| 76 | + |
| 77 | +def _void_result_ok(result: _DdogVoidResult) -> bool: |
| 78 | + if result.tag == DDOG_VOID_RESULT_OK: |
| 79 | + return True |
| 80 | + if _lib is not None: |
| 81 | + try: |
| 82 | + _lib.ddog_Error_drop(ctypes.byref(result.err)) |
| 83 | + except Exception: # nosec: B110 |
| 84 | + pass |
| 85 | + return False |
| 86 | + |
| 87 | + |
| 88 | +try: |
| 89 | + # Native heap profiling via the gotter is Linux-only; on every other |
| 90 | + # platform the underlying library is a no-op, so don't even try to load. |
| 91 | + if os.name != "posix" or os.uname().sysname != "Linux": |
| 92 | + raise OSError("native heap gotter is only supported on Linux") |
| 93 | + |
| 94 | + _path: str = _library_path() |
| 95 | + if not os.path.exists(_path): |
| 96 | + raise FileNotFoundError(_path) |
| 97 | + |
| 98 | + # RTLD_GLOBAL so the loaded code is unambiguously resolvable; RTLD_NOW so any |
| 99 | + # unresolved symbol fails here (fail-closed) rather than at first call. |
| 100 | + _lib = ctypes.CDLL(_path, mode=ctypes.RTLD_GLOBAL | getattr(os, "RTLD_NOW", 0)) |
| 101 | + |
| 102 | + _lib.ddog_heap_gotter_install.argtypes = [] |
| 103 | + _lib.ddog_heap_gotter_install.restype = _DdogVoidResult |
| 104 | + _lib.ddog_heap_gotter_is_installed.argtypes = [] |
| 105 | + _lib.ddog_heap_gotter_is_installed.restype = ctypes.c_bool |
| 106 | + _lib.ddog_Error_drop.argtypes = [ctypes.POINTER(_DdogError)] |
| 107 | + _lib.ddog_Error_drop.restype = None |
| 108 | + |
| 109 | + is_available = True |
| 110 | + |
| 111 | +except Exception as e: |
| 112 | + failure_msg = str(e) |
| 113 | + _lib = None |
| 114 | + |
| 115 | + |
| 116 | +def install() -> bool: |
| 117 | + """Install the native heap GOT overrides. Returns True if now installed. |
| 118 | +
|
| 119 | + Idempotent and safe to call more than once (e.g. after fork). No-op that |
| 120 | + returns False when the cdylib is unavailable. |
| 121 | + """ |
| 122 | + if not is_available or _lib is None: |
| 123 | + return False |
| 124 | + try: |
| 125 | + return _void_result_ok(_lib.ddog_heap_gotter_install()) |
| 126 | + except Exception: |
| 127 | + return False |
| 128 | + |
| 129 | + |
| 130 | +def is_installed() -> bool: |
| 131 | + """Return whether native heap GOT overrides are currently installed.""" |
| 132 | + if not is_available or _lib is None: |
| 133 | + return False |
| 134 | + try: |
| 135 | + return bool(_lib.ddog_heap_gotter_is_installed()) |
| 136 | + except Exception: |
| 137 | + return False |
0 commit comments