Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -183,6 +183,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