Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4f1ddfb
JFR event types and emission for reference chains (#796)
jbachorik Sep 22, 2026
1700bf3
Implement reference-chain tracking and the leak-signal engine
jbachorik Sep 17, 2026
3ac47ce
Deallocate GetObjectsWithTags results in hopLabelClassFor
jbachorik Sep 17, 2026
96dc67b
Fix clang scan-build findings in the tracker and engine
jbachorik Sep 17, 2026
d0b175b
Fix review findings in the tracker and liveness engine
jbachorik Sep 17, 2026
c2c5143
Clear the klass-population scratch in the test-reset seam
jbachorik Sep 18, 2026
5c1717c
Replace uncommitted-plan and line-number references with symbol refs
jbachorik Sep 18, 2026
e1da26d
Adapt tracker to merged ReferenceChainHop; drop Jira refs
jbachorik Sep 18, 2026
d744989
Make comments layer-local and drop stale plan refs from tests
jbachorik Sep 18, 2026
68ddab6
Drop forward references to the profiler-side writer
jbachorik Sep 21, 2026
a8bf8b5
Restore the merged-hop event API in the reference-chain tests
jbachorik Sep 21, 2026
1602a86
Drop design-doc reference from the population table comment
jbachorik Sep 21, 2026
9f141e3
Drop the last cross-layer references from the test comments
jbachorik Sep 21, 2026
2937dba
Fix epoch rollback, class-tag races and weak-ref handling in the live…
jbachorik Sep 23, 2026
5f9ced3
Fix reference-chain pass termination, leak-tag root handling and batc…
jbachorik Sep 23, 2026
0cefc61
Wire the reference-chain tracker into the profiler lifecycle
jbachorik Sep 23, 2026
bd6e6b0
Split referenceChains into focused translation units with concise com…
jbachorik Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ddprof-lib/src/main/cpp/callTraceHashTable.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class CallTraceHashTable {
// - ACQUIRE loads in collect(), put(), and putWithExistingId()
// Required for correct visibility on weakly-ordered architectures (aarch64).
LongHashTable* _table;

volatile u64 _overflow;

u64 calcHash(int num_frames, ASGCT_CallFrame *frames, bool truncated);
Expand Down
1 change: 1 addition & 0 deletions ddprof-lib/src/main/cpp/callTraceStorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

#include <unistd.h>
#include "callTraceStorage.h"
#include "counters.h"
#include "log.h"
Expand Down
68 changes: 68 additions & 0 deletions ddprof-lib/src/main/cpp/classTagAllocator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright 2026, Datadog, Inc.
* SPDX-License-Identifier: Apache-2.0
*/

#ifndef _CLASS_TAG_ALLOCATOR_H
#define _CLASS_TAG_ALLOCATOR_H

#include "arch.h"
#include <jni.h>

// Process-wide, negative JVMTI class-object tag allocator, shared by
// ReferenceChainTracker (which tags every loaded class's own jclass object
// via SetTag - see resolveLoadedClasses(), referenceChains.cpp) and
// LivenessTracker (which needs a stable per-class identifier independent of
// Profiler::classMap()'s dictionary id - see KlassPopulationEntry::
// stable_class_tag's own comment, livenessTracker.h, for why: that
// dictionary can be compacted/regenerated, silently reassigning the same
// class a different id at different points in the process's life, breaking
// any attempt to correlate a klass_id LivenessTracker reports as growing
// against ReferenceChainTracker::FrontierEntry::referrer_klass values
// recorded at a different time).
//
// A single shared counter, not one independently owned by each subsystem,
// for two reasons, both load-bearing:
// 1. Two independent counters could otherwise hand out the SAME numeric
// value to TWO DIFFERENT classes (one minted by each subsystem for a
// class the other has not seen yet), making any cross-subsystem
// comparison meaningless.
// 2. Class tags must stay strictly NEGATIVE:
// ReferenceChainTracker::heapReferenceCallback() (referenceChains.cpp)
// uses `*tag_ptr < 0` to distinguish "this heap-walk-visited object is a
// pre-tagged class object" from an ordinary admitted instance (always
// tagged with a positive value via nextTag()). A class tagged by a
// counter that does not preserve this sign convention would be
// misidentified as an ordinary object and incorrectly admitted into the
// frontier table - a real correctness bug, not just a matching
// inconvenience.
//
// Deliberately a plain header-only function (Meyer's-singleton pattern,
// exactly like LivenessTracker::instance()/ReferenceChainTracker::
// instance()'s own lazy-static singletons) rather than a member of either
// singleton class: ReferenceChainTracker already depends on LivenessTracker
// (referenceChains.cpp includes livenessTracker.h and calls into it), so
// putting this counter inside either one and having the other call into it
// would introduce a circular dependency between the two headers.
namespace ClassTagAllocator {

inline volatile jlong &magnitude() {
static volatile jlong m = 1;
return m;
}

// Hands out a fresh negative class tag - see this file's own header comment
// for why negative, and why this must be the only place in the process that
// mints one.
inline jlong next() { return -atomicIncRelaxed(magnitude(), (jlong)1); }

// Test-only: resets the shared counter back to its starting value. Without
// this, gtest cases that assert on exact tag values (e.g. "the first class
// tagged gets -1") would see values keep climbing across every TEST_F in the
// same gtest binary, since this counter is genuinely process-wide (shared
// with LivenessTracker) rather than per-ReferenceChainTracker-instance.
inline void resetForTest() { magnitude() = 1; }

} // namespace ClassTagAllocator

#endif
39 changes: 39 additions & 0 deletions ddprof-lib/src/main/cpp/counters.h
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,45 @@
* signal for spotting a recurrence. */ \
X(METADATA_TREE_NULL_CHILD, "metadata_tree_null_child") \
X(METADATA_TREE_DEPTH_EXCEEDED, "metadata_tree_depth_exceeded") \
/* A resolved datadog.ReferenceChain could not be cached in \
* ReferenceChainTracker::_resolved_chains (referenceChains.h): a brand-new \
* leak-candidate klass arrived with the cache already at \
* MAX_RESOLVED_CHAINS, so its chain is dropped rather than evicting some \
* other still-live sample's chain. See that constant's own comment. */ \
X(REFERENCE_CHAIN_EVENTS_DROPPED, "reference_chain_events_dropped") \
/* ReferenceChainTracker::releaseSearchTags() (referenceChains.cpp) failed \
* to call GetObjectsWithTags() for at least one batch - the search's tag \
* release is retried on a later call rather than proceeding, but this \
* counts how often that retry path is taken. */ \
X(REFERENCE_CHAIN_TAG_RELEASE_FAILED, "reference_chain_tag_release_failed") \
/* The profiler-side reference-chain writer could not acquire a \
* sample-record lock within its bounded retry budget and dropped the \
* already-dequeued datadog.ReferenceChain event for this dump - not \
* permanently lost, since ReferenceChainTracker::_resolved_chains (see \
* REFERENCE_CHAIN_EVENTS_DROPPED above) keeps the resolved chain cached \
* and re-emits it on a later dump while the leak candidate is still \
* live. */ \
X(REFERENCE_CHAIN_WRITE_DROPPED, "reference_chain_write_dropped") \
/* FrontierTable's own calloc/realloc-backed storage (referenceChains.cpp) - \
* outside NMT's visibility since it bypasses os::malloc, so this is the only \
* way to attribute its native RSS contribution. */ \
X(REFERENCE_CHAIN_FRONTIER_TABLE_BYTES, "reference_chain_frontier_table_bytes") \
X(REFERENCE_CHAIN_FRONTIER_TABLE_CAPACITY, "reference_chain_frontier_table_capacity") \
X(REFERENCE_CHAIN_CANDIDATE_COUNT, "reference_chain_candidate_count") \
X(REFERENCE_CHAIN_CANDIDATES_FOUND, "reference_chain_candidates_found") \
/* admitStaticFieldRoots() per-class non-static quota: non-STATIC_FIELD \
* edges (CONSTANT_POOL, INTERFACE, SUPERCLASS, CLASS_LOADER, ...) that \
* were dropped because the class already hit \
* STATIC_FIELD_SWEEP_NON_STATIC_CAP_PER_CLASS. Total drops across all \
* classes/laps — compare against kind_counts (k9 total) to gauge how \
* much CP pressure the quota is absorbing. */ \
X(REFERENCE_CHAIN_STATIC_SWEEP_NON_STATIC_DROPPED, "reference_chain_static_sweep_non_static_dropped") \
/* Incremented once per class that hit the non-static cap at least once \
* in a lap (on the first drop for that class). Distinguishes "a few fat \
* outlier classes dropping many edges" from "systematic drops across \
* almost all classes" — if this tracks the total class count per lap, \
* the cap is too low; if it stays near zero, the cap is fine. */ \
X(REFERENCE_CHAIN_STATIC_SWEEP_CLASSES_CAPPED, "reference_chain_static_sweep_classes_capped") \
DD_COUNTER_TABLE_FAULT_INJECTION(X) \
DD_COUNTER_TABLE_FI_DEBUG(X) \
DD_COUNTER_TABLE_DEBUG(X)
Expand Down
9 changes: 5 additions & 4 deletions ddprof-lib/src/main/cpp/flightRecorder.h
Original file line number Diff line number Diff line change
Expand Up @@ -581,20 +581,21 @@ class FlightRecorder {

// Mirrors recordHeapUsage()'s shape exactly - ReferenceChainAbandonedEvent
// is not stack-sample-shaped (no tid/call_trace_id), same as HeapUsage.
// Called from Profiler::writeReferenceChainAbandoned() (profiler.cpp),
// Called from the profiler's dump-time abandoned-event drain,
// wired from Profiler::dump() the same way LivenessTracker::flush() is.
void recordReferenceChainAbandoned(int lock_index,
ReferenceChainAbandonedEvent *event);

// Mirrors recordReferenceChainAbandoned() above exactly, for
// ReferenceChainEvent instead. Called from Profiler::writeReferenceChain()
// (profiler.cpp), itself called from Profiler::dump()'s drain loop over
// ReferenceChainEvent instead. Called from the profiler's dump()-time
// writer, itself called from Profiler::dump()'s drain loop over
// the engine's resolved-chain cache snapshot: the BFS
// scheduling thread only caches resolved chains and each dump re-emits
// the cache, so chain events
// are written on dump()'s own thread, not from the tracker thread, and
// unlike recordReferenceChainAbandoned() (unbounded retry budget per
// event) the batch shares one deadline (writeReferenceChain()'s comment).
// event) the batch shares one deadline (see the writer's contract in
// Profiler - the drain batch, not each event, owns the retry budget).
void recordReferenceChain(int lock_index, ReferenceChainEvent *event);
};

Expand Down
Loading
Loading