Skip to content

Commit d10a58f

Browse files
committed
feat: add libdd-heap- allocation tracking
1 parent 3630553 commit d10a58f

49 files changed

Lines changed: 4635 additions & 2 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ libdd-crashtracker*/ @DataDog/libdatadog-profiling
4949
libdd-data-pipeline*/ @DataDog/libdatadog-apm
5050
libdd-ddsketch*/ @DataDog/libdatadog-apm @DataDog/apm-common-components-core
5151
libdd-dogstatsd-client @DataDog/apm-common-components-core
52+
libdd-heap-*/ @DataDog/libdatadog-profiling
5253
libdd-http-client @DataDog/apm-common-components-core
5354
libdd-agent-client @DataDog/apm-common-components-core
5455
libdd-library-config*/ @DataDog/apm-sdk-capabilities-rust

.github/workflows/miri.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ jobs:
7979
# Override rust-toolchain.toml so miri runs on the dated nightly
8080
# (miri is nightly-only) instead of the workspace MSRV.
8181
RUSTUP_TOOLCHAIN: ${{ steps.nightly-version.outputs.version }}
82-
run: MIRIFLAGS="-Zmiri-disable-isolation" cargo miri nextest run ${{ needs.setup.outputs.crates }} --partition count:${{ matrix.partition }}/5 --no-tests=pass
82+
# Tests that require FFI/syscalls miri can't execute (e.g. the
83+
# libdd-heap-* crates' C-side primitives) skip themselves via
84+
# `#[cfg(not(miri))]` at the source level. No workflow-side
85+
# filtering needed.
86+
run: |
87+
CRATES="${{ needs.setup.outputs.crates }}"
88+
MIRIFLAGS="-Zmiri-disable-isolation" cargo miri nextest run $CRATES --partition count:${{ matrix.partition }}/5 --no-tests=pass
8389
# We need to disable isolation because
8490
# "unsupported operation: `clock_gettime` with `REALTIME` clocks not available when isolation is enabled"

