|
113 | 113 | IS_EDITABLE = False # Set to True if the package is being installed in editable mode |
114 | 114 |
|
115 | 115 | NATIVE_CRATE = HERE / "src" / "native" |
| 116 | +# Standalone cdylib wrapper around libdatadog's heap-gotter FFI. Built out-of-band |
| 117 | +# (opt-in) because it pins libdatadog `main` rather than the tagged `src/native` rev. |
| 118 | +NATIVE_HEAP_GOTTER_CRATE = HERE / "src" / "native_heap_gotter" |
116 | 119 | DDTRACE_DIR = HERE / "ddtrace" |
117 | 120 | LIBDDWAF_DOWNLOAD_DIR = DDTRACE_DIR / "appsec" / "_ddwaf" / "libddwaf" |
118 | 121 | IAST_DIR = DDTRACE_DIR / "appsec" / "_iast" / "_taint_tracking" |
|
124 | 127 |
|
125 | 128 | BUILD_PROFILING_NATIVE_TESTS = os.getenv("DD_PROFILING_NATIVE_TESTS", "0").lower() in ("1", "yes", "on", "true") |
126 | 129 |
|
| 130 | +# Opt-in build of the native heap-gotter cdylib (Phase 1: allocation-only native |
| 131 | +# heap profiling via GOT rewriting, driven at runtime by the FH eBPF profiler). |
| 132 | +# Off by default so mainline wheels are not pinned to a moving libdatadog `main` |
| 133 | +# SHA and normal builds don't pay the extra cargo fetch/compile. The staging A/B |
| 134 | +# harness sets this to bake the artifact into its custom wheels; runtime install |
| 135 | +# is separately gated by DD_PROFILING_NATIVE_HEAP_ENABLED. |
| 136 | +BUILD_NATIVE_HEAP_GOTTER = os.getenv("DD_PROFILING_NATIVE_HEAP_BUILD", "0").lower() in ("1", "yes", "on", "true") |
| 137 | + |
127 | 138 | CURRENT_OS = platform.system() |
128 | 139 | SERVERLESS_BUILD = os.getenv("DD_SERVERLESS_BUILD", "0").lower() in ("1", "yes", "on", "true") |
129 | 140 | WHEEL_FLAVOR = "-serverless" if SERVERLESS_BUILD else "" |
@@ -844,6 +855,13 @@ def run(self) -> None: |
844 | 855 | with _time_phase("build_libdd_wrapper"): |
845 | 856 | self.build_libdd_wrapper() |
846 | 857 |
|
| 858 | + # Build the native heap-gotter cdylib (opt-in, Linux 64-bit only). It is |
| 859 | + # a standalone ctypes-loaded library with no dependency on the other |
| 860 | + # extensions, so ordering relative to them does not matter. |
| 861 | + if BUILD_NATIVE_HEAP_GOTTER and CURRENT_OS == "Linux" and is_64_bit_python(): |
| 862 | + with _time_phase("build_heap_gotter"): |
| 863 | + self.build_heap_gotter() |
| 864 | + |
847 | 865 | # Build all declared shared C++ dependencies before extension builds. |
848 | 866 | with _time_phase("build_shared_deps"): |
849 | 867 | self.build_shared_deps() |
@@ -1006,6 +1024,54 @@ def build_libdd_wrapper(self): |
1006 | 1024 | else: |
1007 | 1025 | print(f"Skipping libdd_wrapper build (no changes): {wrapper_name}") |
1008 | 1026 |
|
| 1027 | + def build_heap_gotter(self): |
| 1028 | + """Build the native heap-gotter cdylib via cargo and stage it for packaging. |
| 1029 | +
|
| 1030 | + Produces ``libdd_heap_gotter<EXT_SUFFIX>.so`` under |
| 1031 | + ``ddtrace/internal/datadog/profiling/`` (mirroring the ``_native`` / |
| 1032 | + ``libdd_wrapper`` naming so the ctypes activator can resolve it with the |
| 1033 | + same EXT_SUFFIX logic). The wrapper crate has no Python linkage, so a |
| 1034 | + single ``target/`` dir is shared across interpreter versions. |
| 1035 | + """ |
| 1036 | + suffix = getattr(self, "suffix", None) or sysconfig.get_config_var("EXT_SUFFIX") |
| 1037 | + gotter_name = f"libdd_heap_gotter{suffix}" |
| 1038 | + |
| 1039 | + if IS_EDITABLE or getattr(self, "inplace", False): |
| 1040 | + output_dir = Path(__file__).parent / "ddtrace" / "internal" / "datadog" / "profiling" |
| 1041 | + else: |
| 1042 | + output_dir = Path(__file__).parent / Path(self.build_lib) / "ddtrace" / "internal" / "datadog" / "profiling" |
| 1043 | + output_dir.mkdir(parents=True, exist_ok=True) |
| 1044 | + gotter_library = output_dir / gotter_name |
| 1045 | + |
| 1046 | + cargo_cmd = [ |
| 1047 | + "cargo", |
| 1048 | + "build", |
| 1049 | + "--release", |
| 1050 | + "--manifest-path", |
| 1051 | + str(NATIVE_HEAP_GOTTER_CRATE / "Cargo.toml"), |
| 1052 | + ] + DD_CARGO_ARGS |
| 1053 | + subprocess.run(cargo_cmd, check=True) |
| 1054 | + |
| 1055 | + # The wrapper crate's [lib] name is "dd_heap_gotter", so cargo emits a |
| 1056 | + # single-lib-prefixed artifact (unlike libdatadog's own liblib*.so). |
| 1057 | + # Glob defensively in case the extension/name assumption ever drifts. |
| 1058 | + release_dir = NATIVE_HEAP_GOTTER_CRATE / "target" / "release" |
| 1059 | + built = release_dir / "libdd_heap_gotter.so" |
| 1060 | + if not built.exists(): |
| 1061 | + candidates = [c for c in release_dir.glob("libdd_heap_gotter.*") if c.suffix in (".so", ".dylib")] |
| 1062 | + if not candidates: |
| 1063 | + raise RuntimeError(f"Not able to find heap-gotter cdylib in {release_dir}") |
| 1064 | + built = candidates[0] |
| 1065 | + |
| 1066 | + shutil.copy2(built, gotter_library) |
| 1067 | + print(f"Built and copied heap-gotter cdylib: {gotter_name}") |
| 1068 | + |
| 1069 | + # Set SONAME so the loader records the staged name, matching build_rust. |
| 1070 | + if CURRENT_OS == "Linux": |
| 1071 | + subprocess.run(["patchelf", "--set-soname", gotter_name, gotter_library], check=True) |
| 1072 | + elif CURRENT_OS == "Darwin": |
| 1073 | + subprocess.run(["install_name_tool", "-id", gotter_name, gotter_library], check=True) |
| 1074 | + |
1009 | 1075 | def build_shared_deps(self) -> None: |
1010 | 1076 | """Build all shared C++ dependencies declared in SHARED_DEPS. |
1011 | 1077 |
|
@@ -1750,7 +1816,9 @@ def get_exts_for(name): |
1750 | 1816 | "ddtrace.appsec.sca": ["_cve_data.json"], |
1751 | 1817 | "ddtrace.internal": ["third-party.tar.gz"], |
1752 | 1818 | "ddtrace.internal.datadog.profiling": ( |
1753 | | - ["libdd_wrapper*.*"] + (["test/*"] if BUILD_PROFILING_NATIVE_TESTS else []) |
| 1819 | + ["libdd_wrapper*.*"] |
| 1820 | + + (["libdd_heap_gotter*.*"] if BUILD_NATIVE_HEAP_GOTTER else []) |
| 1821 | + + (["test/*"] if BUILD_PROFILING_NATIVE_TESTS else []) |
1754 | 1822 | ), |
1755 | 1823 | }, |
1756 | 1824 | zip_safe=False, |
|
0 commit comments