Skip to content
Open
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ before working on a subsystem.
| TLS context (thread-local tracing context, distributed tracing) | [doc/architecture/TLSContext.md](doc/architecture/TLSContext.md) |
| Profiling engines (CPU, wall-clock, allocation, native memory, liveness) | [doc/architecture/ProfilingEngines.md](doc/architecture/ProfilingEngines.md) |
| JFR integration (FlightRecorder, metadata, constant pools, buffers) | [doc/architecture/JfrIntegration.md](doc/architecture/JfrIntegration.md) |
| Live-heap reference chains (implementation, signal scheduling, design) | [doc/architecture/LiveHeapReferenceChainsImplementation.md](doc/architecture/LiveHeapReferenceChainsImplementation.md), [doc/architecture/ReferenceChainsSignalsExplained.md](doc/architecture/ReferenceChainsSignalsExplained.md), [doc/ReferenceChainsDesign.md](doc/ReferenceChainsDesign.md), [doc/ReferenceChainsCollectionSummary.md](doc/ReferenceChainsCollectionSummary.md) |
| Live-heap reference chains (implementation, anchor rotation over durable roots and capped local collection holders, signal scheduling, design) | [doc/architecture/LiveHeapReferenceChainsImplementation.md](doc/architecture/LiveHeapReferenceChainsImplementation.md), [doc/architecture/ReferenceChainsSignalsExplained.md](doc/architecture/ReferenceChainsSignalsExplained.md), [doc/ReferenceChainsDesign.md](doc/ReferenceChainsDesign.md), [doc/ReferenceChainsCollectionSummary.md](doc/ReferenceChainsCollectionSummary.md) |
| Native memory profiling (malloc/free interception, sampling) | [doc/architecture/NativeMemoryProfiling.md](doc/architecture/NativeMemoryProfiling.md) |
| Startup sanity checks | [doc/architecture/SanityChecks.md](doc/architecture/SanityChecks.md) |
| SIGSEGV-safe library patching | [doc/architecture/SigsegvPatching.md](doc/architecture/SigsegvPatching.md) |
Expand Down
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
226 changes: 204 additions & 22 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,175 @@ void ReferenceChainTracker::addToStaticAnchorIndex(jlong tag,
root_kind != (u8)JVMTI_HEAP_REFERENCE_JNI_GLOBAL) {
return;
}
// A local holder later attached by a durable root now belongs to the uncapped durable share;
// release its slot in the local cap (it stays indexed - the dedup below is a no-op for it).
_local_anchor_tags.erase(tag);
indexAnchorForRotation(tag, own_class_tag);
}

void ReferenceChainTracker::noteLocalRootHolder(jlong tag,
jlong own_class_tag) {
if (_static_anchor_index_tags.count(tag) > 0) {
return; // already indexed (durable root or earlier local report)
}
auto shape = _class_shape_cache.find(own_class_tag);
bool confirmed_container = false;
if (shape != _class_shape_cache.end()) {
if (shape->second != (u8)AnchorClassShape::CONTAINER) {
return; // classified non-container: never indexed
}
if (_local_anchor_tags.size() < LOCAL_ANCHOR_INDEX_CAP) {
_local_anchor_tags.emplace(tag, own_class_tag);
indexAnchorForRotation(tag, own_class_tag);
return;
}
// At the cap: defer to resolvePendingLocalAnchors(), which may compact dead local entries
// (it can take the frontier lock; this callback must not).
confirmed_container = true;
}
if (_pending_local_anchors.size() >= PENDING_LOCAL_ANCHOR_CAP) {
if (!confirmed_container || _pending_local_anchor_tags.count(tag) > 0) {
return; // re-reported by the next root enumeration
}
// A confirmed container is walkable as soon as a local slot frees, so it outranks an entry not
// yet confirmed as a container: evict the oldest such entry (re-reported by the next root
// enumeration). Forward-only cursor: while the buffer is full nothing is appended, the only
// overwrite is at the cursor with a container, and skipped slots are containers, so every slot
// below the cursor stays a confirmed container and the scan totals <= PENDING_LOCAL_ANCHOR_CAP
// per enumeration.
if (_pending_local_evict_cursor > _pending_local_anchors.size()) {
_pending_local_evict_cursor = 0; // defensive; cannot happen while the buffer is full
}
while (_pending_local_evict_cursor < _pending_local_anchors.size()) {
auto s = _class_shape_cache.find(
_pending_local_anchors[_pending_local_evict_cursor].class_tag);
if (s == _class_shape_cache.end() ||
s->second != (u8)AnchorClassShape::CONTAINER) {
break;
}
_pending_local_evict_cursor++;
}
if (_pending_local_evict_cursor >= _pending_local_anchors.size()) {
return; // only confirmed containers parked: dropped, re-reported by the next enumeration
}
PendingLocalAnchor &victim = _pending_local_anchors[_pending_local_evict_cursor++];
_pending_local_anchor_tags.erase(victim.tag);
victim = PendingLocalAnchor{tag, own_class_tag};
_pending_local_anchor_tags.insert(tag);
return;
}
if (!_pending_local_anchor_tags.insert(tag).second) {
return;
}
_pending_local_anchors.push_back(PendingLocalAnchor{tag, own_class_tag});
}

