Skip to content

Commit 061d9fc

Browse files
feat(profiling): add heap-live allocation tracking to Profile
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9e8a71a commit 061d9fc

File tree

13 files changed

+823
-44
lines changed

13 files changed

+823
-44
lines changed

datadog-profiling-replayer/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ fn main() -> anyhow::Result<()> {
209209

210210
let before = Instant::now();
211211
for (timestamp, sample) in samples {
212-
outprof.try_add_sample(sample, timestamp)?;
212+
outprof.try_add_sample(sample, timestamp, None)?;
213213
}
214214
let duration = before.elapsed();
215215

examples/ffi/exporter.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ int main(int argc, char *argv[]) {
6666
.values = {&value, 1},
6767
.labels = {&label, 1},
6868
};
69-
auto add_result = ddog_prof_Profile_add(profile.get(), sample, 0);
69+
auto add_result = ddog_prof_Profile_add(profile.get(), sample, 0, 0);
7070
if (add_result.tag != DDOG_PROF_PROFILE_RESULT_OK) {
7171
print_error("Failed to add sample to profile: ", add_result.err);
7272
ddog_Error_drop(&add_result.err);

examples/ffi/exporter_manager.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ ddog_prof_Profile *create_profile_with_sample(void) {
6060
};
6161

6262
// Pass 0 as the timestamp parameter
63-
ddog_prof_Profile_Result add_result = ddog_prof_Profile_add(profile, sample, 0);
63+
ddog_prof_Profile_Result add_result = ddog_prof_Profile_add(profile, sample, 0, 0);
6464
if (add_result.tag != DDOG_PROF_PROFILE_RESULT_OK) {
6565
print_error("Failed to add sample to profile", &add_result.err);
6666
ddog_Error_drop(&add_result.err);

examples/ffi/profiles.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ int main(void) {
6161
for (int i = 0; i < NUM_SAMPLES; i++) {
6262
label.num = i;
6363

64-
ddog_prof_Profile_Result add_result = ddog_prof_Profile_add(&profile, sample, 0);
64+
ddog_prof_Profile_Result add_result = ddog_prof_Profile_add(&profile, sample, 0, 0);
6565
if (add_result.tag != DDOG_PROF_PROFILE_RESULT_OK) {
6666
ddog_CharSlice message = ddog_Error_message(&add_result.err);
6767
fprintf(stderr, "%.*s", (int)message.len, message.ptr);

libdd-profiling-ffi/src/profiles/datatypes.rs

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -583,15 +583,26 @@ impl From<ProfileNewResult> for Result<Profile, Error> {
583583
/// The `profile` ptr must point to a valid Profile object created by this
584584
/// module.
585585
/// This call is _NOT_ thread-safe.
586+
///
587+
/// # Arguments
588+
/// * `track_ptr` - If non-zero, also track this allocation for heap-live
589+
/// profiling. The sample data is copied into owned storage and will be
590+
/// automatically injected during profile reset. Use 0 to skip tracking.
586591
#[must_use]
587592
#[no_mangle]
588593
pub unsafe extern "C" fn ddog_prof_Profile_add(
589594
profile: *mut Profile,
590595
sample: Sample,
591596
timestamp: Option<NonZeroI64>,
597+
track_ptr: usize,
592598
) -> ProfileResult {
593599
(|| {
594600
let profile = profile_ptr_to_inner(profile)?;
601+
let track = if track_ptr != 0 {
602+
Some(track_ptr as u64)
603+
} else {
604+
None
605+
};
595606
let uses_string_ids = sample
596607
.labels
597608
.first()
@@ -600,7 +611,7 @@ pub unsafe extern "C" fn ddog_prof_Profile_add(
600611
if uses_string_ids {
601612
profile.add_string_id_sample(sample.into(), timestamp)
602613
} else {
603-
profile.try_add_sample(sample.try_into()?, timestamp)
614+
profile.try_add_sample(sample.try_into()?, timestamp, track)
604615
}
605616
})()
606617
.context("ddog_prof_Profile_add failed")
@@ -930,6 +941,76 @@ pub unsafe extern "C" fn ddog_prof_Profile_reset(profile: *mut Profile) -> Profi
930941
.into()
931942
}
932943

944+
/// Enable heap-live allocation tracking on this profile. When enabled,
945+
/// calls to `ddog_prof_Profile_add` with a non-zero `track_ptr` will copy
946+
/// the sample data into owned storage. Tracked allocations are automatically
947+
/// injected during profile reset and survive across resets.
948+
///
949+
/// # Arguments
950+
/// * `profile` - A mutable reference to the profile.
951+
/// * `max_tracked` - Maximum number of allocations to track simultaneously.
952+
/// * `excluded_labels` - Label keys to strip from tracked allocations
953+
/// (e.g., high-cardinality labels like "span id").
954+
/// * `excluded_labels_len` - Number of elements in `excluded_labels`.
955+
/// * `alloc_size_idx` - Index of alloc-size in the sample values array.
956+
/// * `heap_live_samples_idx` - Index of heap-live-samples in the sample values array.
957+
/// * `heap_live_size_idx` - Index of heap-live-size in the sample values array.
958+
///
959+
/// # Safety
960+
/// The `profile` ptr must point to a valid Profile object created by this
961+
/// module. `excluded_labels` must be valid for `excluded_labels_len` elements.
962+
/// This call is _NOT_ thread-safe.
963+
#[no_mangle]
964+
pub unsafe extern "C" fn ddog_prof_Profile_enable_heap_live_tracking(
965+
profile: *mut Profile,
966+
max_tracked: usize,
967+
excluded_labels: *const CharSlice<'_>,
968+
excluded_labels_len: usize,
969+
alloc_size_idx: usize,
970+
heap_live_samples_idx: usize,
971+
heap_live_size_idx: usize,
972+
) -> ProfileResult {
973+
(|| {
974+
let profile = profile_ptr_to_inner(profile)?;
975+
let labels: Vec<&str> = if excluded_labels.is_null() || excluded_labels_len == 0 {
976+
Vec::new()
977+
} else {
978+
let slice = std::slice::from_raw_parts(excluded_labels, excluded_labels_len);
979+
slice
980+
.iter()
981+
.map(|cs| cs.try_to_utf8())
982+
.collect::<Result<Vec<_>, _>>()?
983+
};
984+
profile.enable_heap_live_tracking(
985+
max_tracked,
986+
&labels,
987+
alloc_size_idx,
988+
heap_live_samples_idx,
989+
heap_live_size_idx,
990+
);
991+
anyhow::Ok(())
992+
})()
993+
.context("ddog_prof_Profile_enable_heap_live_tracking failed")
994+
.into()
995+
}
996+
997+
/// Remove a tracked heap-live allocation by pointer. No-op if heap-live
998+
/// tracking is disabled or the pointer is not tracked.
999+
///
1000+
/// # Arguments
1001+
/// * `profile` - A mutable reference to the profile.
1002+
/// * `ptr` - The pointer value of the allocation to untrack.
1003+
///
1004+
/// # Safety
1005+
/// The `profile` ptr must point to a valid Profile object created by this
1006+
/// module. This call is _NOT_ thread-safe.
1007+
#[no_mangle]
1008+
pub unsafe extern "C" fn ddog_prof_Profile_untrack_allocation(profile: *mut Profile, ptr: usize) {
1009+
if let Ok(profile) = profile_ptr_to_inner(profile) {
1010+
profile.untrack_allocation(ptr as u64);
1011+
}
1012+
}
1013+
9331014
#[cfg(test)]
9341015
mod tests {
9351016
use super::*;
@@ -965,7 +1046,7 @@ mod tests {
9651046
labels: Slice::empty(),
9661047
};
9671048

968-
let result = Result::from(ddog_prof_Profile_add(&mut profile, sample, None));
1049+
let result = Result::from(ddog_prof_Profile_add(&mut profile, sample, None, 0));
9691050
result.unwrap_err();
9701051
ddog_prof_Profile_drop(&mut profile);
9711052
Ok(())
@@ -1013,7 +1094,7 @@ mod tests {
10131094
labels: Slice::from(&labels),
10141095
};
10151096

1016-
Result::from(ddog_prof_Profile_add(&mut profile, sample, None))?;
1097+
Result::from(ddog_prof_Profile_add(&mut profile, sample, None, 0))?;
10171098
assert_eq!(
10181099
profile
10191100
.inner
@@ -1023,7 +1104,7 @@ mod tests {
10231104
1
10241105
);
10251106

1026-
Result::from(ddog_prof_Profile_add(&mut profile, sample, None))?;
1107+
Result::from(ddog_prof_Profile_add(&mut profile, sample, None, 0))?;
10271108
assert_eq!(
10281109
profile
10291110
.inner
@@ -1099,7 +1180,7 @@ mod tests {
10991180
labels: Slice::from(labels.as_slice()),
11001181
};
11011182

1102-
Result::from(ddog_prof_Profile_add(&mut profile, main_sample, None)).unwrap();
1183+
Result::from(ddog_prof_Profile_add(&mut profile, main_sample, None, 0)).unwrap();
11031184
assert_eq!(
11041185
profile
11051186
.inner
@@ -1109,7 +1190,7 @@ mod tests {
11091190
1
11101191
);
11111192

1112-
Result::from(ddog_prof_Profile_add(&mut profile, test_sample, None)).unwrap();
1193+
Result::from(ddog_prof_Profile_add(&mut profile, test_sample, None, 0)).unwrap();
11131194
assert_eq!(
11141195
profile
11151196
.inner

libdd-profiling/benches/add_samples.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ pub fn bench_add_sample_vs_add2(c: &mut Criterion) {
129129
values: &values,
130130
labels: vec![],
131131
};
132-
black_box(profile.try_add_sample(sample, None)).unwrap();
132+
black_box(profile.try_add_sample(sample, None, None)).unwrap();
133133
}
134134
black_box(profile.only_for_testing_num_aggregated_samples())
135135
})

libdd-profiling/examples/profiles.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ fn main() {
4949
// Intentionally use the current time.
5050
let mut profile = Profile::try_new(&sample_types, Some(period)).unwrap();
5151

52-
match profile.try_add_sample(sample, None) {
52+
match profile.try_add_sample(sample, None, None) {
5353
Ok(_) => {}
5454
Err(_) => exit(1),
5555
}

libdd-profiling/src/cxx.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ impl Profile {
410410
};
411411

412412
// Profile interns the strings
413-
self.inner.try_add_sample(api_sample, None)?;
413+
self.inner.try_add_sample(api_sample, None, None)?;
414414
Ok(())
415415
}
416416

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use crate::api;
5+
use crate::internal::owned_types::{OwnedFrame, OwnedLabel};
6+
use std::collections::HashMap;
7+
use std::hash::BuildHasherDefault;
8+
9+
/// Tracks live heap allocations inside a Profile.
10+
/// Stores owned copies of sample data (frames, labels, values) so tracked
11+
/// allocations survive across profile resets. Injected into the Profile's
12+
/// observations automatically before serialization via
13+
/// `reset_and_return_previous()`.
14+
type FxBuildHasher = BuildHasherDefault<rustc_hash::FxHasher>;
15+
16+
pub(crate) struct HeapLiveState {
17+
pub tracked: HashMap<u64, TrackedAlloc, FxBuildHasher>,
18+
pub max_tracked: usize,
19+
pub excluded_labels: Vec<Box<str>>,
20+
/// Index of alloc-size in the sample values array (to read allocation size).
21+
alloc_size_idx: usize,
22+
/// Index of heap-live-samples in the sample values array (to write 1).
23+
heap_live_samples_idx: usize,
24+
/// Index of heap-live-size in the sample values array (to write the size).
25+
heap_live_size_idx: usize,
26+
/// Total number of values per sample.
27+
num_values: usize,
28+
}
29+
30+
/// A single tracked live allocation with owned frame/label/values data.
31+
pub(crate) struct TrackedAlloc {
32+
pub frames: Vec<OwnedFrame>,
33+
pub labels: Vec<OwnedLabel>,
34+
pub values: Vec<i64>,
35+
}
36+
37+
impl HeapLiveState {
38+
pub fn new(
39+
max_tracked: usize,
40+
excluded_labels: &[&str],
41+
alloc_size_idx: usize,
42+
heap_live_samples_idx: usize,
43+
heap_live_size_idx: usize,
44+
num_values: usize,
45+
) -> Self {
46+
Self {
47+
tracked: HashMap::with_capacity_and_hasher(max_tracked, FxBuildHasher::default()),
48+
max_tracked,
49+
excluded_labels: excluded_labels.iter().map(|s| Box::from(*s)).collect(),
50+
alloc_size_idx,
51+
heap_live_samples_idx,
52+
heap_live_size_idx,
53+
num_values,
54+
}
55+
}
56+
57+
/// Track a new allocation. Copies borrowed strings from the sample into
58+
/// owned storage. Constructs heap-live-only values: all zeros except
59+
/// heap-live-samples=1 and heap-live-size=alloc_size.
60+
/// Returns false if tracker is full.
61+
pub fn track(&mut self, ptr: u64, sample: &api::Sample) -> bool {
62+
if self.tracked.len() >= self.max_tracked {
63+
return false;
64+
}
65+
let alloc = TrackedAlloc::from_api_sample(
66+
sample,
67+
&self.excluded_labels,
68+
self.alloc_size_idx,
69+
self.heap_live_samples_idx,
70+
self.heap_live_size_idx,
71+
self.num_values,
72+
);
73+
self.tracked.insert(ptr, alloc);
74+
true
75+
}
76+
77+
/// Remove a tracked allocation. No-op if ptr is not tracked.
78+
pub fn untrack(&mut self, ptr: u64) {
79+
self.tracked.remove(&ptr);
80+
}
81+
}
82+
83+
impl TrackedAlloc {
84+
fn from_api_sample(
85+
sample: &api::Sample,
86+
excluded_labels: &[Box<str>],
87+
alloc_size_idx: usize,
88+
heap_live_samples_idx: usize,
89+
heap_live_size_idx: usize,
90+
num_values: usize,
91+
) -> Self {
92+
let frames = sample
93+
.locations
94+
.iter()
95+
.map(|loc| OwnedFrame {
96+
function_name: loc.function.name.into(),
97+
filename: loc.function.filename.into(),
98+
line: loc.line,
99+
})
100+
.collect();
101+
102+
let labels = sample
103+
.labels
104+
.iter()
105+
.filter(|l| !excluded_labels.iter().any(|ex| ex.as_ref() == l.key))
106+
.map(|l| OwnedLabel {
107+
key: l.key.into(),
108+
str_value: l.str.into(),
109+
num: l.num,
110+
num_unit: l.num_unit.into(),
111+
})
112+
.collect();
113+
114+
// Construct heap-live-only values: all zeros except the heap-live
115+
// fields. The allocation size is read from the original sample's
116+
// alloc-size slot.
117+
let alloc_size = sample.values.get(alloc_size_idx).copied().unwrap_or(0);
118+
let mut values = vec![0i64; num_values];
119+
values[heap_live_samples_idx] = 1;
120+
values[heap_live_size_idx] = alloc_size;
121+
122+
TrackedAlloc {
123+
frames,
124+
labels,
125+
values,
126+
}
127+
}
128+
}

libdd-profiling/src/internal/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
mod endpoint_stats;
55
mod endpoints;
66
mod function;
7+
pub(crate) mod heap_live;
78
mod label;
89
mod location;
910
mod mapping;

0 commit comments

Comments
 (0)