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
6 changes: 2 additions & 4 deletions ddprof-lib/src/main/cpp/javaApi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1125,12 +1125,10 @@ 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,
LivenessTracker::instance()->seedKlassPopulationSampleForTest(
(u32)klassId, (u32)count, (u64)epoch, evicted,
&evicted_count, KlassPopulationEntry::MAX_REPRESENTATIVES_PER_KLASS);
for (int i = 0; i < evicted_count; i++) {
if (evicted[i] != nullptr) {
Expand Down
44 changes: 36 additions & 8 deletions ddprof-lib/src/main/cpp/livenessTracker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -882,15 +882,30 @@ void LivenessTracker::insertThreadGen(KlassCountScratch &scratch,
return;
}
}
int idx;
if (scratch.thread_count < KlassCountScratch::MAX_THREADS_PER_KLASS) {
int idx = scratch.thread_count++;
scratch.threads[idx].tid = tid;
scratch.threads[idx].ages[0] = age;
scratch.threads[idx].age_count = 1;
scratch.threads[idx].count = 1; // this object is the tid's first this epoch
}
// else: thread table full — additional threads are not tracked, but
// the oldest[] array still captures instances from all threads.
idx = scratch.thread_count++;
} else {
// Keeping only the first tids hides later, larger one-shot cohorts behind
// early small allocations. Replace the smallest observed retained set;
// ties prefer keeping the tid with more distinct surviving ages.
idx = 0;
for (int i = 1; i < scratch.thread_count; i++) {
const auto &candidate = scratch.threads[i];
const auto &victim = scratch.threads[idx];
if (candidate.count < victim.count ||
(candidate.count == victim.count && candidate.age_count < victim.age_count)) {
idx = i;
}
}
}
// Count only objects observed since this tid entered the scratch. Inherit
// neither the evicted tid's count nor its ages: both feed qualification.
// Reappearing tids can be undercounted at capacity, never overcounted.
scratch.threads[idx].tid = tid;
scratch.threads[idx].ages[0] = age;
scratch.threads[idx].age_count = 1;
scratch.threads[idx].count = 1;
}

