Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8f1c030
Add JFR event types for reference chains
jbachorik Sep 17, 2026
3dfc7ca
Keep chain events inside the recording buffer; keep truncated labels
jbachorik Sep 17, 2026
2eae6f2
Drop transient and stale line-number references from comments
jbachorik Sep 18, 2026
a66056c
Address review: merge chain hops, trim comments, drop doc/jira refs
jbachorik Sep 18, 2026
d9f0786
Make comments layer-local: no references to later stack layers
jbachorik Sep 18, 2026
f9ec27d
Drop a later-layer class name from the arguments test comment
jbachorik Sep 21, 2026
584fec2
Wire reference chains into the profiler lifecycle and JNI API
jbachorik Sep 17, 2026
cc0b62a
Drop remaining Jira and cross-layer test references
jbachorik Sep 18, 2026
b4c0d06
referenceChains: harden leak-tag lifecycle and STW-safe invalidation
jbachorik Sep 28, 2026
9187abb
livenessTracker: leak-tag pool guards, exclusive track() fill, urgenc…
jbachorik Sep 28, 2026
c4dd668
os: serialize cgroup limit/usage access, cache usage leaf
jbachorik Sep 28, 2026
9ce7d88
misc: objectSampler start order, dump-failure debug log, Java seams p…
jbachorik Sep 28, 2026
0beeb21
livenessTracker: record zero samples for klasses whose population died
jbachorik Sep 28, 2026
5b61e7c
Drop orphaned referenceChains split-TU test includes
jbachorik Sep 28, 2026
1173f79
fuzz: point the FrontierTable target at the monolithic header
jbachorik Sep 29, 2026
c8d99af
referenceChains: port the fold-dropped frontier and chain-walk fixes
jbachorik Sep 30, 2026
fef180c
livenessTracker/os: port the fold-dropped medium-severity fixes
jbachorik Sep 30, 2026
e775f30
referenceChains tests: wire orphaned rc-3 suites, port gap regression…
jbachorik Sep 30, 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
20 changes: 20 additions & 0 deletions ddprof-lib/src/main/cpp/counters.h
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,26 @@
* and re-emits it on a later dump while the leak candidate is still \
* live. */ \
X(REFERENCE_CHAIN_WRITE_DROPPED, "reference_chain_write_dropped") \
/* LivenessTracker::releaseLeakTag() was called for a leak-tag slot that \
* is already free (zero/zero encoding) - a double release. The release is \
* dropped: pushing the index twice would let acquireLeakTag() hand the \
* same tag to two live objects. A nonzero rate here means the leak-tag \
* ownership accounting (tagLeakInstances()' tag-adoption branch) is \
* sharing one pool tag between entries. */ \
X(REFERENCE_CHAIN_LEAK_TAG_DOUBLE_RELEASE, "reference_chain_leak_tag_double_release") \
/* LivenessTracker urgency-boost observability (admitForTracking()): \
* ADMITS counts every 100% admission made while _urgent_tracking is set \
* and the table is below its cap; BACKED_OFF counts admissions at the \
* high-water mark, where the boost degrades to watched-tid-only and the \
* thread falls back to the configured subsample ratio. A persistently \
* rising BACKED_OFF means the urgency window is outpacing the cleanup \
* reaper. */ \
X(LIVENESS_URGENT_BOOST_ADMITS, "liveness_urgent_boost_admits") \
X(LIVENESS_URGENT_BOOST_BACKED_OFF, "liveness_urgent_boost_backed_off") \
/* Defensive cap: releaseLeakTag() found the free list full. Unreachable \
* by construction while every release is paired with an acquire; a \
* nonzero value means the acquire/release pairing is broken somewhere. */ \
X(REFERENCE_CHAIN_LEAK_TAG_RELEASE_OVERFLOW, "reference_chain_leak_tag_release_overflow") \
/* 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. */ \
Expand Down
217 changes: 217 additions & 0 deletions ddprof-lib/src/main/cpp/javaApi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,223 @@ Java_com_datadoghq_profiler_JavaProfiler_dumpContext(JNIEnv* env, jclass unused)
TEST_LOG("===> Context: tid:%lu, spanId=%lu, rootSpanId=%lu", OS::threadId(), spanId, rootSpanId);
}

// LivenessTracker/ReferenceChainTracker test seams. Unlike
// testlog()/dumpContext() above (harmless no-ops in release, via TEST_LOG's
// own release-mode expansion to nothing), these mutate real tracker state
// (tagging objects, seeding population history) - shipping them into a
// release build would let a caller corrupt the actual leak-detection state,
// not just add a silent no-op. Guarded out entirely instead, so they only
// exist in the debug build ddprof-test's `testdebug` Gradle task loads
// (`-DDEBUG`, see ConfigurationPresets.kt's configureDebug()) - never in the
// `-DNDEBUG` release build.
#ifdef DEBUG
#include "livenessTracker.h"
#include "referenceChains.h"
#include <vector>