.github/workflows/test.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,14 @@ jobs:
321321
-w /workspace \
322322
libdatadog-centos7 \
323323
sh -c "$NEXTEST_CMD"
324+
# nextest with --no-tests=pass exits 0 without writing junit.xml
325+
# when the filter selects zero tests; downstream junit-consuming
326+
# steps still expect the file. Emit an empty report so they
327+
# have something to read.
328+
if [ ! -f target/nextest/ci/junit.xml ]; then
329+
mkdir -p target/nextest/ci
330+
printf '<?xml version="1.0" encoding="UTF-8"?>\n<testsuites name="nextest-run" tests="0" failures="0" errors="0" disabled="0" skipped="0" time="0"></testsuites>\n' > target/nextest/ci/junit.xml
331+
fi
324332
- name: Add file attributes to JUnit XML
325333
if: success() || failure()
326334
run: cargo run --bin add_junit_file_attributes -- target/nextest/ci/junit.xml
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: 'Verify libdd-heap-sampler bindings'
2+
on:
3+
pull_request:
4+
types: [opened, synchronize, reopened]
5+
paths:
6+
- 'libdd-heap-sampler/**'
7+
- '.github/workflows/verify-heap-sampler-bindings.yml'
8+
env:
9+
CARGO_TERM_COLOR: always
10+
CARGO_INCREMENTAL: 0
11+
jobs:
12+
verify-bindings:
13+
name: "Verify libdd-heap-sampler generated bindings are in sync"
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout sources
17+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
18+
- name: Install libclang-dev + cross libc headers
19+
# Only this verification job needs libclang; the normal build
20+
# path for libdd-heap-sampler consumes the checked-in bindings
21+
# under `src/generated/<arch>/` and does not depend on bindgen
22+
# at all. The `*-cross` packages provide `<stdint.h>`,
23+
# `<string.h>`, etc. for the non-native target arch, letting
24+
# `build.rs` regenerate both aarch64 and x86_64 bindings from a
25+
# single runner via clang's `--target=<arch>-linux-gnu` flag.
26+
run: |
27+
sudo apt-get update
28+
sudo apt-get install -y libclang-dev libc6-dev-amd64-cross libc6-dev-arm64-cross
29+
- name: Read Rust version from rust-toolchain.toml
30+
id: rust-version
31+
run: echo "version=$(grep -Po '^channel = "\K[^"]+' rust-toolchain.toml)" >> $GITHUB_OUTPUT
32+
- name: Install ${{ steps.rust-version.outputs.version }} toolchain
33+
run: rustup set profile minimal && rustup install ${{ steps.rust-version.outputs.version }} && rustup default ${{ steps.rust-version.outputs.version }}
34+
- name: Cache [rust]
35+
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # 2.8.1
36+
with:
37+
cache-targets: true
38+
cache-bin: true
39+
- name: Regenerate bindings
40+
# `LIBDD_HEAP_SAMPLER_REGEN=1` rewrites `src/generated/<arch>/*`
41+
# in-place via bindgen. If the committed files were stale the
42+
# git diff below fails and instructs the author how to refresh
43+
# them. We use an env var (rather than a cargo feature) so that
44+
# unrelated `--all-features` CI jobs cannot accidentally invoke
45+
# bindgen — see libdd-heap-sampler/Cargo.toml for the rationale.
46+
run: LIBDD_HEAP_SAMPLER_REGEN=1 cargo build -p libdd-heap-sampler
47+
- name: Verify committed bindings match regenerated output
48+
run: |
49+
if ! git diff --exit-code libdd-heap-sampler/src/generated/; then
50+
echo "::error::libdd-heap-sampler/src/generated/ is out of date."
51+
echo "Run \`LIBDD_HEAP_SAMPLER_REGEN=1 cargo build -p libdd-heap-sampler\` locally and commit the result."
52+
exit 1
53+
fi

Cargo.lock

Lines changed: 56 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
members = [
66
"builder",
77
"libdd-alloc",
8+
"libdd-heap-sampler",
9+
"libdd-heap-allocator",
10+
"libdd-heap-gotter",
11+
"libdd-heap-gotter-ffi",
812
"libdd-crashtracker",
913
"libdd-crashtracker-ffi",
1014
"datadog-ffe",

NOTICE

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,11 @@ Datadog libdatadog
22
Copyright 2021-2022 Datadog, Inc.
33

44
This product includes software developed at Datadog (<https://www.datadoghq.com/>).
5+
6+
--
7+
8+
This product bundles a copy of the libbpf/usdt single-header USDT
9+
library in `libdd-heap-sampler/vendor/usdt.h`. That file is licensed
10+
under the BSD 2-Clause License, Copyright (c) 2024 Meta Platforms, Inc.
11+
and affiliates. The SPDX identifier and copyright notice are retained
12+
verbatim in the file header. Upstream: <https://github.com/libbpf/usdt>.

libdd-heap-allocator/Cargo.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
[package]
5+
name = "libdd-heap-allocator"
6+
version = "0.1.0"
7+
description = "Rust GlobalAlloc wrapper that drives libdd-heap-sampler."
8+
homepage = "https://github.com/DataDog/libdatadog/tree/main/libdd-heap-allocator"
9+
repository = "https://github.com/DataDog/libdatadog/tree/main/libdd-heap-allocator"
10+
edition.workspace = true
11+
rust-version.workspace = true
12+
license.workspace = true
13+
publish = false
14+
15+
[lib]
16+
bench = false
17+
18+
[dependencies]
19+
libdd-heap-sampler = { path = "../libdd-heap-sampler" }

libdd-heap-allocator/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# libdd-heap-allocator
2+
3+
Rust `GlobalAlloc` wrapper with USDT-based heap profiling, effectively implementing [libdd-heap-sampler](../libdd-heap-sampler) for Rust apps at compile time. This lets Rust users quickly setup sampled heap profiling within their application regardless of the particular allocator they are using.
4+
5+
For this to work _well_, you should make sure everything passes through the global allocator!
6+
7+
Usage:
8+
9+
```rust
10+
use libdd_heap_allocator::SampledAllocator;
11+
use std::alloc::System;
12+
13+
// Wrap the default system allocator
14+
#[global_allocator]
15+
static ALLOC: SampledAllocator<System> = SampledAllocator::<System>::DEFAULT;
16+
```
17+
18+
To wrap a custom allocator instead; note that this is kind of ill-advised; we want to see _all_ allocations for the process:
19+
20+
```rust
21+
#[global_allocator]
22+
static ALLOC: SampledAllocator<MyAllocator> = SampledAllocator::new(MyAllocator::new());
23+
```
24+
25+
See [`examples/usdt_demo.rs`](examples/usdt_demo.rs) for a runnable demo that fires USDT probes in a loop for `bpftrace` to observe.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Sample app exercising `libdd-heap-allocator` as the global allocator.
5+
//!
6+
//! Install `SampledAllocator<System>` globally, then loop producing
7+
//! allocations (strings joined into a single buffer) so a tracer attached
8+
//! to the `ddheap:alloc` USDT probe sees samples fire periodically.
9+
//!
10+
//! Run (Linux, inside the crate's Lima VM):
11+
//! ```
12+
//! cargo run --example usdt_demo -p libdd-heap-allocator
13+
//! ```
14+
//! and in another shell, attach a tracer, e.g.
15+
//! ```
16+
//! sudo bpftrace -p <pid> -e 'usdt:*:ddheap:alloc { printf("alloc %p %d %d\n", arg0, arg1, arg2); }'
17+
//! ```
18+
//!
19+
//! `SampledAllocator` is Linux-only; on other targets the example
20+
//! compiles to an empty `main` so clippy/test on non-Linux don't fail
21+
//! with "configured out".
22+
23+
#[cfg(not(target_os = "linux"))]
24+
fn main() {}
25+
26+
#[cfg(target_os = "linux")]
27+
fn main() {
28+
linux::main();
29+
}
30+
31+
#[cfg(target_os = "linux")]
32+
mod linux {
33+
use libdd_heap_allocator::SampledAllocator;
34+
use std::alloc::System;
35+
use std::thread::sleep;
36+
use std::time::Duration;
37+
38+
#[global_allocator]
39+
static ALLOC: SampledAllocator<System> = SampledAllocator::<System>::DEFAULT;
40+
41+
pub fn main() {
42+
println!(
43+
"pid={}; attach a tracer on 'usdt:*:ddheap:alloc'",
44+
std::process::id()
45+
);
46+
47+
let mut i: u64 = 0;
48+
loop {
49+
// ~1000 small allocations + one larger join: plenty of alloc
50+
// pressure to cross the default 512 KiB sampling interval over
51+
// a handful of iterations.
52+
let parts: Vec<String> = (0..1000)
53+
.map(|j| format!("chunk-{i}-{j}-with-some-padding-to-make-it-meaningful"))
54+
.collect();
55+
let joined = parts.join(", ");
56+
println!("[{i}] joined {} bytes", joined.len());
57+
i = i.wrapping_add(1);
58+
sleep(Duration::from_secs(1));
59+
}
60+
}
61+
} // mod linux

0 commit comments

Comments
 (0)