Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions libdd-profiling-heap-gotter-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ default = ["cbindgen"]
cbindgen = ["build_common/cbindgen", "libdd-common-ffi/cbindgen"]
# See libdd-profiling-heap-gotter's test-support feature. Not for production use.
test-support = ["libdd-profiling-heap-gotter/test-support"]
# Forward live-heap (retained-heap) tracking down to the sampler
live-heap = ["libdd-profiling-heap-gotter/live-heap"]

[build-dependencies]
build_common = { path = "../build-common" }
Expand Down
24 changes: 22 additions & 2 deletions libdd-profiling-heap-gotter-ffi/tests/usdt_notes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
//! attached consumer fire twice, so this guards the "one note per probe"
//! invariant documented in the sampler's `probes.c`.
//!
//! Split into two tests because the two probes have different lifetimes:
//! `ddheap:alloc` is expected in every build, while `ddheap:free` only
//! exists when compiled with live-heap tracking (the `live-heap` feature) —
//! its absence otherwise is intentional, so it's covered by its own
//! feature-gated test rather than folded into a single "all probes present"
//! assertion.
//!
//! Inspecting the linked cdylib (rather than running the check in-process)
//! validates the real shipped artifact. Mirrors
//! libdd-otel-thread-ctx-ffi/tests/elf_properties.rs.
Expand All @@ -25,9 +32,22 @@ fn cdylib_path() -> PathBuf {
.join("liblibdd_profiling_heap_gotter_ffi.so")
}

/// `ddheap:alloc` is unconditional: every build samples allocations, so its
/// note must always be present exactly once.
#[test]
#[cfg_attr(miri, ignore)]
fn ddheap_alloc_probe_has_one_note() {
let path = cdylib_path();
libdd_profiling_heap_sampler::usdt_check::check_usdt_probes_in(&path, &["alloc"]).unwrap();
}

