Skip to content

Commit 1712f68

Browse files
feat(profiling): add heap-live profiling for memory leak detection
Track allocations that survive across profile exports using heap-live-samples and heap-live-size sample types. Samples are emitted in batches at export time. Enabled via DD_PROFILING_HEAP_LIVE_ENABLED when allocation profiling is active. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent de9c414 commit 1712f68

15 files changed

Lines changed: 579 additions & 35 deletions

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

profiling/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ cfg-if = { version = "1.0" }
2222
cpu-time = { version = "1.0" }
2323
chrono = { version = "0.4" }
2424
crossbeam-channel = { version = "0.5", default-features = false, features = ["std"] }
25+
dashmap = { version = "6.1" }
2526
http = { version = "1.4" }
2627
libdd-alloc = { git = "https://github.com/DataDog/libdatadog", tag = "v27.0.0" }
2728
libdd-profiling = { git = "https://github.com/DataDog/libdatadog", tag = "v27.0.0" }

profiling/src/allocation/allocation_ge84.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::allocation::{allocation_profiling_stats_should_collect, collect_allocation};
1+
use crate::allocation::{allocation_profiling_stats_should_collect, collect_allocation, free_allocation};
22
use crate::bindings as zend;
33
use crate::PROFILER_NAME;
44
use core::ptr;
@@ -286,7 +286,7 @@ unsafe extern "C" fn alloc_prof_malloc(len: size_t) -> *mut c_void {
286286
}
287287

288288
if allocation_profiling_stats_should_collect(len) {
289-
collect_allocation(len);
289+
collect_allocation(ptr, len);
290290
}
291291

