Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a916643
Add JFR event types for reference chains
jbachorik Sep 17, 2026
ba42eb3
Wire reference chains into the profiler lifecycle and JNI API
jbachorik Sep 17, 2026
6c9c67e
Address review: merge chain hops, trim comments, drop doc/jira refs
jbachorik Sep 18, 2026
c72dedd
Drop remaining Jira and cross-layer test references
jbachorik Sep 18, 2026
bd0b608
livenessTracker: urgency counters, OOM guards, epoch accounting
jbachorik Sep 28, 2026
4f0ac15
misc: objectSampler start order, dump-failure test log, Java seams pa…
jbachorik Sep 28, 2026
bdd3a66
referenceChains: defer resolved-chain invalidation off the FollowRefe…
jbachorik Sep 30, 2026
15fee20
referenceChains tests: PES/ClassTagAllocator coverage, fix cacheChain…
jbachorik Sep 30, 2026
39cd841
objectSampler: roll back on LivenessTracker start failure
jbachorik Oct 6, 2026
3854f88
javaApi: release stubs for debug test seams, null-check jvmti
jbachorik Oct 6, 2026
c67a79b
referenceChains: batch deferred invalidations, cache probe start
jbachorik Oct 6, 2026
57da0a2
tests: cover urgency backstop and account_epoch, tidy PES tests
jbachorik Oct 6, 2026
7ea2afa
Trim comments and test-seam Javadocs
jbachorik Oct 6, 2026
96b2691
sphinx: address review feedback on PR #798
jbachorik Oct 6, 2026
b1026ad
sphinx: address review feedback on PR #798
jbachorik Oct 6, 2026
1ced450
Trim review-fix comments
jbachorik Oct 7, 2026
aec7774
sphinx: address review feedback on PR #798
jbachorik Oct 7, 2026
3f071a2
Fix urgency check comment
jbachorik Oct 7, 2026
4ce119b
Address deep review findings on reference chains
jbachorik Oct 7, 2026
6c84483
sphinx: address review feedback on PR #798
jbachorik Oct 9, 2026
61177f0
Defer chain invalidation on the auto-mark path too
jbachorik Oct 9, 2026
5a1f703
Skip the terminal-outcome guard on TSan pass-budget exhaustion
jbachorik Oct 9, 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
4 changes: 4 additions & 0 deletions ddprof-lib/src/main/cpp/counters.h
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@
* 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 urgency boost: admissions below the table cap vs. \
* admissions that hit the cap and fell back to the subsample ratio. */ \
X(LIVENESS_URGENT_BOOST_ADMITS, "liveness_urgent_boost_admits") \
X(LIVENESS_URGENT_BOOST_BACKED_OFF, "liveness_urgent_boost_backed_off") \
/* 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
311 changes: 311 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,317 @@ 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. They mutate real tracker
// state, so they are only implemented in DEBUG builds; release builds get the
// no-op stubs below.
#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) {
ProfiledThread::initCurrentThreadSignalSafe();
LivenessTracker::instance()->setGcGenerationsForTest(enabled);
Comment thread
jbachorik marked this conversation as resolved.
return JNI_TRUE;
}

extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_seedKlassPopulationSample0(
JNIEnv *env, jclass unused, jint klassId, jint count, jlong epoch) {
ProfiledThread::initCurrentThreadSignalSafe();
int slot;
bool created;
jweak evicted[KlassPopulationEntry::MAX_REPRESENTATIVES_PER_KLASS];
int evicted_count = 0;
LivenessTracker::instance()->klassPopulationRecordForTest(
(u32)klassId, (u32)count, (u64)epoch, &slot, &created, evicted,
&evicted_count, KlassPopulationEntry::MAX_REPRESENTATIVES_PER_KLASS);
for (int i = 0; i < evicted_count; i++) {
if (evicted[i] != nullptr) {
env->DeleteWeakGlobalRef(evicted[i]);
}
}
}

// See tidTrendRecordForTest() (livenessTracker.h) for the tid requirements.
extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_seedTidTrendSample0(
JNIEnv *env, jclass unused, jint klassId, jint tid, jint count,
jlong epoch) {
ProfiledThread::initCurrentThreadSignalSafe();
jweak evicted[KlassPopulationEntry::MAX_REPRESENTATIVES_PER_KLASS];
int evicted_count = 0;
LivenessTracker::instance()->tidTrendRecordForTest(
(u32)klassId, (jint)tid, (u32)count, (u64)epoch, evicted,
&evicted_count, KlassPopulationEntry::MAX_REPRESENTATIVES_PER_KLASS);
for (int i = 0; i < evicted_count; i++) {
if (evicted[i] != nullptr) {
env->DeleteWeakGlobalRef(evicted[i]);
}
}
}

// Makes `representative` klassId's leak-candidate representative, so a seeded
// population sample and a tagged root can drive pollWatchedTargets() without
// the real sampler or walk. The tracker owns the weak global ref created here.
extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_setKlassPopulationRepresentativeForTest0(
JNIEnv *env, jclass unused, jint klassId, jobject representative) {
ProfiledThread::initCurrentThreadSignalSafe();
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) {
ProfiledThread::initCurrentThreadSignalSafe();
LivenessTracker::instance()->klassPopulationResetForTest();
}

extern "C" DLLEXPORT jintArray JNICALL
Java_com_datadoghq_profiler_JavaProfiler_selectLeakCandidateKlassIds0(
JNIEnv *env, jclass unused) {
ProfiledThread::initCurrentThreadSignalSafe();
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) {
ProfiledThread::initCurrentThreadSignalSafe();
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) {
ProfiledThread::initCurrentThreadSignalSafe();
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) {
ProfiledThread::initCurrentThreadSignalSafe();
jvmtiEnv *jvmti = VM::jvmti();
if (jvmti == nullptr) {
return;
}
ReferenceChainTracker::instance()->finishLoopIterationSerialized(jvmti, env);
}

extern "C" DLLEXPORT jint JNICALL
Java_com_datadoghq_profiler_JavaProfiler_drainReferenceChainEventCount0(
JNIEnv *env, jclass unused) {
ProfiledThread::initCurrentThreadSignalSafe();
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) {
ProfiledThread::initCurrentThreadSignalSafe();
jvmtiEnv *jvmti = VM::jvmti();
if (jvmti == nullptr) {
return;
}
ReferenceChainTracker::instance()->resetSearchStateForTest(jvmti, env);
}

// Reads target's existing tag without tagging it. Return values are described
// at pendingExpandPositionForTest().
extern "C" DLLEXPORT jlong JNICALL
Java_com_datadoghq_profiler_JavaProfiler_getReferenceChainPendingPositionForTest0(
JNIEnv *env, jclass unused, jobject target) {
ProfiledThread::initCurrentThreadSignalSafe();
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(
Comment thread
jbachorik marked this conversation as resolved.
tag);
}

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

// Seeds one heap-floor sample for secondsToOOM() without waiting for a GC.
// Timestamps are only compared with each other, so any increasing sequence works.
extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_heapFloorRecordForTest0(
JNIEnv *env, jclass unused, jlong usedBytes, jlong timestampNs) {
ProfiledThread::initCurrentThreadSignalSafe();
LivenessTracker::instance()->heapFloorRecordForTest((u64)usedBytes,
(u64)timestampNs);
}

// Overrides the max heap size secondsToOOM() projects against.
extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_setMaxHeapBytesForTest0(
JNIEnv *env, jclass unused, jlong maxHeapBytes) {
ProfiledThread::initCurrentThreadSignalSafe();
LivenessTracker::instance()->setMaxHeapBytesForTest((jlong)maxHeapBytes);
}

// Stops real GCs from recording heap-floor samples while a test seeds them.
extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_setHeapFloorRecordingForTest0(
JNIEnv *env, jclass unused, jboolean enabled) {
ProfiledThread::initCurrentThreadSignalSafe();
LivenessTracker::instance()->setHeapFloorRecordingForTest(enabled == JNI_TRUE);
}

// Evaluates the real scheduling gate without executing a BFS pass. This is not read-only: under
// the engine lock it may account completed-search pain and restart/reset terminal search state.
extern "C" DLLEXPORT jboolean JNICALL
Java_com_datadoghq_profiler_JavaProfiler_shouldRunPassForTest0(JNIEnv *env,
jclass unused) {
ProfiledThread::initCurrentThreadSignalSafe();
return ReferenceChainTracker::instance()->shouldRunPassForTest(
OS::nanotime())
? JNI_TRUE
: JNI_FALSE;
}

// Lets a test wait for a pass that started after it created an object.
extern "C" DLLEXPORT jint JNICALL
Java_com_datadoghq_profiler_JavaProfiler_referenceChainPassesRunForTest0(
JNIEnv *env, jclass unused) {
ProfiledThread::initCurrentThreadSignalSafe();
return (jint)ReferenceChainTracker::instance()->passesRun();
}

#else // !DEBUG

// Release builds: no-op stubs so the Java natives still link.
extern "C" DLLEXPORT jboolean JNICALL
Java_com_datadoghq_profiler_JavaProfiler_setGcGenerationsEnabled0(
JNIEnv *env, jclass unused, jboolean enabled) {
return JNI_FALSE;
}

extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_seedKlassPopulationSample0(
JNIEnv *env, jclass unused, jint klassId, jint count, jlong epoch) {}

extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_seedTidTrendSample0(
JNIEnv *env, jclass unused, jint klassId, jint tid, jint count,
jlong epoch) {}

extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_setKlassPopulationRepresentativeForTest0(
JNIEnv *env, jclass unused, jint klassId, jobject representative) {}

extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_resetKlassPopulationForTest0(
JNIEnv *env, jclass unused) {}

extern "C" DLLEXPORT jintArray JNICALL
Java_com_datadoghq_profiler_JavaProfiler_selectLeakCandidateKlassIds0(
JNIEnv *env, jclass unused) {
return env->NewIntArray(0);
}

extern "C" DLLEXPORT jlong JNICALL
Java_com_datadoghq_profiler_JavaProfiler_tagAsReferenceChainRoot0(
JNIEnv *env, jclass unused, jobject target) {
return 0;
}

extern "C" DLLEXPORT jboolean JNICALL
Java_com_datadoghq_profiler_JavaProfiler_runReferenceChainPass0(
JNIEnv *env, jclass unused) {
return JNI_FALSE;
}

extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_pollReferenceChainTargets0(
JNIEnv *env, jclass unused) {}

extern "C" DLLEXPORT jint JNICALL
Java_com_datadoghq_profiler_JavaProfiler_drainReferenceChainEventCount0(
JNIEnv *env, jclass unused) {
return 0;
}

extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_resetReferenceChainSearchForTest0(
JNIEnv *env, jclass unused) {}

extern "C" DLLEXPORT jlong JNICALL
Java_com_datadoghq_profiler_JavaProfiler_getReferenceChainPendingPositionForTest0(
JNIEnv *env, jclass unused, jobject target) {
return -2;
}

extern "C" DLLEXPORT jlong JNICALL
Java_com_datadoghq_profiler_JavaProfiler_getReferenceChainPendingSizeForTest0(
JNIEnv *env, jclass unused) {
return 0;
}

extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_heapFloorRecordForTest0(
JNIEnv *env, jclass unused, jlong usedBytes, jlong timestampNs) {}

extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_setMaxHeapBytesForTest0(
JNIEnv *env, jclass unused, jlong maxHeapBytes) {}

extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_setHeapFloorRecordingForTest0(
JNIEnv *env, jclass unused, jboolean enabled) {}

extern "C" DLLEXPORT jboolean JNICALL
Java_com_datadoghq_profiler_JavaProfiler_shouldRunPassForTest0(JNIEnv *env,
jclass unused) {
return JNI_FALSE;
}

extern "C" DLLEXPORT jint JNICALL
Java_com_datadoghq_profiler_JavaProfiler_referenceChainPassesRunForTest0(
JNIEnv *env, jclass unused) {
return 0;
}

#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