/// `ddheap:free` only exists when built with live-heap tracking. Gated on the
/// `live-heap` feature (forwarded to the sampler crate) so this test only
/// runs, and only asserts the note is present, in builds that opt in.
#[cfg(feature = "live-heap")]
#[test]
#[cfg_attr(miri, ignore)]
fn ddheap_probes_have_one_note_each() {
fn ddheap_free_probe_has_one_note() {
let path = cdylib_path();
libdd_profiling_heap_sampler::usdt_check::check_usdt_probes_in(&path).unwrap();
libdd_profiling_heap_sampler::usdt_check::check_usdt_probes_in(&path, &["free"]).unwrap();
}
5 changes: 5 additions & 0 deletions libdd-profiling-heap-sampler/include/datadog/heap/probes.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ void dd_probe_alloc(void *user, uint64_t size, uint64_t weight);
/*
* Emits the `ddheap:free` USDT.
* ptr - user-visible pointer being freed
*
* The symbol always exists, but the USDT is only emitted when compiled
* with live-heap tracking. The absence of the `ddheap:free` note in
* .note.stapsdt signals to external profilers that this binary does not
* support live-heap correlation.
*/
void dd_probe_free(void *ptr);

Expand Down
7 changes: 5 additions & 2 deletions libdd-profiling-heap-sampler/src/allocation_freed.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
* dd_sample_flag_check_and_clear confirmed that ptr carries the sample flag,
* meaning this allocation was previously sampled.
*
* Fires the ddheap:free USDT with the user-visible pointer, then returns
* Fires the ddheap:free USDT (when live-heap tracking is enabled) with the
* user-visible pointer, then returns
* the raw pointer and adjusted size that the caller must forward to the
* real deallocator. On x86-64 the size grows by DD_HEADER_BYTES to cover
* the header that was reserved at allocation time; on arm64 the size is
Expand All @@ -21,7 +22,9 @@
dd_alloc_freed_t dd_allocation_freed_slow(void *ptr, void *raw, size_t size,
size_t alignment) {
/* Fire with the user-visible pointer, matching what was reported at alloc
* time, so the profiler can correlate the two events by address. */
* time, so the profiler can correlate the two events by address. Safe to
* call unconditionally: dd_probe_free is a no-op USDT-wise when live-heap
Comment thread
r1viollet marked this conversation as resolved.
* tracking is off. */
dd_probe_free(ptr);

dd_alloc_freed_t out = {
Expand Down
32 changes: 32 additions & 0 deletions libdd-profiling-heap-sampler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,38 @@ mod tests {
}
}

// With live-heap tracking compiled out, dd_allocation_freed is a
// straight passthrough: it never inspects the sample-flag header and
// never fires ddheap:free. Verify this even when the underlying
// memory happens to contain the magic pattern that *would* trigger
// the slow path if live-heap were enabled.
#[cfg(not(feature = "live-heap"))]
#[test]
fn freed_is_passthrough_when_live_heap_disabled() {
const MAGIC: u64 = 0xfab1eddec0dedca7;
const HEADER_BYTES: usize = 16;

// Set up a buffer with the magic header so that, if the flag-check
// were compiled in, it would consider this allocation sampled.
let mut buf = vec![0u8; 128];
let base = buf.as_mut_ptr() as usize;
let user_addr = base + HEADER_BYTES;

// Stamp magic at ptr - HEADER_BYTES
buf[0..8].copy_from_slice(&MAGIC.to_ne_bytes());
// Stamp a plausible offset
buf[8..16].copy_from_slice(&(HEADER_BYTES as u64).to_ne_bytes());

let ptr = user_addr as *mut c_void;
unsafe {
let freed = dd_allocation_freed(ptr, 64, 8);
// Without live-heap the pointer is returned unchanged (no
// flag-check, no raw-pointer recovery).
assert_eq!(freed.ptr, ptr);
assert_eq!(freed.size, 64);
}
}

#[test]
fn zero_sampling_interval_disables_sampling() {
let mut tl = dd_tl_state_t {
Expand Down
5 changes: 5 additions & 0 deletions libdd-profiling-heap-sampler/src/probes.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

#include <datadog/heap/probes.h>
#include <datadog/heap/sample_flag.h>

#include <errno.h>

Expand All @@ -29,7 +30,11 @@ void dd_probe_alloc(void *user, uint64_t size, uint64_t weight) {
}

void dd_probe_free(void *ptr) {
#if DD_HEAP_LIVE_TRACKING
int saved_errno = errno;
USDT(ddheap, free, ptr);
errno = saved_errno;
#else
(void)ptr;
Comment thread
scottgerring marked this conversation as resolved.
#endif
}
33 changes: 24 additions & 9 deletions libdd-profiling-heap-sampler/src/usdt_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
//! note; a regression (e.g. `static inline` + bindgen's `wrap_static_fns`, or
//! LTO inlining across TUs) could duplicate the entry. This check catches that.
//!
//! `ddheap:alloc` is expected in every build. `ddheap:free` is only expected
//! when compiled with live-heap tracking (the `live-heap` feature): its
//! absence is how external profilers detect that a binary doesn't support
//! live-heap correlation (see `probes.h`), so callers must pass the probe
//! list that matches the build under test rather than assume both are always
//! present.
//!
//! Call [`sanity_check`] from within a shared object or statically linked
//! executable, or point [`check_usdt_probes_in`] at a built artifact.
//!
Expand All @@ -23,23 +30,28 @@ use std::path::{Path, PathBuf};

/// USDT provider name emitted by `probes.c` (`USDT(ddheap, ...)`).
const PROVIDER: &str = "ddheap";
/// Probes we expect, each exactly once.
const EXPECTED_PROBES: &[&str] = &["alloc", "free"];

/// As [`sanity_check`], but takes the object file as an argument. Useful for a
/// test setting where the test code is separate from the artifact to validate.
pub fn check_usdt_probes_in(path: &Path) -> anyhow::Result<()> {
/// test setting where the test code is separate from the artifact to
/// validate. `expected_probes` lists the probe names (without the `ddheap:`
/// provider prefix) that must each appear exactly once.
pub fn check_usdt_probes_in(path: &Path, expected_probes: &[&str]) -> anyhow::Result<()> {
let data = std::fs::read(path).with_context(|| format!("failed to read {}", path.display()))?;
let elf = ElfBytes::<AnyEndian>::minimal_parse(&data)
.with_context(|| format!("failed to parse ELF at {}", path.display()))?;
check_one_note_per_probe(&elf)?;
check_one_note_per_probe(&elf, expected_probes)?;
Ok(())
}

/// Check that the current running module carries exactly one `.note.stapsdt`
/// entry per ddheap probe.
/// entry per ddheap probe expected in this build (`alloc` always, `free`
/// only when the `live-heap` feature is enabled).
pub fn sanity_check() -> anyhow::Result<()> {
check_usdt_probes_in(&own_elf_path()?)
let mut expected = vec!["alloc"];
if cfg!(feature = "live-heap") {
expected.push("free");
}
check_usdt_probes_in(&own_elf_path()?, &expected)
}

/// Locate the current running module (shared or not) via `/proc/self/maps`.
Expand Down Expand Up @@ -79,7 +91,10 @@ fn own_elf_path() -> anyhow::Result<PathBuf> {
}

/// Parse `.note.stapsdt` and assert each expected probe appears exactly once.
fn check_one_note_per_probe(elf: &ElfBytes<'_, AnyEndian>) -> anyhow::Result<()> {
fn check_one_note_per_probe(
elf: &ElfBytes<'_, AnyEndian>,
expected_probes: &[&str],
) -> anyhow::Result<()> {
let shdr = elf
.section_header_by_name(".note.stapsdt")
.context("failed to read section headers")?
Expand Down Expand Up @@ -118,7 +133,7 @@ fn check_one_note_per_probe(elf: &ElfBytes<'_, AnyEndian>) -> anyhow::Result<()>
}
}

for &probe in EXPECTED_PROBES {
for &probe in expected_probes {
match counts.get(probe).copied().unwrap_or(0) {
1 => {}
0 => bail!("USDT probe '{PROVIDER}:{probe}' has no .note.stapsdt entry"),
Expand Down
Loading