feat(profiling): add heap-live profiling for memory leak detection#3623
Conversation
|
✨ Fix all issues with BitsAI or with Cursor
|
Benchmarks [ profiler ]Benchmark execution time: 2026-06-10 11:12:20 Comparing candidate commit 446a8d9 in PR branch Found 0 performance improvements and 9 performance regressions! Performance is the same for 19 metrics, 8 unstable metrics.
|
bc087f2 to
817465a
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3623 +/- ##
==========================================
+ Coverage 62.11% 62.20% +0.08%
==========================================
Files 141 141
Lines 13387 13387
Branches 1753 1753
==========================================
+ Hits 8315 8327 +12
+ Misses 4273 4263 -10
+ Partials 799 797 -2 see 2 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
Benchmarks [ tracer ]Benchmark execution time: 2026-06-10 08:43:23 Comparing candidate commit 703fec2 in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 194 metrics, 0 unstable metrics.
|
4aaffbf to
540cc19
Compare
0ecb066 to
79ff80d
Compare
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 <[email protected]>
- Use functional style (map + match) in collect_batched_heap_live_samples - Only create ProfileIndex when heap-live tracking is enabled - Replace 32 repetitive I/O profiling lines with a loop - Use filter_map in sample type filter method - Add early bail-out in free_allocation when heap-live is disabled Co-Authored-By: Claude Opus 4.5 <[email protected]>
Replace default SipHash with a simple bit-mixing hasher optimized for pointer addresses. Since pointers are already well-distributed, we use `ptr ^ (ptr >> 4)` instead of expensive cryptographic hashing. This reduces overhead in untrack_allocation() which is called on every free. Co-Authored-By: Claude Opus 4.5 <[email protected]>
d87cdf9 to
91f9d71
Compare
- Cargo.lock: drop unrelated lockfile churn so the diff vs master only contains entries cargo actually pulled in for dashmap. - SampleData / LiveHeapSample: Arc-share frames and labels so the in-flight sample message and the heap-live tracker share one allocation each, and re-emitting tracked entries on every export becomes Arc::clone instead of deep clone. - realloc: guard against NULL return so we don't free-track a still- live prev_ptr. - visibility: tighten heap-live helpers to pub(crate); drop the unused clear_live_heap_tracker / live_heap_tracker_len helpers. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
ProfileIndex is effectively a singleton per request (sample_types fixed at startup, tags resolved once per rinit), but it was being deep-cloned on every prepare_sample_message, every HashMap insert, every heap-live track, and every heap-live emission. Wrap it in Arc so: - SampleMessage::key and LiveHeapSample::key share one allocation. - HashMap<Arc<ProfileIndex>, InternalProfile> keys via Arc's Hash/Eq (which delegate to ProfileIndex), so behavior is unchanged. - The heap-live re-emission path becomes a pure Arc::clone instead of a deep Vec<ValueType> clone. Behavior-preserving refactor. Pairs with the existing Arc<Backtrace> / Arc<Vec<Label>> work on SampleData.
The previous implementation paid for a Profiler::get() (OnceLock atomic load) + AtomicPtr load + SystemSettings deref on every efree/erealloc, even when heap-live profiling was disabled. That overhead is likely behind the timeline-memory bench regression observed in the PoC, since that benchmark exercises the free path heavily without enabling heap-live. Add a static AtomicBool that mirrors profiling_heap_live_enabled and gate the alloc handlers (alloc_prof_free, alloc_prof_realloc) on it *before* touching Profiler::get(). When heap-live is off the off-path collapses to a single relaxed load + branch. The static is set in Profiler::new and cleared in Profiler::stop / Profiler::kill, so teardown is also safe (any late free during shutdown short-circuits cleanly). Profiler::is_heap_live_enabled now reads the same static so all gates agree on one source of truth, dropping the AtomicPtr/deref dance on the allocation-sampling hot path as well.
…unter DashMap::len() acquires a read-lock on all 16 shards per call, paying 16 lock acquisitions on every sampled allocation just for a racy cap check. Replace with a shared AtomicUsize that is incremented on insert and decremented on remove; a single Relaxed load serves the cap check with identical TOCTOU semantics. Co-Authored-By: Claude Sonnet 4.6 (1M context) <[email protected]>
…ce lock contention DashMap::iter() holds a per-shard read lock for each entry while in scope. With the previous code, handle_sample_message was called while the lock was still held, blocking concurrent efree calls on PHP threads for the full duration of a 4096-entry export iteration. Snapshot all values into a Vec first (cheap: just Arc::clone per field), then process with no locks held. Co-Authored-By: Claude Sonnet 4.6 (1M context) <[email protected]>
…n free/update methods Call sites in the allocation hooks already gate on HEAP_LIVE_ENABLED before calling free_allocation and update_allocation_size. The inner checks were dead code and misleading. Replace with debug_assert and a precondition doc comment. Co-Authored-By: Claude Sonnet 4.6 (1M context) <[email protected]>
The XOR-shift mix (ptr ^ (ptr >> 4)) leaves sequential bump-allocator addresses correlated in the high bits, clustering them onto the same DashMap shards under ZTS workloads. FxHasher's multiply-rotate mix fully avalanches bits, spreading sequential pointers evenly across all 16 shards. rustc-hash is already a direct dependency. Co-Authored-By: Claude Sonnet 4.6 (1M context) <[email protected]>
…eap-live sample values Replace fragile string matching on "heap-live-samples"/"heap-live-size" in collect_batched_heap_live_samples with SampleTypeFilter::filter(). A rename or typo in the type strings previously produced silent all-zero heap-live exports. Derive Clone on SampleTypeFilter and pass a clone to TimeCollector. Co-Authored-By: Claude Sonnet 4.6 (1M context) <[email protected]>
Previously, in-place reallocs called update_allocation_size() which mutated the tracked allocation_size from S0 to S1 in place. This broke the upscaling invariant: the Poisson estimator requires that the sampling decision was made at the size used in the upscale formula, but the allocation was sampled at S0 while the upscaler saw S1. Fix by treating in-place reallocs the same as pointer-moving reallocs: untrack the old allocation and re-sample at the new size. The sampling decision is then always made at the reported size, keeping the estimator unbiased. In-place reallocs are rare in PHP, so the performance impact is negligible. Remove update_allocation_size() from Profiler, allocation/mod.rs, and both allocation handler files entirely.
451c010 to
08f80e1
Compare
08f80e1 to
703fec2
Compare
I think we should yes. The Heap Live profiles show what triggered that allocation. Also: we are sharing the sample message with the allocation profiler, so once all that information is collected, we already have it anyway (it would be extra work to remove that). What would be nice is if in the backend we could go from a stack trace to the span in the tracer (it is possible the other way round, just not back), but I do not know if that is too much important at all. So TLDR: I am in favor of keeping that information, also because it is consistent with the allocation profiler. |
morrisonlevi
left a comment
There was a problem hiding this comment.
Code wise, it's looking okay to me. I've still not played much with its functionality. However, it's off by default so at this stage I'm willing to approve.
|
@ajohnston-knak version |
Description
Track allocations that survive across profile exports using
heap-live-samplesandheap-live-sizesample types. Samples are emitted in batches at export time.Enable via
DD_PROFILING_EXPERIMENTAL_HEAP_LIVE_ENABLEDordatadog.profiling.experimental_heap_live_enabled(default disabled), only works when allocation profiling is active.Note
This feature is default disabled and if you are running a SAPI like PHP-FPM or mod_php (Apache) that is purely request/response based, you do not get any value out of this. This feature is meant for long running process (CLI workers consuming messages, ReactPHP applications, FrankenPHP Worker Mode, ...).
Reviewer checklist
PROF-13688