extern "C" DLLEXPORT jboolean JNICALL
Java_com_datadoghq_profiler_JavaProfiler_setGcGenerationsEnabled0(
JNIEnv *env, jclass unused, jboolean enabled) {
LivenessTracker::instance()->setGcGenerationsForTest(enabled);
return JNI_TRUE;
}

extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_seedKlassPopulationSample0(
JNIEnv *env, jclass unused, jint klassId, jint count, jlong epoch) {
int slot;
bool created;
LivenessTracker::instance()->klassPopulationRecordForTest(
(u32)klassId, (u16)count, (u64)epoch, &slot, &created);
}

// Seeds one per-(klass, tid) trend sample - see tidTrendRecordForTest()'s
// own comment (livenessTracker.h) for the synthetic-flag exemption and the
// real-tid requirement scenarios must honor.
extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_seedTidTrendSample0(
JNIEnv *env, jclass unused, jint klassId, jint tid, jint count,
jlong epoch) {
LivenessTracker::instance()->tidTrendRecordForTest(
(u32)klassId, (jint)tid, (u32)count, (u64)epoch);
}

// Wires a real, caller-chosen live object in as klassId's leak-candidate
// representative, so a test-seeded slope signal (seedKlassPopulationSample0
// above) and a directly-tagged frontier root (tagAsReferenceChainRoot0
// below) can be joined into one deterministic end-to-end run of
// pollWatchedTargets()'s bridging step - without either LivenessTracker's
// real allocation sampler or ReferenceChainTracker's root-seeded walk ever
// running. Takes its own weak global ref (klassPopulationSetRepresentativeForTest()'s
// own contract, livenessTracker.h) rather than aliasing any handle the
// caller manages.
extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_setKlassPopulationRepresentativeForTest0(
JNIEnv *env, jclass unused, jint klassId, jobject representative) {
jweak rep = env->NewWeakGlobalRef(representative);
LivenessTracker::instance()->klassPopulationSetRepresentativeForTest(
env, (u32)klassId, rep);
}

extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_resetKlassPopulationForTest0(
JNIEnv *env, jclass unused) {
LivenessTracker::instance()->klassPopulationResetForTest();
}

extern "C" DLLEXPORT jintArray JNICALL
Java_com_datadoghq_profiler_JavaProfiler_selectLeakCandidateKlassIds0(
JNIEnv *env, jclass unused) {
KlassCandidate candidates[5];
int n = LivenessTracker::instance()->selectLeakCandidates(candidates, 5);
jintArray result = env->NewIntArray(n);
if (result == nullptr || n == 0) {
return result;
}
jint ids[5];
for (int i = 0; i < n; i++) {
ids[i] = (jint)candidates[i].klass_id;
}
env->SetIntArrayRegion(result, 0, n, ids);
return result;
}

extern "C" DLLEXPORT jlong JNICALL
Java_com_datadoghq_profiler_JavaProfiler_tagAsReferenceChainRoot0(
JNIEnv *env, jclass unused, jobject target) {
jvmtiEnv *jvmti = VM::jvmti();
if (jvmti == nullptr) {
return 0;
}
return ReferenceChainTracker::instance()->tagAsRootForTest(jvmti, env,
target);
}

extern "C" DLLEXPORT jboolean JNICALL
Java_com_datadoghq_profiler_JavaProfiler_runReferenceChainPass0(
JNIEnv *env, jclass unused) {
jvmtiEnv *jvmti = VM::jvmti();
if (jvmti == nullptr) {
return JNI_FALSE;
}
return ReferenceChainTracker::instance()->runPassSerialized(jvmti, env);
}

extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_pollReferenceChainTargets0(
JNIEnv *env, jclass unused) {
jvmtiEnv *jvmti = VM::jvmti();
if (jvmti == nullptr) {
return;
}
ReferenceChainTracker::instance()->pollWatchedTargetsSerialized(jvmti, env);
}

extern "C" DLLEXPORT jint JNICALL
Java_com_datadoghq_profiler_JavaProfiler_drainReferenceChainEventCount0(
JNIEnv *env, jclass unused) {
std::vector<ReferenceChainEvent> events;
ReferenceChainTracker::instance()->drainPendingChainEvents(&events);
return (jint)events.size();
}

extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_resetReferenceChainSearchForTest0(
JNIEnv *env, jclass unused) {
jvmtiEnv *jvmti = VM::jvmti();
ReferenceChainTracker::instance()->resetSearchStateForTest(jvmti, env);
}

