Skip to content

Commit 65d8d24

Browse files
authored
feat(heap-profiling): add initial heap profiling primitives [PROF-15190] (#2166)
# What does this PR do? Adds initial support for eBPF heap profiling to libdatadog for the [ebpf heap profiling project](https://docs.google.com/document/d/1cy6OUisjW4_vAIaAu9G5l3RGuPPLT_MRZuhDPJHxXuw/edit?tab=t.mtwnx5ijdb84). 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 * I have vendored `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 appropriately * I have gated the binding generation behind an env var and committed the bindings. This is because the generation imposes a fair bit of extra _stuff_ (llvm and supporting bits) on _every_ build job and developer. I've added a CI job to validate that the bindings generated reflect what's checked in to avoid the footgun. # Motivation An enthusiasm for heap profiling # Additional Notes Anything else we should know when reviewing? * [PoC-y branch for the full host profiler with heap profiler](https://github.com/DataDog/opentelemetry-ebpf-profiler/tree/sgg/heap-prof-poc) * The profiling backend + frontend are done; you can see heap profiles pushed through using this infrastructure in [staging](https://dd.datad0g.com/profiling/explorer?query=service%3A%2Ausdt%2A&agg_m=%40prof_ebpf_cpu_cores&agg_m_source=base&agg_t=sum&extra_search_fields=%7B%22filters_query%22%3A%22%22%2C%22sample_type%22%3A%22cpu-time%22%7D&my_code=enabled&profile_type=ebpf-alloc-size&profiling-flame-graph__expanded_groups=3269946435%2C4099416842&refresh_mode=paused&viz=flame_graph&zoom=2156769548&from_ts=1782885547816&to_ts=1782889147816&live=false) # How to test the change? Describe here in detail how the change can be validated. Co-authored-by: scott.gerring <scott.gerring@datadoghq.com>
1 parent 9c33359 commit 65d8d24

51 files changed

Lines changed: 6467 additions & 1 deletion

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
@@ -50,6 +50,7 @@ libdd-crashtracker*/ @DataDog/libdatadog-profiling
5050
libdd-data-pipeline*/ @DataDog/libdatadog-apm
5151
libdd-ddsketch*/ @DataDog/libdatadog-apm @DataDog/apm-common-components-core
5252
libdd-dogstatsd-client @DataDog/apm-common-components-core
53+
libdd-heap-*/ @DataDog/libdatadog-profiling
5354
libdd-http-client @DataDog/apm-common-components-core
5455
libdd-agent-client @DataDog/apm-common-components-core
5556
libdd-library-config*/ @DataDog/apm-sdk-capabilities-rust
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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
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/` and does not depend on bindgen at all.
22+
run: |
23+
sudo apt-get update
24+
sudo apt-get install -y libclang-dev
25+
- name: Read Rust version from rust-toolchain.toml
26+
id: rust-version
27+
run: echo "version=$(grep -Po '^channel = "\K[^"]+' rust-toolchain.toml)" >> $GITHUB_OUTPUT
28+
- name: Install ${{ steps.rust-version.outputs.version }} toolchain
29+
run: rustup set profile minimal && rustup install ${{ steps.rust-version.outputs.version }} && rustup default ${{ steps.rust-version.outputs.version }}
30+
- name: Cache [rust]
31+
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # 2.8.1
32+
with:
33+
cache-targets: true
34+
cache-bin: true
35+
- name: Regenerate bindings
36+
# `LIBDD_HEAP_SAMPLER_REGEN=1` rewrites `src/generated/*`
37+
# in-place via bindgen. If the committed files were stale the
38+
# git diff below fails and instructs the author how to refresh
39+
# them. We use an env var (rather than a cargo feature) so that
40+
# unrelated `--all-features` CI jobs cannot accidentally invoke
41+
# bindgen — see libdd-heap-sampler/Cargo.toml for the rationale.
42+
run: LIBDD_HEAP_SAMPLER_REGEN=1 cargo build -p libdd-heap-sampler
43+
- name: Verify committed bindings match regenerated output
44+
run: |
45+
if ! git diff --exit-code libdd-heap-sampler/src/generated/; then
46+
echo "::error::libdd-heap-sampler/src/generated/ is out of date."
47+
echo "Run \`LIBDD_HEAP_SAMPLER_REGEN=1 cargo build -p libdd-heap-sampler\` locally and commit the result."
48+
exit 1
49+
fi

Cargo.lock

Lines changed: 58 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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
[features]
19+
# Forward live-heap (retained-heap) tracking down to the sampler. Off by
20+
# default: a bare SampledAllocator does allocation profiling only. Enable
21+
# to flag allocations and sample frees. See libdd-heap-sampler.
22+
live-heap = ["libdd-heap-sampler/live-heap"]
23+
24+
[dependencies]
25+
libdd-heap-sampler = { path = "../libdd-heap-sampler" }
26+
27+
[dev-dependencies]
28+
criterion = "0.5.1"
29+
30+
[[bench]]
31+
name = "sampler_overhead"
32+
harness = false

libdd-heap-allocator/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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:
19+
20+
```rust
21+
#[global_allocator]
22+
static ALLOC: SampledAllocator<MyAllocator> = SampledAllocator::new(MyAllocator::new());
23+
```
24+
25+
For profiling, prefer wrapping the allocator that is actually installed as the
26+
process global allocator. Heap profiling is most useful when all allocations in
27+
the process pass through the sampled wrapper.
28+
29+
See [`examples/usdt_demo.rs`](examples/usdt_demo.rs) for a runnable demo that fires USDT probes in a loop for `bpftrace` to observe.
30+
31+
## Benchmarking sampler overhead
32+
33+
The `sampler_overhead` Criterion benchmark measures the allocator/sampler hot path without installing `SampledAllocator` as the process global allocator. It compares direct `System` allocation, `SampledAllocator<System>`, a no-op allocator, `SampledAllocator<NoopAllocator>`, and direct sampler calls.
34+
35+
```sh
36+
cargo bench -p libdd-heap-allocator --bench sampler_overhead
37+
```
38+
39+
One quick validation run produced these fast-path results:
40+
41+
| Size | Base: `System` alloc/free | Sampled fast path | Overhead | Overhead % |
42+
|---:|---:|---:|---:|---:|
43+
| 16 B | 5.9719 ns | 10.916 ns | +4.9441 ns | +82.8% |
44+
| 64 B | 5.9309 ns | 12.405 ns | +6.4741 ns | +109.2% |
45+
| 256 B | 5.9639 ns | 10.827 ns | +4.8631 ns | +81.5% |
46+
| 4096 B | 23.237 ns | 29.402 ns | +6.1650 ns | +26.5% |
47+
| 65536 B | 23.880 ns | 28.496 ns | +4.6160 ns | +19.3% |
48+
49+
The no-op allocator comparison isolates the wrapper/sampler fast path at roughly **+5–6 ns per alloc/free pair**.

0 commit comments

Comments
 (0)