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
31 changes: 23 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
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
123 changes: 112 additions & 11 deletions ddprof-lib/src/main/cpp/referenceChainEvents.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,13 @@ void ReferenceChainTracker::pollWatchedTargets(jvmtiEnv *jvmti, JNIEnv *jni) {
return;
}

// Don't rebuild chains against cache entries the heap callback asked to drop.
drainPendingChainInvalidations();

// Stamp every entry this poll refreshes with the current search generation.
const u64 current_search_ns = load(_search_start_ns);

// Don't rebuild chains against cache entries the heap callback asked to drop, and refresh the
// ones whose path changed (the only site that applies REBUILD invalidations).
applyPendingChainInvalidations(jvmti, jni, current_search_ns);

// klass_ids resolved (and therefore already pruned-if-dead) by the candidate loop below, so the
// prune pass afterwards skips re-resolving them - it only needs to cover cached klasses that are
// no longer flagged.
Expand Down Expand Up @@ -572,7 +573,7 @@ int ReferenceChainTracker::snapshotResolvedChainTags(jlong *out) {

void ReferenceChainTracker::deferResolvedChainInvalidation(
jlong source_tag, const jlong *resolved_chain_tags,
int resolved_chain_tag_count) {
int resolved_chain_tag_count, ChainInvalidationKind kind) {
bool was_cached = false;
for (int i = 0; i < resolved_chain_tag_count; i++) {
if (resolved_chain_tags[i] == source_tag) {
Expand All @@ -583,32 +584,61 @@ void ReferenceChainTracker::deferResolvedChainInvalidation(
if (!was_cached) {
return;
}
// Cache keys are frontier tags (>= 0); a negative key would alias another tag's REBUILD
// encoding, so it is never queued.
assert(source_tag >= 0);
if (source_tag < 0) {
return;
}
const jlong evict = encodePendingChainInvalidation(source_tag, ChainInvalidationKind::EVICT);
const jlong rebuild = encodePendingChainInvalidation(source_tag, ChainInvalidationKind::REBUILD);

_pending_chain_invalidations_lock.lock();
for (jlong tag : _pending_chain_invalidations) {
if (tag == source_tag) {
for (jlong &pending : _pending_chain_invalidations) {
if (pending == evict) {
_pending_chain_invalidations_lock.unlock();
return; // EVICT already queued - it wins over any REBUILD
}
if (pending == rebuild) {
if (kind == ChainInvalidationKind::EVICT) {
pending = evict; // the source was rejected after its path changed: erase, don't rebuild
}
_pending_chain_invalidations_lock.unlock();
return;
}
}
// Cache insertion is serialized with traversals on the engine thread and dumps can only remove
// keys, so one pre-traversal snapshot bounds distinct eligible tags to the cache cap.
// keys, so one pre-traversal snapshot bounds distinct eligible tags to the cache cap. REBUILD
// entries the dump path leaves pending do not break the bound: only pollWatchedTargets() inserts
// into the cache, and it drains every pending entry first.
assert((int)_pending_chain_invalidations.size() <
MAX_PENDING_CHAIN_INVALIDATIONS);
if ((int)_pending_chain_invalidations.size() <
MAX_PENDING_CHAIN_INVALIDATIONS) {
_pending_chain_invalidations.push_back(source_tag);
_pending_chain_invalidations.push_back(
kind == ChainInvalidationKind::REBUILD ? rebuild : evict);
}
_pending_chain_invalidations_lock.unlock();
}

void ReferenceChainTracker::drainPendingChainInvalidations() {
// Serialize drainers and scratch-buffer use with the cache. The producer lock is held only for
// the O(1) handoff, never for unordered_map erase or node destruction.
// a bounded (<= MAX_PENDING_CHAIN_INVALIDATIONS) in-place partition - EVICT entries move to the
// reserved scratch buffer, REBUILD entries stay pending for the BFS thread - never for
// unordered_map erase or node destruction. Neither step reallocates.
ExclusiveLockGuard guard(&_resolved_chains_lock);
_pending_chain_invalidations_lock.lock();
assert(_draining_chain_invalidations.empty());
_pending_chain_invalidations.swap(_draining_chain_invalidations);
size_t kept = 0;
for (size_t i = 0; i < _pending_chain_invalidations.size(); i++) {
jlong pending = _pending_chain_invalidations[i];
if (isPendingChainRebuild(pending)) {
_pending_chain_invalidations[kept++] = pending;
} else {
_draining_chain_invalidations.push_back(pending);
}
}
_pending_chain_invalidations.resize(kept);
_pending_chain_invalidations_lock.unlock();

for (jlong tag : _draining_chain_invalidations) {
Expand All @@ -620,6 +650,76 @@ void ReferenceChainTracker::drainPendingChainInvalidations() {
_draining_chain_invalidations.clear();
}

void ReferenceChainTracker::applyPendingChainInvalidations(
jvmtiEnv *jvmti, JNIEnv *jni, u64 current_search_ns) {
// REBUILD tags whose cached chain is current-generation. A chain cached by an earlier search
// under the same numeric tag describes a different object (restartSearch() resets the tag
// namespace), so a path change on the new object says nothing about it: leave it alone.
jlong rebuild_tags[MAX_PENDING_CHAIN_INVALIDATIONS];
int rebuild_count = 0;
{
ExclusiveLockGuard guard(&_resolved_chains_lock);
_pending_chain_invalidations_lock.lock();
assert(_draining_chain_invalidations.empty());
_pending_chain_invalidations.swap(_draining_chain_invalidations);
_pending_chain_invalidations_lock.unlock();

for (jlong pending : _draining_chain_invalidations) {
jlong tag = pendingChainInvalidationTag(pending);
if (!isPendingChainRebuild(pending)) {
if (_resolved_chains.erase(tag) > 0) {
TEST_LOG("ReferenceChainTracker::drainPendingChainInvalidations source_tag=%lld",
(long long)tag);
}
continue;
}
auto it = _resolved_chains.find(tag);
if (it != _resolved_chains.end() &&
it->second.source_search_ns == current_search_ns &&
rebuild_count < MAX_PENDING_CHAIN_INVALIDATIONS) {
rebuild_tags[rebuild_count++] = tag;
}
}
_draining_chain_invalidations.clear();
}

for (int i = 0; i < rebuild_count; i++) {
const jlong tag = rebuild_tags[i];
ReferenceChainEvent event;
// The frontier entry stays intact until restartSearch(), so this does not depend on the
// candidate/discovered slots runPass()'s terminal branch may already have zeroed.
if (!buildChainEvent(jvmti, jni, tag, &event)) {
TEST_LOG("ReferenceChainTracker::applyPendingChainInvalidations rebuild failed, "
"keeping cached chain source_tag=%lld",
(long long)tag);
continue;
}
if (suppressChainEvent(event)) {
// The new path turned the chain into a filtered shape: same outcome as every other build
// site (see buildDiscoveredInstanceChains()).
invalidateResolvedChain(tag);
continue;
}
if (isLeakTag((jlong)event._target_tag) && !chainTargetsCurrentLeakOwner(event)) {
// Dead-owner rebuild: keep the old chain; the next dump evicts it if it is dead too.
continue;
}
event._start_time = TSC::ticks();
ExclusiveLockGuard guard(&_resolved_chains_lock);
auto it = _resolved_chains.find(tag);
// Replace only: a dump may have evicted the entry (dead owner) since the scan above, and a
// rebuild must never resurrect it.
if (it != _resolved_chains.end() &&
it->second.source_search_ns == current_search_ns) {
it->second.event = std::move(event);
it->second.source_tag = tag;
TEST_LOG("ReferenceChainTracker::applyPendingChainInvalidations rebuilt "
"source_tag=%lld depth=%u",
(long long)tag, it->second.event._depth);
}
}
}

void ReferenceChainTracker::clearPendingChainInvalidations() {
ExclusiveLockGuard cache_guard(&_resolved_chains_lock);
_pending_chain_invalidations_lock.lock();
Expand Down Expand Up @@ -759,7 +859,8 @@ void ReferenceChainTracker::recordDiscoveredInstance(
_candidate_discovered_tags[s][d] = frontier_tag;
if (invalidation_mode == CacheInvalidationMode::DEFERRED) {
deferResolvedChainInvalidation(victim, resolved_chain_tags,
resolved_chain_tag_count);
resolved_chain_tag_count,
ChainInvalidationKind::EVICT);
} else {
invalidateResolvedChain(victim);
}
Expand Down
27 changes: 25 additions & 2 deletions ddprof-lib/src/main/cpp/referenceChainTraversal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,25 @@ jvmtiIterationControl JNICALL ReferenceChainTracker::heapRootCallback(
ctx->frontier, *tag_ptr, translated_root_kind)) {
ctx->tracker->deferResolvedChainInvalidation(
*tag_ptr, ctx->resolved_chain_tags,
ctx->resolved_chain_tag_count);
ctx->resolved_chain_tag_count,
ReferenceChainTracker::ChainInvalidationKind::REBUILD);
}
break;
default:
break;
}
// 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 +376,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 +1062,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
23 changes: 13 additions & 10 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 @@ -457,15 +457,16 @@ jint JNICALL ReferenceChainTracker::heapReferenceCallback(
if (ctx->frontier->improveChain(*tag_ptr, parent_tag, referrer_klass,
depth, 0, edge_field_index,
(u8)reference_kind)) {
// Chain was improved — invalidate any cached chain for this tag so pollWatchedTargets
// rebuilds it with the deeper path.
// Chain was improved — queue a REBUILD so pollWatchedTargets replaces any cached chain for
// this tag with the deeper path (never erased: the source is still live).
// Deferred: we're inside the FollowReferences pause (see deferResolvedChainInvalidation()).
ctx->tracker->deferResolvedChainInvalidation(
*tag_ptr, ctx->resolved_chain_tags,
ctx->resolved_chain_tag_count);
ctx->resolved_chain_tag_count,
ReferenceChainTracker::ChainInvalidationKind::REBUILD);
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 @@ -477,7 +478,8 @@ jint JNICALL ReferenceChainTracker::heapReferenceCallback(
// durable root.
ctx->tracker->deferResolvedChainInvalidation(
*tag_ptr, ctx->resolved_chain_tags,
ctx->resolved_chain_tag_count);
ctx->resolved_chain_tag_count,
ReferenceChainTracker::ChainInvalidationKind::REBUILD);
}
} else {
// Already-admitted entry reached via a NEW root-like edge (parent_tag == 0): the static-field
Expand All @@ -489,7 +491,8 @@ jint JNICALL ReferenceChainTracker::heapReferenceCallback(
(u8)reference_kind)) {
ctx->tracker->deferResolvedChainInvalidation(
*tag_ptr, ctx->resolved_chain_tags,
ctx->resolved_chain_tag_count);
ctx->resolved_chain_tag_count,
ReferenceChainTracker::ChainInvalidationKind::REBUILD);
} 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
Expand Down
Loading
Loading