292292
ptr
@@ -316,6 +316,11 @@ unsafe fn alloc_prof_orig_alloc(len: size_t) -> *mut c_void {
316316
/// custom handlers won't be installed. We cannot just point to the original
317317
/// `zend::_zend_mm_free()` as the function definitions differ.
318318
unsafe extern "C" fn alloc_prof_free(ptr: *mut c_void) {
319+
// Check if this was a tracked allocation (before freeing!)
320+
if !ptr.is_null() {
321+
free_allocation(ptr);
322+
}
323+
319324
tls_zend_mm_state_get!(free)(ptr);
320325
}
321326

@@ -348,12 +353,21 @@ unsafe extern "C" fn alloc_prof_realloc(prev_ptr: *mut c_void, len: size_t) -> *
348353

349354
// during startup, minit, rinit, ... current_execute_data is null
350355
// we are only interested in allocations during userland operations
351-
if zend::ddog_php_prof_get_current_execute_data().is_null() || ptr::eq(ptr, prev_ptr) {
356+
if zend::ddog_php_prof_get_current_execute_data().is_null() {
352357
return ptr;
353358
}
354359

355-
if allocation_profiling_stats_should_collect(len) {
356-
collect_allocation(len);
360+
// If pointer changed, treat as free(old) + alloc(new)
361+
if !ptr::eq(ptr, prev_ptr) {
362+
// Untrack the old allocation if it was tracked
363+
if !prev_ptr.is_null() {
364+
free_allocation(prev_ptr);
365+
}
366+
367+
// Sample the new allocation
368+
if allocation_profiling_stats_should_collect(len) {
369+
collect_allocation(ptr, len);
370+
}
357371
}
358372

359373
ptr

profiling/src/allocation/allocation_le83.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::allocation::{allocation_profiling_stats_should_collect, collect_allocation};
1+
use crate::allocation::{allocation_profiling_stats_should_collect, collect_allocation, free_allocation};
22
use crate::bindings::{
33
self as zend, datadog_php_install_handler, datadog_php_zif_handler,
44
ddog_php_prof_copy_long_into_zval,
@@ -300,7 +300,7 @@ unsafe extern "C" fn alloc_prof_malloc(len: size_t) -> *mut c_void {
300300
}
301301

302302
if allocation_profiling_stats_should_collect(len) {
303-
collect_allocation(len);
303+
collect_allocation(ptr, len);
304304
}
305305

306306
ptr
@@ -330,6 +330,11 @@ unsafe fn alloc_prof_orig_alloc(len: size_t) -> *mut c_void {
330330
/// custom handlers won't be installed. We cannot just point to the original
331331
/// `zend::_zend_mm_free()` as the function definitions differ.
332332
unsafe extern "C" fn alloc_prof_free(ptr: *mut c_void) {
333+
// Check if this was a tracked allocation (before freeing!)
334+
if !ptr.is_null() {
335+
free_allocation(ptr);
336+
}
337+
333338
tls_zend_mm_state_get!(free)(ptr);
334339
}
335340

@@ -358,12 +363,21 @@ unsafe extern "C" fn alloc_prof_realloc(prev_ptr: *mut c_void, len: size_t) -> *
358363

359364
// during startup, minit, rinit, ... current_execute_data is null
360365
// we are only interested in allocations during userland operations
361-
if zend::ddog_php_prof_get_current_execute_data().is_null() || ptr::eq(ptr, prev_ptr) {
366+
if zend::ddog_php_prof_get_current_execute_data().is_null() {
362367
return ptr;
363368
}
364369

365-
if allocation_profiling_stats_should_collect(len) {
366-
collect_allocation(len);
370+
// If pointer changed, treat as free(old) + alloc(new)
371+
if !ptr::eq(ptr, prev_ptr) {
372+
// Untrack the old allocation if it was tracked
373+
if !prev_ptr.is_null() {
374+
free_allocation(prev_ptr);
375+
}
376+
377+
// Sample the new allocation
378+
if allocation_profiling_stats_should_collect(len) {
379+
collect_allocation(ptr, len);
380+
}
367381
}
368382

369383
ptr

profiling/src/allocation/mod.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,13 @@ impl AllocationProfilingStats {
135135
}
136136
}
137137

138+
/// Collect an allocation sample and optionally track it for live heap profiling.
139+
///
140+
/// # Arguments
141+
/// * `ptr` - The pointer returned by the allocator (used for live heap tracking)
142+
/// * `len` - The size of the allocation in bytes
138143
#[cold]
139-
pub fn collect_allocation(len: size_t) {
144+
pub fn collect_allocation(ptr: *mut c_void, len: size_t) {
140145
if let Some(profiler) = Profiler::get() {
141146
// Check if there's a pending time interrupt that we can handle now
142147
// instead of waiting for an interrupt handler. This is slightly more
@@ -150,6 +155,7 @@ pub fn collect_allocation(len: size_t) {
150155
unsafe {
151156
profiler.collect_allocations(
152157
zend::ddog_php_prof_get_current_execute_data(),
158+
ptr,
153159
1_i64,
154160
len as i64,
155161
(interrupt_count > 0).then_some(interrupt_count),
@@ -158,6 +164,14 @@ pub fn collect_allocation(len: size_t) {
158164
}
159165
}
160166

167+
/// Called when memory is freed. If this pointer was tracked for live heap,
168+
/// sends the deallocation sample to cancel out the original allocation.
169+
pub fn free_allocation(ptr: *mut c_void) {
170+
if let Some(profiler) = Profiler::get() {
171+
profiler.free_allocation(ptr);
172+
}
173+
}
174+
161175
#[cfg(not(php_zend_mm_set_custom_handlers_ex))]
162176
pub fn alloc_prof_startup() {
163177
allocation_le83::alloc_prof_startup();

profiling/src/config.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ pub struct SystemSettings {
4545
pub profiling_experimental_cpu_time_enabled: bool,
4646
pub profiling_allocation_enabled: bool,
4747
pub profiling_allocation_sampling_distance: NonZeroU32,
48+
pub profiling_heap_live_enabled: bool,
4849
pub profiling_timeline_enabled: bool,
4950
pub profiling_exception_enabled: bool,
5051
pub profiling_exception_message_enabled: bool,
@@ -69,6 +70,7 @@ impl SystemSettings {
6970
profiling_experimental_cpu_time_enabled: false,
7071
profiling_allocation_enabled: false,
7172
profiling_allocation_sampling_distance: NonZeroU32::MAX,
73+
profiling_heap_live_enabled: false,
7274
profiling_timeline_enabled: false,
7375
profiling_exception_enabled: false,
7476
profiling_exception_message_enabled: false,
@@ -98,6 +100,7 @@ impl SystemSettings {
98100
profiling_experimental_cpu_time_enabled: profiling_experimental_cpu_time_enabled(),
99101
profiling_allocation_enabled: profiling_allocation_enabled(),
100102
profiling_allocation_sampling_distance: profiling_allocation_sampling_distance(),
103+
profiling_heap_live_enabled: profiling_heap_live_enabled(),
101104
profiling_timeline_enabled: profiling_timeline_enabled(),
102105
profiling_exception_enabled: profiling_exception_enabled(),
103106
profiling_exception_message_enabled: profiling_exception_message_enabled(),
@@ -405,6 +408,7 @@ pub(crate) enum ConfigId {
405408
ProfilingExperimentalCpuTimeEnabled,
406409
ProfilingAllocationEnabled,
407410
ProfilingAllocationSamplingDistance,
411+
ProfilingHeapLiveEnabled,
408412
ProfilingTimelineEnabled,
409413
ProfilingExceptionEnabled,
410414
ProfilingExceptionMessageEnabled,
@@ -437,6 +441,7 @@ impl ConfigId {
437441
ProfilingExperimentalCpuTimeEnabled => b"DD_PROFILING_EXPERIMENTAL_CPU_TIME_ENABLED\0",
438442
ProfilingAllocationEnabled => b"DD_PROFILING_ALLOCATION_ENABLED\0",
439443
ProfilingAllocationSamplingDistance => b"DD_PROFILING_ALLOCATION_SAMPLING_DISTANCE\0",
444+
ProfilingHeapLiveEnabled => b"DD_PROFILING_HEAP_LIVE_ENABLED\0",
440445
ProfilingTimelineEnabled => b"DD_PROFILING_TIMELINE_ENABLED\0",
441446
ProfilingExceptionEnabled => b"DD_PROFILING_EXCEPTION_ENABLED\0",
442447
ProfilingExceptionMessageEnabled => b"DD_PROFILING_EXCEPTION_MESSAGE_ENABLED\0",
@@ -475,6 +480,7 @@ static DEFAULT_SYSTEM_SETTINGS: SystemSettings = SystemSettings {
475480
profiling_allocation_enabled: true,
476481
// SAFETY: value is > 0.
477482
profiling_allocation_sampling_distance: unsafe { NonZeroU32::new_unchecked(1024 * 4096) },
483+
profiling_heap_live_enabled: false,
478484
profiling_timeline_enabled: true,
479485
profiling_exception_enabled: true,
480486
profiling_exception_message_enabled: false,
@@ -553,6 +559,17 @@ unsafe fn profiling_allocation_sampling_distance() -> NonZeroU32 {
553559
unsafe { NonZeroU32::new_unchecked(int) }
554560
}
555561

562+
/// # Safety
563+
/// This function must only be called after config has been initialized in
564+
/// rinit, and before it is uninitialized in mshutdown.
565+
unsafe fn profiling_heap_live_enabled() -> bool {
566+
profiling_allocation_enabled()
567+
&& get_system_bool(
568+
ProfilingHeapLiveEnabled,
569+
DEFAULT_SYSTEM_SETTINGS.profiling_heap_live_enabled,
570+
)
571+
}
572+
556573
/// # Safety
557574
/// This function must only be called after config has been initialized in
558575
/// rinit, and before it is uninitialized in mshutdown.
@@ -1014,6 +1031,18 @@ pub(crate) fn minit(module_number: libc::c_int) {
10141031
displayer: None,
10151032
env_config_fallback: None,
10161033
},
1034+
zai_config_entry {
1035+
id: transmute::<ConfigId, u16>(ProfilingHeapLiveEnabled),
1036+
name: ProfilingHeapLiveEnabled.env_var_name(),
1037+
type_: ZAI_CONFIG_TYPE_BOOL,
1038+
default_encoded_value: ZaiStr::literal(b"0\0"),
1039+
aliases: ptr::null_mut(),
1040+
aliases_count: 0,
1041+
ini_change: Some(zai_config_system_ini_change),
1042+
parser: None,
1043+
displayer: None,
1044+
env_config_fallback: None,
1045+
},
10171046
zai_config_entry {
10181047
id: transmute::<ConfigId, u16>(ProfilingTimelineEnabled),
10191048
name: ProfilingTimelineEnabled.env_var_name(),

profiling/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,19 @@ unsafe extern "C" fn minfo(module_ptr: *mut zend::ModuleEntry) {
860860
no_all
861861
}
862862
);
863+
zend::php_info_print_table_row(
864+
2,
865+
c"Heap Live Profiling Enabled".as_ptr(),
866+
if system_settings.profiling_heap_live_enabled {
867+
yes
868+
} else if !system_settings.profiling_allocation_enabled {
869+
c"false (requires allocation profiling)".as_ptr()
870+
} else if system_settings.profiling_enabled {
871+
no
872+
} else {
873+
no_all
874+
},
875+
);
863876
zend::php_info_print_table_row(
864877
2,
865878
c"Timeline Enabled".as_ptr(),

profiling/src/profiling/backtrace.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::profiling::stack_walking::ZendFrame;
22
use core::ops::Deref;
33

4-
#[derive(Debug)]
4+
#[derive(Clone, Debug)]
55
pub struct Backtrace {
66
frames: Vec<ZendFrame>,
77
}

0 commit comments

Comments
 (0)