void LivenessTracker::accumulateKlassCount(u32 klass_id, jlong age,
Expand Down Expand Up @@ -999,6 +1014,9 @@ jweak LivenessTracker::recordKlassPopulationSampleLocked(
_klass_population[slot].ring_head = 0;
_klass_population[slot].ring_fill = 0;
_klass_population[slot].consecutive_positive = 0;
#ifdef DEBUG
_klass_population[slot].test_seeded_history = false;
#endif
_klass_population[slot].cached_slope = 0.0;
// A reused (evicted) slot's previous class's per-tid trends must not
// leak onto the new one, same as the fields above.
Expand All @@ -1011,6 +1029,16 @@ jweak LivenessTracker::recordKlassPopulationSampleLocked(
}

KlassPopulationEntry &entry = _klass_population[slot];
#ifdef DEBUG
if (entry.test_seeded_history) {
// Real GC folds must not replace a JNI test fixture's seeded ramp with
// the sampled cohort's generation count. Keep its LRU epoch current.
entry.last_updated_epoch = epoch;
*out_slot = slot;
*out_created = created;
return evicted_ref;
}
#endif
entry.count_ring[entry.ring_head] = count;
entry.ring_head = (u8)((entry.ring_head + 1) % KLASS_POPULATION_RING_SIZE);
if (entry.ring_fill < KLASS_POPULATION_RING_SIZE) {
Expand Down
32 changes: 32 additions & 0 deletions ddprof-lib/src/main/cpp/livenessTracker.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ typedef struct KlassPopulationEntry {
// without this counter it gets reported as a leak candidate almost as
// often as a real leak does.
u8 consecutive_positive;
#ifdef DEBUG
// Explicit JNI test seeds own their class trend. Real GC folds preserve it
// until the test resets the population table or the entry is evicted.
bool test_seeded_history;
#endif
// Slope (regression value at the ring's newest sample minus the value at
// its oldest sample - see ringWindowStats, livenessTracker.cpp) as of the
// last push, computed and cached by hasQualifyingGrowth() alongside
Expand Down Expand Up @@ -1260,6 +1265,7 @@ class alignas(alignof(SpinLock)) LivenessTracker {
_klass_population_size--;
}

// Ordinary algorithm-test histories remain subject to GC updates and decay.
jweak klassPopulationRecordForTest(u32 klass_id, u32 count, u64 epoch,
int *out_slot, bool *out_created,
jweak *out_evicted = nullptr,
Expand All @@ -1276,6 +1282,32 @@ class alignas(alignof(SpinLock)) LivenessTracker {
return evicted;
}

#ifdef DEBUG
/**
* Seeds the JNI traversal fixture's class history and protects it from real
* GC updates. Clear protection while recording another explicit seed so it
* updates the same ring and qualification counters as ordinary samples.
*/
void seedKlassPopulationSampleForTest(
u32 klass_id, u32 count, u64 epoch, jweak *out_evicted = nullptr,
int *out_evicted_count = nullptr, int max_evicted = 0) {
_table_lock.lock();
klass_id = resolveTestKlassAliasLocked(klass_id);
for (int i = 0; i < _klass_population_size; i++) {
if (_klass_population[i].klass_id == klass_id) {
_klass_population[i].test_seeded_history = false;
break;
}
}
int slot;
bool created;
recordKlassPopulationSampleLocked(klass_id, count, epoch, &slot, &created,
out_evicted, out_evicted_count, max_evicted);
_klass_population[slot].test_seeded_history = true;
_table_lock.unlock();
}
#endif

// Seeds one per-tid trend sample for tests/scenarios: pushes `count` as
// tid's epoch sample on klass_id's KlassPopulationEntry, marking the
// trend SYNTHETIC (exempt from the real fold's absent-tid decay and slot
Expand Down
31 changes: 24 additions & 7 deletions ddprof-lib/src/main/cpp/referenceChainAnchors.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,11 @@ ReferenceChainTracker::collectStaticFieldAnchorsForRotation(int max_count) {
// fair pick vector here and is skipped at consumption time via fresh_kept_tags).
for (size_t i = 0; i < idx_size; i++) {
jlong tag = _static_anchor_index[i];
jlong own_class_tag = i < _static_anchor_own_class_tags.size()
? _static_anchor_own_class_tags[i] : 0;
FrontierEntry entry{};
if (!frontier->lookupLocked(tag, &entry) ||
entry.parent_tag != 0 ||
(entry.root_kind != (u8)JVMTI_HEAP_REFERENCE_STATIC_FIELD &&
entry.root_kind != (u8)JVMTI_HEAP_REFERENCE_JNI_GLOBAL) ||
!isSelectableAnchor(entry, own_class_tag) ||
(entry.state != FrontierEntryState::FRONTIER &&
entry.state != FrontierEntryState::EXPANDED) ||
isQueuedForRotation(tag)) {
Expand Down Expand Up @@ -201,15 +201,15 @@ ReferenceChainTracker::collectStaticFieldAnchorsForRotation(int max_count) {
}
pos = (size_t)(it - _static_anchor_index.begin());
}
jlong own_class_tag = pos < _static_anchor_own_class_tags.size()
? _static_anchor_own_class_tags[pos] : 0;
FrontierEntry entry{};
if (!frontier->lookupLocked(tag, &entry) ||
entry.parent_tag != 0 ||
(entry.root_kind != (u8)JVMTI_HEAP_REFERENCE_STATIC_FIELD &&
entry.root_kind != (u8)JVMTI_HEAP_REFERENCE_JNI_GLOBAL) ||
!isSelectableAnchor(entry, own_class_tag) ||
(entry.state != FrontierEntryState::FRONTIER &&
entry.state != FrontierEntryState::EXPANDED) ||
isQueuedForRotation(tag) || entry.leak_tag != 0) {
continue; // dead/demoted/queued/leak-tier: first look spent, not
continue; // dead/ineligible/queued/leak-tier: first look spent, not
// fresh-kept (the leak tier selects it via the scan if it is leak-tagged)
}
bool keep = false; // container or not-yet-classified rides the
Expand Down Expand Up @@ -333,6 +333,23 @@ void ReferenceChainTracker::addToStaticAnchorIndex(jlong tag,
root_kind != (u8)JVMTI_HEAP_REFERENCE_JNI_GLOBAL) {
return;
}
indexAnchorForRotation(tag, own_class_tag);
}

bool ReferenceChainTracker::isSelectableAnchor(const FrontierEntry &entry,
jlong own_class_tag) const {
if (entry.parent_tag == 0 &&
(entry.root_kind == (u8)JVMTI_HEAP_REFERENCE_STATIC_FIELD ||
entry.root_kind == (u8)JVMTI_HEAP_REFERENCE_JNI_GLOBAL)) {
return true;
}
auto shape = _class_shape_cache.find(own_class_tag);
return shape != _class_shape_cache.end() &&
shape->second == (u8)AnchorClassShape::CONTAINER;
}

void ReferenceChainTracker::indexAnchorForRotation(jlong tag,
jlong own_class_tag) {
// Dedup: a push at first admission and another at upgrade would double-add.
if (!_static_anchor_index_tags.insert(tag).second) {
return;
Expand Down
30 changes: 29 additions & 1 deletion ddprof-lib/src/main/cpp/referenceChainTraversal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,24 @@ jvmtiIterationControl JNICALL ReferenceChainTracker::heapRootCallback(
default:
break;
}
// Record durable roots even when the holder already had a frontier entry:
// neither first admission nor a retained chain path may skip the index.
if (*tag_ptr > 0 && !isLeakTag(*tag_ptr)) {
ctx->tracker->addToStaticAnchorIndex(*tag_ptr, class_tag,
translated_root_kind);
}
// Remember local holders before expansion can reparent them into a deeper
// chain. Known collections still need bounded descend walks after that change
// to observe replacement backing storage. Classification runs outside this
// callback; unknown types are indexed but do not qualify for a walk yet.
if (isTransientRootKind(translated_root_kind) && class_tag != 0 &&
*tag_ptr > 0 && !isLeakTag(*tag_ptr)) {
auto shape = ctx->tracker->_class_shape_cache.find(class_tag);
if (shape == ctx->tracker->_class_shape_cache.end() ||
shape->second == (u8)AnchorClassShape::CONTAINER) {
ctx->tracker->indexAnchorForRotation(*tag_ptr, class_tag);
}
}
return JVMTI_ITERATION_CONTINUE;
}

Expand Down Expand Up @@ -363,7 +381,12 @@ void ReferenceChainTracker::runPassManualWalk(jvmtiEnv *jvmti, JNIEnv *jni,
std::vector<AtRiskAnchor> static_anchor_fifo_drained;
drainStaticAnchorFifo(STATIC_ANCHOR_FIFO_DRAIN, static_anchor_fifo_drained);
for (const AtRiskAnchor &at_risk : static_anchor_fifo_drained) {
static_anchor_tags.push_back(at_risk.tag);
// A reparented collection can also have been selected by the persistent
// container tier. The combined selection is bounded by 48 + 16 entries.
if (std::find(static_anchor_tags.begin(), static_anchor_tags.end(),
at_risk.tag) == static_anchor_tags.end()) {
static_anchor_tags.push_back(at_risk.tag);
}
}
if (rotation_tags.empty() && leak_accumulation_tags.empty() &&
stale_expanded_tags.empty() && static_anchor_tags.empty()) {
Expand Down Expand Up @@ -1044,6 +1067,11 @@ bool ReferenceChainTracker::runPass(jvmtiEnv *jvmti, JNIEnv *jni,
if (manual_first_pass) {
_search_started = true;
store(_search_start_ns, OS::nanotime());
// Register the candidates that triggered this search before walking. The
// first pass can drain the frontier; without a watched klass it would then
// complete and release its tags before the loop's post-pass poll can
// reconstruct any chain or schedule candidate-scoped walks.
pollWatchedTargets(jvmti, jni);
}

// Root/stack-ref enumeration alone never discovers a root's transitive children
Expand Down
19 changes: 10 additions & 9 deletions ddprof-lib/src/main/cpp/referenceChainWalk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -446,9 +446,9 @@ jint JNICALL ReferenceChainTracker::heapReferenceCallback(
// This new path is deeper - replace the shallow root-attached entry with the deeper
// chain-attached entry.
u32 referrer_klass = ctx->tracker->classTags()->resolve(class_tag);
// Pre-read the CURRENT shape: if improveChain() below succeeds, this root-attached durable
// entry is about to be replaced with a deeper chain-attached one - i.e. it is leaving the
// population collectStaticFieldAnchorsForRotation() can select, at exactly this moment.
// Pre-read the CURRENT shape: improveChain() can remove this entry's
// durable-root eligibility. A known collection remains in the container
// tier; other holders need the FIFO's first look below.
FrontierEntry pre_improve_entry{};
bool was_root_attached_durable =
ctx->frontier->lookup(*tag_ptr, &pre_improve_entry) &&
Expand All @@ -464,8 +464,8 @@ jint JNICALL ReferenceChainTracker::heapReferenceCallback(
*tag_ptr, ctx->resolved_chain_tags,
ctx->resolved_chain_tag_count);
if (was_root_attached_durable) {
// Demotion push: the replaced entry's static/JNI-global attribution was its only
// anchor-tier eligibility, and it is gone now.
// Ensure a first look even when class-shape reconciliation has not
// yet classified this reparented holder as a collection.
ctx->tracker->pushAtRiskStaticAnchor(
*tag_ptr, ctx->tracker->classTags()->resolve(class_tag));
}
Expand All @@ -491,13 +491,14 @@ jint JNICALL ReferenceChainTracker::heapReferenceCallback(
*tag_ptr, ctx->resolved_chain_tags,
ctx->resolved_chain_tag_count);
} else if (reference_kind == JVMTI_HEAP_REFERENCE_STATIC_FIELD) {
// The upgrade refused (maybeUpgradeRootAttachedRootKind returns false for parent_tag != 0
// by design), so this STATIC_FIELD edge just proved an at-risk static attachment the anchor
// tier's parent_tag == 0 filter can never see: a holder already admitted as a non-root
// child (find-anchor-holder-eviction).
// The sweep reached a holder first admitted through another object.
// Keep its static attachment indexed: confirmed collections must remain
// eligible after the FIFO's one-time descend walk has been consumed.
FrontierEntry entry{};
if (ctx->frontier->lookup(*tag_ptr, &entry) &&
entry.parent_tag != 0) {
ctx->tracker->addToStaticAnchorIndex(
*tag_ptr, class_tag, (u8)reference_kind);
ctx->tracker->pushAtRiskStaticAnchor(
*tag_ptr, ctx->tracker->classTags()->resolve(class_tag));
}
Expand Down
24 changes: 13 additions & 11 deletions ddprof-lib/src/main/cpp/referenceChains.h
Original file line number Diff line number Diff line change
Expand Up @@ -434,10 +434,8 @@ class ReferenceChainTracker {
}
} _priority_expand_set;

// A static holder richly referenced from the running graph is EXCLUDED from the static-anchor tier forever once its
// frontier entry is chain-attached (first admitted via a non-root path, or demoted by
// improveChain) - maybeUpgradeRootAttachedRootKind() refuses entries with parent_tag != 0 by
// design, so collectStaticFieldAnchorsForRotation() (parent_tag == 0 filter) can never select it.
// Chain-attached holders without a known collection shape need a first
// descend walk when a static edge or reparenting removes their root eligibility.
struct AtRiskAnchor {
jlong tag;
u32 klass_id;
Expand All @@ -457,13 +455,14 @@ class ReferenceChainTracker {
std::unordered_map<u32, u32> _static_anchor_fifo_klass_counts;
static constexpr u32 STATIC_ANCHOR_ATRISK_PER_KLASS_CAP = 64;

// Index of root-attached STATIC_FIELD/JNI_GLOBAL frontier entries, so
// collectStaticFieldAnchorsForRotation() iterates O(anchors) instead of scanning the full
// frontier table O(frontier_size).
// Index of holders observed at durable root edges or local roots. Known collections remain
// eligible after reparenting, so replacing their storage is still observed.
// Non-collection local holders are excluded after classification.
// collectStaticFieldAnchorsForRotation() iterates these anchors rather than
// scanning the full frontier table.
std::vector<jlong> _static_anchor_index;

// Parallel to _static_anchor_index: the OWN class tag of each anchor object (the class of the
// static field's VALUE, not the holder class).
// Parallel to _static_anchor_index: the runtime class tag of each indexed holder.
std::vector<jlong> _static_anchor_own_class_tags;

// Dedup companion for _static_anchor_index (O(1) membership; the population is ~28k on a real JVM
Expand Down Expand Up @@ -933,14 +932,17 @@ class ReferenceChainTracker {
int *edges_admitted, bool *truncated,
bool *frontier_cap_hit, u64 *safepoint_ns);

// Selects root-attached durable entries with a tiered cursor: leak-tagged, fresh, container-shaped,
// then everything else fairly.
// Selects durable roots and indexed collections (including reparented ones)
// with a tiered cursor: leak-tagged, fresh, containers, then other roots fairly.
std::vector<jlong> collectStaticFieldAnchorsForRotation(int max_count);
bool isSelectableAnchor(const FrontierEntry &entry, jlong own_class_tag) const;
// At-risk anchor push: deduped, capped, and quota-limited per klass.
void pushAtRiskStaticAnchor(jlong tag, u32 klass_id);
// Add `tag` to _static_anchor_index if its root_kind is a durable anchor-tier kind (STATIC_FIELD
// or JNI_GLOBAL).
void addToStaticAnchorIndex(jlong tag, jlong own_class_tag, u8 root_kind);
// Shared deduplicated index insertion for durable roots and local holders.
void indexAnchorForRotation(jlong tag, jlong own_class_tag);

// True iff `klass` implements java/util/Collection or java/util/Map, directly or transitively
// (superclass chain + interfaces of every visited class, depth-bounded, visited set to survive
Expand Down
Loading
Loading