-
Notifications
You must be signed in to change notification settings - Fork 536
feat(profiling): native-heap live-heap cleanup (ABI, build env, auditwheel) (PROF-15423) #19325
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
50cbaab
0dd603e
7c41b5e
664c8fa
988abb1
7d2117f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
vlad-scherbich marked this conversation as resolved.
vlad-scherbich marked this conversation as resolved.
vlad-scherbich marked this conversation as resolved.
vlad-scherbich marked this conversation as resolved.
|
|
vlad-scherbich marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,31 +1,38 @@ | ||
| """Activator for native (C/C++) heap allocation profiling via GOT rewriting. | ||
|
|
||
| This module dlopen's the `libdd_heap_gotter` cdylib (built out-of-band from | ||
| libdatadog's `libdd-profiling-heap-gotter-ffi`; see `src/native_heap_gotter`) | ||
| and drives it through a tiny, stable C ABI: | ||
|
|
||
| bool ddtrace_heap_gotter_install(void); # install + report success | ||
| bool ddtrace_heap_gotter_is_installed(void); # current install state | ||
|
|
||
| Calling `install()` patches the process's GOT entries for heap allocation | ||
| symbols so that Datadog's `ddheap:alloc` USDT probe sites fire on sampled allocations. | ||
| The Full Host eBPF profiler then attaches uprobes to those sites to collect native allocation stacks. | ||
|
|
||
| If the cdylib is missing or anything goes wrong loading it, `is_available` is `False` | ||
| and `install()` is a no-op. Loading this module must never raise. | ||
|
|
||
| Installation cannot be undone (the patched GOT entries point at functions inside the cdylib), | ||
| so the library must stay mapped for the life of the process. We keep the `ctypes.CDLL` handle | ||
| at module scope and never unload it. | ||
|
|
||
| After a successful `install()`, a child of `fork()` inherits the mapping and the patched GOT. | ||
| Re-entering `install()` in that child is therefore unnecessary; we skip the native call when | ||
| this module already recorded a successful arm (Python module state is also inherited). That | ||
| avoids re-locking upstream's process-global registry mutex. Upstream does not yet implement a | ||
| `pthread_atfork` child reset, so forking *during* an in-flight `install()`/`update()` can still | ||
| leave that mutex locked in the child — prefer arming on the main thread, or after fork in the | ||
| worker (gunicorn/uWSGI-style), and treat mid-install fork as unsafe until libdatadog lands | ||
| atfork handling. | ||
| Dlopens ``libdd_heap_gotter`` (see ``src/native_heap_gotter``) and drives: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This whole comment reads very LLMy IMO |
||
|
|
||
| bool ddtrace_heap_gotter_install(void); | ||
| bool ddtrace_heap_gotter_is_installed(void); | ||
| bool ddtrace_heap_gotter_live_heap_enabled(void); # True if built with ddheap:free | ||
|
|
||
| ``install()`` patches GOT entries so ``ddheap:alloc`` (and, on live-heap builds, | ||
| ``ddheap:free``) USDT sites fire; the Full Host eBPF profiler attaches uprobes. | ||
| Nothing is collected or uploaded from Python. | ||
|
|
||
| Two independent runtime gates, both required for GOT to be patched: | ||
|
|
||
| * ``DD_PROFILING_NATIVE_HEAP_ENABLED`` — ddtrace install gate (also the | ||
| setup.py build gate). When false, this module is not imported from the | ||
| profiler and ``install()`` is never called. | ||
| * ``DD_HEAP_SAMPLING_ENABLED`` — libdatadog process-start bypass (unset = | ||
| on; ``0``/``false``/``no``/``off`` disables). Honored inside | ||
| ``install_heap_overrides``; a falsey value leaves the GOT untouched and | ||
| ``install()`` returns False. ddtrace does not set or wrap this variable. | ||
|
|
||
| ``live_heap_enabled()`` is a compile-time property of the loaded artifact | ||
| (default-on ``live-heap`` feature): True when the cdylib stamps retain flags and | ||
| emits ``ddheap:free``. False if the cdylib is missing, alloc-only | ||
| (``--no-default-features``), or predates this symbol (bound defensively). | ||
|
|
||
| Missing/broken load → ``is_available`` False, ``install()`` no-op; import never | ||
| raises. Install is permanent (GOT points into the cdylib), so the CDLL handle | ||
| stays mapped for the process lifetime. | ||
|
|
||
| After a successful ``install()``, fork children inherit the mapping and patched | ||
| GOT; ``_armed`` skips re-entering the native installer. Upstream has no | ||
| ``pthread_atfork`` reset for its registry mutex — prefer arming on the main | ||
| thread or in the worker after fork; mid-install fork is unsafe. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
@@ -35,27 +42,26 @@ | |
| import sysconfig | ||
|
|
||
|
|
||
| # Mirror the ddup/stack modules: importers (notably settings/profiling.py) read | ||
| # these two attributes to decide whether the feature can run. | ||
| # Mirror ddup/stack: settings/profiling.py reads these to gate the feature. | ||
| is_available: bool = False | ||
| failure_msg: str = "" | ||
|
|
||
| _lib: ctypes.CDLL | None = None # kept alive for process lifetime; never dlclose'd | ||
| # Set when install() has succeeded in this process (inherited across fork). | ||
| _armed: bool = False | ||
| # Compile-time live-heap capability of the loaded artifact (see module docstring). | ||
| _live_heap_available: bool = False | ||
|
|
||
| _lib: ctypes.CDLL | None = None # process lifetime; never dlclose'd | ||
| _armed: bool = False # successful install(); inherited across fork | ||
|
|
||
|
|
||
| def _library_path() -> str: | ||
| # The cdylib is staged next to libdd_wrapper in the profiling package and | ||
| # carries the interpreter EXT_SUFFIX, matching setup.py's naming. | ||
| # Staged next to libdd_wrapper with the interpreter EXT_SUFFIX (setup.py). | ||
| suffix: str = sysconfig.get_config_var("EXT_SUFFIX") or ".so" | ||
| profiling_dir: str = os.path.dirname(os.path.dirname(__file__)) | ||
| return os.path.join(profiling_dir, "libdd_heap_gotter" + suffix) | ||
|
|
||
|
|
||
| try: | ||
| # Native heap profiling via the gotter is Linux-only; on every other | ||
| # platform the underlying library is a no-op, so don't even try to load. | ||
| # Linux-only; elsewhere the gotter is a no-op. | ||
| sysname = os.uname().sysname if os.name == "posix" else os.name | ||
| if sysname != "Linux": | ||
| raise OSError(f"Native heap gotter is only supported on Linux. Running on {sysname}") | ||
|
|
@@ -64,8 +70,7 @@ def _library_path() -> str: | |
| if not os.path.exists(_path): | ||
| raise FileNotFoundError(_path) | ||
|
|
||
| # RTLD_GLOBAL so the loaded code is unambiguously resolvable; RTLD_NOW so any | ||
| # unresolved symbol fails here (fail-closed) rather than at first call. | ||
| # RTLD_GLOBAL for resolvability; RTLD_NOW fail-closed on unresolved symbols. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The previous comment was clearer I believe |
||
| _lib = ctypes.CDLL(_path, mode=ctypes.RTLD_GLOBAL | getattr(os, "RTLD_NOW", 0)) | ||
|
|
||
| _lib.ddtrace_heap_gotter_install.argtypes = [] | ||
|
|
@@ -75,18 +80,25 @@ def _library_path() -> str: | |
|
|
||
| is_available = True | ||
|
|
||
| # Optional symbol (pre-Phase-2 cdylibs); failure must not disable install(). | ||
| try: | ||
| _lib.ddtrace_heap_gotter_live_heap_enabled.argtypes = [] | ||
| _lib.ddtrace_heap_gotter_live_heap_enabled.restype = ctypes.c_bool | ||
| _live_heap_available = bool(_lib.ddtrace_heap_gotter_live_heap_enabled()) | ||
| except Exception: | ||
| _live_heap_available = False | ||
|
Copilot marked this conversation as resolved.
|
||
|
|
||
| except Exception as e: | ||
| failure_msg = str(e) | ||
| _lib = None | ||
|
|
||
|
|
||
| def install() -> bool: | ||
| """Install the native heap GOT overrides. Returns True if now installed; False otherwise. | ||
| """Install native heap GOT overrides. True if installed; False otherwise. | ||
|
|
||
| Idempotent at the Python layer: once a call has succeeded, further calls | ||
| (including in a forked child that inherited ``_armed``) return True without | ||
| re-entering the native installer. No-op that returns False when the cdylib | ||
| is unavailable. See the module docstring for fork-safety limits. | ||
| Idempotent at the Python layer: after success (including in a fork child that | ||
| inherited ``_armed``), further calls return True without re-entering the native | ||
| installer. See module docstring for fork-safety limits. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like fork-safety concerns in the module docstring are exclusively about this function -- why not include them in this function's docstring then? |
||
| """ | ||
| global _armed | ||
| if not is_available or _lib is None: | ||
|
|
@@ -112,3 +124,12 @@ def is_installed() -> bool: | |
| return bool(_lib.ddtrace_heap_gotter_is_installed()) | ||
| except Exception: | ||
| return False | ||
|
|
||
|
|
||
| def live_heap_enabled() -> bool: | ||
| """Return whether the loaded cdylib was built with live-heap tracking. | ||
|
|
||
| Compile-time property of the artifact (default-on feature). False when the | ||
| cdylib is missing, alloc-only, or predates this symbol. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The feature is currently unreleased to customers, is there any chance predates this symbol ever happen in practice, since it's never been released before? |
||
| """ | ||
| return _live_heap_available | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This release note needs some rework I think |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| features: | ||
| - | | ||
| profiling: Enable collection of samples for native (C/C++) allocations | ||
| in Python processes. Enable with ``DD_PROFILING_NATIVE_HEAP_ENABLED=true``. | ||
| This setting works for Linux only. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -135,7 +135,14 @@ | |||||
| # Opt-in build of the native heap-gotter cdylib. | ||||||
| # Off by default so normal builds don't pay the extra cargo fetch/compile and | ||||||
| # mainline wheels don't ship the artifact until it GA's. | ||||||
| BUILD_NATIVE_HEAP_GOTTER: bool = os.getenv("DD_PROFILING_NATIVE_HEAP_BUILD", "0").lower() in ("1", "yes", "on", "true") | ||||||
| # Same env var as runtime arming (ProfilingConfigNativeHeap.enabled); setup.py | ||||||
| # reads it via os.getenv during the package build, independent of DDConfig. | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Do we care about how we read env vars?
Comment on lines
+138
to
+139
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This whole comment doesn't sound very useful and even hurts readability I think. We're in |
||||||
| BUILD_NATIVE_HEAP_GOTTER: bool = os.getenv("DD_PROFILING_NATIVE_HEAP_ENABLED", "0").lower() in ( | ||||||
| "1", | ||||||
| "yes", | ||||||
| "on", | ||||||
| "true", | ||||||
| ) | ||||||
|
vlad-scherbich marked this conversation as resolved.
vlad-scherbich marked this conversation as resolved.
|
||||||
| # Keep the staged cdylib unstripped when building with the upstream test-support | ||||||
| # feature (hook-hit counter for e2e / integration tests). | ||||||
| BUILD_NATIVE_HEAP_GOTTER_TEST_SUPPORT: bool = os.getenv("DD_PROFILING_NATIVE_HEAP_TEST_SUPPORT", "0").lower() in ( | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,13 @@ | |||||
| /// Returns the result of `install_heap_overrides`, i.e. whether at least one | ||||||
| /// allocator symbol's GOT entry was resolved and patched (so hooks will run). | ||||||
| /// | ||||||
| /// libdatadog independently honors `DD_HEAP_SAMPLING_ENABLED` (unset = | ||||||
| /// enabled; `0`/`false`/`no`/`off` disables). That check lives inside | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| /// `install_heap_overrides`: a falsey value returns false without touching | ||||||
| /// the GOT. Distinct from `DD_PROFILING_NATIVE_HEAP_ENABLED`, which is the | ||||||
| /// ddtrace-side gate that decides whether this function is called. This | ||||||
| /// wrapper does not read or set the libdatadog env var. | ||||||
| /// | ||||||
| /// After a successful install, a `fork()` child inherits the mapping and the | ||||||
| /// patched GOT, so a second native install is usually unnecessary. Upstream has | ||||||
| /// no `pthread_atfork` child reset for the process-global registry mutex | ||||||
|
|
@@ -62,6 +69,18 @@ pub extern "C" fn ddtrace_heap_gotter_update() { | |||||
| libdd_profiling_heap_gotter::update_heap_overrides(); | ||||||
| } | ||||||
|
|
||||||
| /// Compile-time live-heap capability: `true` when built with the `live-heap` | ||||||
| /// feature (`ddheap:free` + retain flagging). Not a runtime toggle; keep in | ||||||
| /// lockstep with Cargo.toml's forward to `libdd-profiling-heap-gotter/live-heap`. | ||||||
| /// | ||||||
| /// # Safety | ||||||
| /// | ||||||
| /// C ABI entry point with no arguments and no pointers; always safe to call. | ||||||
| #[no_mangle] | ||||||
| pub extern "C" fn ddtrace_heap_gotter_live_heap_enabled() -> bool { | ||||||
| cfg!(feature = "live-heap") | ||||||
|
vlad-scherbich marked this conversation as resolved.
|
||||||
| } | ||||||
|
|
||||||
| /// Test-only: number of times a patched hook has run in this process. Lets | ||||||
| /// integration tests prove the patched GOT was actually exercised without a | ||||||
| /// live eBPF attach. Only present when built with the `test-support` feature; | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.