// Diagnostic-only: reads target's existing JVMTI tag (does NOT tag it -
// unlike tagAsReferenceChainRoot0 above, a target the real search has not
// reached yet must be left untagged) and reports its FIFO distance from the
// front of ReferenceChainTracker's pending-expansion queue. See
// ReferenceChainTracker::pendingExpandPositionForTest()'s own comment for
// the return-value contract.
extern "C" DLLEXPORT jlong JNICALL
Java_com_datadoghq_profiler_JavaProfiler_getReferenceChainPendingPositionForTest0(
JNIEnv *env, jclass unused, jobject target) {
jvmtiEnv *jvmti = VM::jvmti();
if (jvmti == nullptr || target == nullptr) {
return -2;
}
jlong tag = 0;
jvmtiError err = jvmti->GetTag(target, &tag);
if (err != JVMTI_ERROR_NONE) {
return -2;
}
return (jlong)ReferenceChainTracker::instance()->pendingExpandPositionForTest(
tag);
}

extern "C" DLLEXPORT jlong JNICALL
Java_com_datadoghq_profiler_JavaProfiler_getReferenceChainPendingSizeForTest0(
JNIEnv *env, jclass unused) {
return (jlong)ReferenceChainTracker::instance()->pendingExpandSizeForTest();
}

// Seeds one heap-floor-ring sample directly (LivenessTracker::secondsToOOM()'s
// input), bypassing the real GarbageCollectionFinish callback - lets a test
// build an arbitrary rising/flat heap-usage-over-time history without
// waiting on real GCs. timestampNs values are only ever compared against
// each other (secondsToOOM()'s own ringWindowStats() deltas), never against
// a real wall clock, so a test may use any self-consistent, strictly
// increasing sequence.
extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_heapFloorRecordForTest0(
JNIEnv *env, jclass unused, jlong usedBytes, jlong timestampNs) {
LivenessTracker::instance()->heapFloorRecordForTest((u64)usedBytes,
(u64)timestampNs);
}

// Bypasses initialize_table()'s JNI-dependent HeapUsage::getMaxHeap() call so
// secondsToOOM() can be exercised against a test-chosen fake max heap size,
// independent of whatever -Xmx this JVM's own shared, no-forkEvery fork
// happens to run with.
extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_setMaxHeapBytesForTest0(
JNIEnv *env, jclass unused, jlong maxHeapBytes) {
LivenessTracker::instance()->setMaxHeapBytesForTest((jlong)maxHeapBytes);
}

// Temporarily disables onGC()'s own recordHeapFloorSample() call so a test
// can seed the heap-floor ring exclusively via heapFloorRecordForTest0()
// without a real GC interleaving a sample with a real OS::nanotime()
// timestamp and real heap usage, corrupting secondsToOOM()'s projection.
extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_setHeapFloorRecordingForTest0(
JNIEnv *env, jclass unused, jboolean enabled) {
LivenessTracker::instance()->setHeapFloorRecordingForTest(enabled == JNI_TRUE);
}

// Exposes ReferenceChainTracker::shouldRunPass() directly (see that seam's
// own comment, referenceChains.h) - unlike runReferenceChainPass0() above,
// which calls runPass() unconditionally, this reports whether the
// search-restart gate itself (canAffordNewSearch() -> hasLeakSignal()) would
// currently allow a fresh/terminal search to start.
extern "C" DLLEXPORT jboolean JNICALL
Java_com_datadoghq_profiler_JavaProfiler_shouldRunPassForTest0(JNIEnv *env,
jclass unused) {
return ReferenceChainTracker::instance()->shouldRunPassForTest(
OS::nanotime())
? JNI_TRUE
: JNI_FALSE;
}

// Exposes ReferenceChainTracker::passesRun() directly - not itself DEBUG-gated on the native side
// (used by production JFR event fields too), but exposed here only for test use: lets a test note
// the current pass count before creating an object, then wait for that count to advance before
// trusting any match against it - the only way to be certain a match came from a pass whose own
// expandFrontier() (and therefore collectStaleExpandedEntriesForRotation()) ran strictly after the
// object existed, rather than from the same pass racing the object's creation.
extern "C" DLLEXPORT jint JNICALL
Java_com_datadoghq_profiler_JavaProfiler_referenceChainPassesRunForTest0(
JNIEnv *env, jclass unused) {
return (jint)ReferenceChainTracker::instance()->passesRun();
}

#endif // DEBUG

// ---- Test-only reads of the current thread's OTEP record -----------------------------------
// Each reads the current carrier's record directly via ProfiledThread::current(), with no
// detach/attach (diagnostic-only, not on any signal-handler or hot write path).
Expand Down
Loading
Loading