void ReferenceChainTracker::resolvePendingLocalAnchors() {
if (_pending_local_anchors.empty()) {
return;
}
bool compacted = false;
for (const PendingLocalAnchor &pending : _pending_local_anchors) {
if (_static_anchor_index_tags.count(pending.tag) > 0) {
continue;
}
auto shape = _class_shape_cache.find(pending.class_tag);
if (shape == _class_shape_cache.end() ||
shape->second != (u8)AnchorClassShape::CONTAINER) {
continue; // non-container, or still unclassified: dropped (see the buffer's comment)
}
if (_local_anchor_tags.size() >= LOCAL_ANCHOR_INDEX_CAP && !compacted) {
// At most one compaction attempt per call. After a compaction that removed nothing (local
// share full of live containers), skip the next LOCAL_ANCHOR_COMPACT_BACKOFF attempts so a
// saturated-but-live share does not pay the locked O(cap) scan every pass.
compacted = true;
if (_local_compact_backoff > 0) {
_local_compact_backoff--;
} else {
_local_compact_backoff =
compactLocalAnchors() == 0 ? LOCAL_ANCHOR_COMPACT_BACKOFF : 0;
}
}
if (_local_anchor_tags.size() >= LOCAL_ANCHOR_INDEX_CAP) {
continue; // still full (live containers, or compaction backed off): the cap holds
}
_local_anchor_tags.emplace(pending.tag, pending.class_tag);
indexAnchorForRotation(pending.tag, pending.class_tag);
}
// Keep capacity: the buffer refills on every root enumeration.
_pending_local_anchors.clear();
_pending_local_anchor_tags.clear();
_pending_local_evict_cursor = 0;
}

size_t ReferenceChainTracker::compactLocalAnchors() {
if (_local_anchor_tags.empty()) {
return 0;
}
std::unordered_set<jlong> removed;
_frontier->withSharedLock([&](const FrontierTable *frontier) {
// Only the local share (O(cap)): the durable share is never compacted here.
for (const auto &local : _local_anchor_tags) {
FrontierEntry entry{};
// Same eligibility as collectStaticFieldAnchorsForRotation(): a released (ABANDONED) entry
// is a dead object and can never be selected again.
if (!frontier->lookupLocked(local.first, &entry) ||
!isSelectableAnchor(entry, local.second) ||
(entry.state != FrontierEntryState::FRONTIER &&
entry.state != FrontierEntryState::EXPANDED)) {
removed.insert(local.first);
}
}
});
if (removed.empty()) {
return 0;
}
// Stable in-place compaction of the two parallel vectors keeps relative order, so the fresh
// queue (a suffix of the index) stays a suffix once the same tags leave it too.
size_t out = 0;
for (size_t i = 0; i < _static_anchor_index.size(); i++) {
jlong tag = _static_anchor_index[i];
if (removed.count(tag) > 0) {
continue;
}
_static_anchor_index[out] = tag;
if (i < _static_anchor_own_class_tags.size()) {
_static_anchor_own_class_tags[out] = _static_anchor_own_class_tags[i];
}
out++;
}
_static_anchor_index.resize(out);
if (_static_anchor_own_class_tags.size() > out) {
_static_anchor_own_class_tags.resize(out);
}
for (jlong tag : removed) {
_static_anchor_index_tags.erase(tag);
_local_anchor_tags.erase(tag);
}
_static_anchor_fresh_queue.erase(
std::remove_if(_static_anchor_fresh_queue.begin(),
_static_anchor_fresh_queue.end(),
[&](jlong tag) { return removed.count(tag) > 0; }),
_static_anchor_fresh_queue.end());
// Positions shifted: the collector clamps out-of-range cursors; in-range ones only lose fairness
// for one lap.
return removed.size();
}

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 Expand Up @@ -469,30 +638,43 @@ bool ReferenceChainTracker::classImplementsContainerOrMap(jvmtiEnv *jvmti,
return found;
}

void ReferenceChainTracker::reconcileAnchorClassShapes(jvmtiEnv *jvmti,
JNIEnv *jni) {
if (jni == nullptr) {
return;
}
if (_static_anchor_own_class_tags.empty()) {
return;
}
// Collect up to ANCHOR_SHAPE_RECONCILE_BUDGET distinct class tags that appear in the anchor index
// but are not yet classified.
std::vector<jlong>
ReferenceChainTracker::collectUnclassifiedAnchorClassTags() const {
// Collect up to ANCHOR_SHAPE_RECONCILE_BUDGET distinct class tags that appear in the pending
// local-holder buffer or the anchor index but are not yet classified. Pending holders go first:
// resolvePendingLocalAnchors() drops any still unclassified after this call, while indexed
// durable anchors simply wait for a later pass.
std::vector<jlong> unknown;
unknown.reserve(8);
std::unordered_set<jlong> seen;
for (jlong class_tag : _static_anchor_own_class_tags) {
auto consider = [&](jlong class_tag) {
if (class_tag == 0 || seen.count(class_tag) > 0 ||
_class_shape_cache.count(class_tag) > 0) {
continue;
return true;
}
seen.insert(class_tag);
unknown.push_back(class_tag);
if ((int)unknown.size() >= ANCHOR_SHAPE_RECONCILE_BUDGET) {
break;
}
return (int)unknown.size() < ANCHOR_SHAPE_RECONCILE_BUDGET;
};
bool room = true;
for (size_t i = 0; room && i < _pending_local_anchors.size(); i++) {
room = consider(_pending_local_anchors[i].class_tag);
}
for (size_t i = 0; room && i < _static_anchor_own_class_tags.size(); i++) {
room = consider(_static_anchor_own_class_tags[i]);
}
return unknown;
}

void ReferenceChainTracker::reconcileAnchorClassShapes(jvmtiEnv *jvmti,
JNIEnv *jni) {
if (jni == nullptr) {
return;
}
if (_static_anchor_own_class_tags.empty() && _pending_local_anchors.empty()) {
return;
}
std::vector<jlong> unknown = collectUnclassifiedAnchorClassTags();
if (unknown.empty()) {
return;
}
Expand Down
Loading
Loading