feat(heap-profiling): add initial heap profiling primitives [PROF-15190]#2166
feat(heap-profiling): add initial heap profiling primitives [PROF-15190]#2166scottgerring wants to merge 12 commits into
Conversation
🎉 All green!🧪 All tests passed 🔗 Commit SHA: adc2f14 | Docs | Datadog PR Page | Give us feedback! |
b6aab83 to
72d9f77
Compare
📚 Documentation Check Results📦
|
Clippy Allow Annotation ReportComparing clippy allow annotations between branches:
Summary by Rule
Annotation Counts by File
Annotation Stats by Crate
About This ReportThis report tracks Clippy allow annotations for specific rules, showing how they've changed in this PR. Decreasing the number of these annotations generally improves code quality. |
72d9f77 to
fc5c074
Compare
🔒 Cargo Deny Results📦
|
b80ec4a to
11cf066
Compare
11cf066 to
f40cfdf
Compare
6fe0523 to
d10a58f
Compare
Artifact Size Benchmark Reportaarch64-alpine-linux-musl
aarch64-unknown-linux-gnu
libdatadog-x64-windows
libdatadog-x86-windows
x86_64-alpine-linux-musl
x86_64-unknown-linux-gnu
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: be77a1cb93
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
ad0beee to
06d136f
Compare
06d136f to
d46e2c4
Compare
There was a problem hiding this comment.
Pull request overview
Adds the first set of heap-profiling building blocks to libdatadog: a Linux-focused sampler that emits ddheap:* USDT probes, plus Rust and runtime-injection frontends (GlobalAlloc wrapper + GOT interposition) and CI to keep the checked-in bindgen artifacts in sync.
Changes:
- Introduces
libdd-heap-sampler(C implementation + checked-in bindgen outputs) with vendoredusdt.hand a workflow to verify regenerated bindings match the committed artifacts. - Adds
libdd-heap-allocator(RustGlobalAllocwrapper) andlibdd-heap-gotter(ELF GOT-patching injector) with demos and tests. - Adds
libdd-heap-gotter-ffito expose install/update/restore over a small C ABI for use by other runtimes.
Reviewed changes
Copilot reviewed 42 out of 47 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/docker/Dockerfile.build | Adds new heap crates to Docker build caching/stubs. |
| NOTICE | Notes bundled usdt.h license attribution. |
| Cargo.toml | Adds new heap crates to workspace members. |
| Cargo.lock | Records new crate deps (incl. bindgen split versions). |
| .github/workflows/verify-heap-sampler-bindings.yml | CI job to regenerate/verify checked-in bindgen artifacts. |
| .github/CODEOWNERS | Assigns heap crates to profiling team. |
| libdd-heap-sampler/Cargo.toml | New sampler crate metadata + build deps. |
| libdd-heap-sampler/build.rs | Builds C sources on Linux; optional bindgen regen via env var. |
| libdd-heap-sampler/README.md | High-level design and binding-regeneration instructions. |
| libdd-heap-sampler/vendor/usdt.h | Vendored libbpf/usdt single-header implementation. |
| libdd-heap-sampler/vendor/README.md | Documents vendoring source/license and refresh procedure. |
| libdd-heap-sampler/src/lib.rs | Rust façade over generated bindings + basic layout/smoke tests. |
| libdd-heap-sampler/src/generated/bindings.rs | Checked-in bindgen Rust declarations. |
| libdd-heap-sampler/src/generated/dd_heap_sampler_static_wrappers.c | Checked-in bindgen static-inline wrappers. |
| libdd-heap-sampler/src/tl_state.c | TLS state init/seed logic for per-thread sampling. |
| libdd-heap-sampler/src/sample_flag.c | Arch-specific sampled-pointer marking (x86 header, arm64 TBI). |
| libdd-heap-sampler/src/probes.c | Non-inline USDT probe emission functions. |
| libdd-heap-sampler/src/allocation_requested.c | Poisson sampler slow-path + size bumping logic. |
| libdd-heap-sampler/src/allocation_created.c | Applies sample flag + emits alloc probe + closes reentry guard. |
| libdd-heap-sampler/src/allocation_freed.c | Emits free probe and returns raw pointer/size adjustments. |
| libdd-heap-sampler/src/allocation_realloc.c | Realloc helpers for sampled-old teardown behavior. |
| libdd-heap-sampler/include/datadog/heap/tl_state.h | Public TLS state struct + fast getters. |
| libdd-heap-sampler/include/datadog/heap/sample_flag.h | Public flagging API + x86/arm64 fast check helpers. |
| libdd-heap-sampler/include/datadog/heap/probes.h | Public probe API + Linux USDT include. |
| libdd-heap-sampler/include/datadog/heap/allocation_requested.h | Public pre-alloc hook API + fast path inline. |
| libdd-heap-sampler/include/datadog/heap/allocation_created.h | Public post-alloc hook API + fast path inline. |
| libdd-heap-sampler/include/datadog/heap/allocation_freed.h | Public pre-free hook API + fast path inline. |
| libdd-heap-sampler/include/datadog/heap/allocation_realloc.h | Public realloc helper API. |
| libdd-heap-allocator/Cargo.toml | New Rust allocator wrapper crate. |
| libdd-heap-allocator/src/lib.rs | Exposes SampledAllocator. |
| libdd-heap-allocator/src/allocator.rs | Implements GlobalAlloc wrapper + tests. |
| libdd-heap-allocator/README.md | Usage docs + example pointer. |
| libdd-heap-allocator/examples/usdt_demo.rs | Demo binary emitting USDT alloc samples. |
| libdd-heap-gotter/Cargo.toml | New GOT interposition crate. |
| libdd-heap-gotter/src/lib.rs | Public API + Linux/no-op cross-platform surface. |
| libdd-heap-gotter/src/hooks.rs | Allocator/dlopen/pthread_create hook implementations. |
| libdd-heap-gotter/src/elf.rs | ELF/DYNAMIC parsing + relocation patching + mprotect guard. |
| libdd-heap-gotter/README.md | GOTter overview documentation. |
| libdd-heap-gotter/examples/gotter_usdt_demo.rs | Demo using GOT overrides to drive sampler via Rust allocations. |
| libdd-heap-gotter/tests/install.rs | Linux integration smoke test for install/restore. |
| libdd-heap-gotter-ffi/Cargo.toml | New C-ABI wrapper crate around gotter. |
| libdd-heap-gotter-ffi/build.rs | Generates C header via cbindgen. |
| libdd-heap-gotter-ffi/cbindgen.toml | C header generation configuration. |
| libdd-heap-gotter-ffi/src/lib.rs | Exposes install/update/restore/is-installed over C ABI. |
| libdd-heap-gotter-ffi/README.md | Documents C ABI and lifetime constraints. |
| libdd-heap-gotter-ffi/examples/cdylib_demo.rs | Demo dynamic-loading and calling the C ABI. |
| libdd-heap-gotter-ffi/tests/install.rs | FFI smoke test for install/restore roundtrip. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
17c7bba to
38fba33
Compare
38fba33 to
c051bd7
Compare
0caaaad to
f0a58df
Compare
Adds a Criterion benchmark comparing direct System allocation, the SampledAllocator hot path, and direct sampler calls, plus a small bench.sh helper for libdd-heap-sampler. Kept on a separate change because it pulls criterion (and thus bindgen via libdd-heap-sampler's build) into the bench compile set, which the libdatadog benchmarking-platform image cannot build without libclang-dev.
af47843 to
00e15fd
Compare
yannham
left a comment
There was a problem hiding this comment.
Partial review: libdd-heap-allocator, libdd-heap-gotter-ffi.
| sleep(Duration::from_secs(1)); | ||
| } | ||
| } | ||
| } // mod linux |
There was a problem hiding this comment.
Hey, we're not in C 😛
| } // mod linux | |
| } |
| To wrap a custom allocator instead; note that this is kind of ill-advised; we want to see _all_ allocations for the process: | ||
|
|
||
| ```rust | ||
| #[global_allocator] | ||
| static ALLOC: SampledAllocator<MyAllocator> = SampledAllocator::new(MyAllocator::new()); | ||
| ``` |
There was a problem hiding this comment.
Also, I'm not sure to understand this mention. Why wrapping a custom allocator is ill-advised? Doesn't the wrapping reach the same blast radius anyway, which is the current project/compilation unit/whatever (This is not a rhetorical question, I don't remember how custom allocators are scoped) ?
| To wrap a custom allocator instead; note that this is kind of ill-advised; we want to see _all_ allocations for the process: | |
| ```rust | |
| #[global_allocator] | |
| static ALLOC: SampledAllocator<MyAllocator> = SampledAllocator::new(MyAllocator::new()); | |
| ``` | |
| To wrap a custom allocator instead: | |
| ```rust | |
| #[global_allocator] | |
| static ALLOC: SampledAllocator<MyAllocator> = SampledAllocator::new(MyAllocator::new()); | |
| ``` | |
| Note that this is kind of ill-advised: we want to see _all_ allocations for the process. |
| #![cfg_attr(not(test), deny(clippy::unwrap_used))] | ||
| #![cfg_attr(not(test), deny(clippy::expect_used))] | ||
| #![cfg_attr(not(test), deny(clippy::todo))] | ||
| #![cfg_attr(not(test), deny(clippy::unimplemented))] |
There was a problem hiding this comment.
| #![cfg_attr(not(test), deny(clippy::unimplemented))] | |
| #![cfg_attr(not(test), deny(clippy::unimplemented))] | |
| #![cfg_attr(not(test), deny(clippy::unreachable))] |
| /// could be installed; the rest of the API can still be called safely. | ||
| #[no_mangle] | ||
| #[must_use] | ||
| #[named] |
There was a problem hiding this comment.
Do you use the function name anymore? It doesn't look so.
| fn assert_ok(result: VoidResult, what: &str) { | ||
| match result { | ||
| VoidResult::Ok => {} | ||
| VoidResult::Err(err) => panic!("{what} failed: {err}"), | ||
| } | ||
| } |
There was a problem hiding this comment.
Nit: I think you can just do assert_eq!(foo, Ok(_)) to the same effect instead of defining this helper. It should print foo and Err(e) in debug mode.
| The `cdylib_demo` example loads the generated shared library with `dlopen`, resolves the C ABI symbols with `dlsym`, installs the GOT hooks, and produces allocation pressure. | ||
|
|
||
| ```bash | ||
| cargo build -p libdd-heap-gotter-ffi |
There was a problem hiding this comment.
Nit, but cargo run will build whatever it needs to automatically I believe?
| cargo build -p libdd-heap-gotter-ffi |
| dd_allocation_created(raw.cast(), req).cast() | ||
| } | ||
|
|
||
| #[cfg(not(target_os = "linux"))] |
There was a problem hiding this comment.
I'm sure rustc does a good job but it's one of those cases where we're pretty sure we want this to always be inlined.
| #[cfg(not(target_os = "linux"))] | |
| #[cfg(not(target_os = "linux"))] | |
| #[inline(always)] |
| self.inner.dealloc(freed.ptr.cast(), inner_layout); | ||
| } | ||
|
|
||
| #[cfg(not(target_os = "linux"))] |
There was a problem hiding this comment.
| #[cfg(not(target_os = "linux"))] | |
| #[cfg(not(target_os = "linux"))] | |
| #[inline(always)] |
| } | ||
|
|
||
| unsafe impl<A: GlobalAlloc> GlobalAlloc for SampledAllocator<A> { | ||
| #[cfg(target_os = "linux")] |
There was a problem hiding this comment.
I'm not entirely sure how it works for allocators, but in general in Rust you need to add #[inline] for any cross-crate (that is cross-compilation-unit) inlining to work (or the compiler just won't include required LLVM bytecode to do so to avoid having huge artifacts). This annotation still lets rustc decide (it can refuse to inline it), but without it there can't cross-crate inlining, so I believe it's safe to add it.
| #[cfg(target_os = "linux")] | |
| #[cfg(target_os = "linux")] | |
| #[inline] |
| self.inner.alloc(layout) | ||
| } | ||
|
|
||
| #[cfg(target_os = "linux")] |
There was a problem hiding this comment.
| #[cfg(target_os = "linux")] | |
| #[cfg(target_os = "linux")] | |
| #[inline] |
What does this PR do?
Adds initial support for eBPF heap profiling to libdatadog for the ebpf heap profiling project. These changes provide the ability for libdatadog users to introduce a USDT into consuming applications allocation and deallocation paths which is fired behind a poisson distributed sampling path every ~0.5 MiB of allocation. This will give us something to hook in user applications from the eBPF side to sample allocations for allocation tracking and sample allocations+frees for live heap.
I expect we will make more changes to this as we go along, but as this is a standalone feature and is largely based on prior work in ddprof it would be nice to get things in now.
A brief description of the change being made with this pull request.
Interesting Details
usdt.h; it is not consistently available on all our build environments (read: old alpine) and this seems to be the common pattern. The API is stable and I see no concerns with this and have updated the license stuff appropriatelyMotivation
An enthusiasm for heap profiling
Additional Notes
Anything else we should know when reviewing?
How to test the change?
Describe here in detail how the change can be validated.