Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5c1b6bf
JFR event types and emission for reference chains (#796)
jbachorik Sep 22, 2026
99f5d28
Implement reference-chain tracking and the leak-signal engine
jbachorik Sep 17, 2026
884dfa2
Deallocate GetObjectsWithTags results in hopLabelClassFor
jbachorik Sep 17, 2026
66be556
Fix clang scan-build findings in the tracker and engine
jbachorik Sep 17, 2026
914ca12
Fix review findings in the tracker and liveness engine
jbachorik Sep 17, 2026
7774a71
Clear the klass-population scratch in the test-reset seam
jbachorik Sep 18, 2026
545410f
Replace uncommitted-plan and line-number references with symbol refs
jbachorik Sep 18, 2026
4ea8965
Adapt tracker to merged ReferenceChainHop; drop Jira refs
jbachorik Sep 18, 2026
2c44905
Make comments layer-local and drop stale plan refs from tests
jbachorik Sep 18, 2026
6e8424c
Drop forward references to the profiler-side writer
jbachorik Sep 21, 2026
5e081be
Restore the merged-hop event API in the reference-chain tests
jbachorik Sep 21, 2026
9163650
Drop design-doc reference from the population table comment
jbachorik Sep 21, 2026
00976df
Drop the last cross-layer references from the test comments
jbachorik Sep 21, 2026
9fec63f
Fix epoch rollback, class-tag races and weak-ref handling in the live…
jbachorik Sep 23, 2026
69b28f2
Fix reference-chain pass termination, leak-tag root handling and batc…
jbachorik Sep 23, 2026
dbdf879
Wire the reference-chain tracker into the profiler lifecycle
jbachorik Sep 23, 2026
5cb686c
Split referenceChains into focused translation units with concise com…
jbachorik Sep 23, 2026
4b349cb
Reject out-of-range frontier tags and publish size under the write lock
jbachorik Sep 25, 2026
c80b7bf
Serialize the leak-tag pool and bound per-epoch JNI resolution
jbachorik Sep 25, 2026
017d2da
Fail safe the canary chain walk and retire dead marker-tag plumbing
jbachorik Sep 25, 2026
46800ff
Close the TOCTOU window in the reference-chains debug-knob read
jbachorik Sep 28, 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
2 changes: 1 addition & 1 deletion ddprof-lib/src/main/cpp/callTraceHashTable.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class CallTraceHashTable {
// - ACQUIRE loads in collect(), put(), and putWithExistingId()
// Required for correct visibility on weakly-ordered architectures (aarch64).
LongHashTable* _table;

volatile u64 _overflow;

u64 calcHash(int num_frames, ASGCT_CallFrame *frames, bool truncated);
Expand Down
1 change: 1 addition & 0 deletions ddprof-lib/src/main/cpp/callTraceStorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

#include <unistd.h>
#include "callTraceStorage.h"
#include "counters.h"
#include "log.h"
Expand Down
76 changes: 76 additions & 0 deletions ddprof-lib/src/main/cpp/classTagAllocator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2026, Datadog, Inc.
* SPDX-License-Identifier: Apache-2.0
*/

#ifndef _CLASS_TAG_ALLOCATOR_H
#define _CLASS_TAG_ALLOCATOR_H

#include "arch.h"
#include <jni.h>

// Process-wide, negative JVMTI class-object tag allocator, shared by
// ReferenceChainTracker (which tags every loaded class's own jclass object
// via SetTag - see resolveLoadedClasses(), referenceChains.cpp) and
// LivenessTracker (which needs a stable per-class identifier independent of
// Profiler::classMap()'s dictionary id - see KlassPopulationEntry::
// stable_class_tag's own comment, livenessTracker.h, for why: that
// dictionary can be compacted/regenerated, silently reassigning the same
// class a different id at different points in the process's life, breaking
// any attempt to correlate a klass_id LivenessTracker reports as growing
// against ReferenceChainTracker::FrontierEntry::referrer_klass values
// recorded at a different time).
//
// A single shared counter, not one independently owned by each subsystem,
// for two reasons, both load-bearing:
// 1. Two independent counters could otherwise hand out the SAME numeric
// value to TWO DIFFERENT classes (one minted by each subsystem for a
// class the other has not seen yet), making any cross-subsystem
// comparison meaningless.
// 2. Class tags must stay strictly NEGATIVE:
// ReferenceChainTracker::heapReferenceCallback() (referenceChains.cpp)
// uses `*tag_ptr < 0` to distinguish "this heap-walk-visited object is a
// pre-tagged class object" from an ordinary admitted instance (always
// tagged with a positive value via nextTag()). A class tagged by a
// counter that does not preserve this sign convention would be
// misidentified as an ordinary object and incorrectly admitted into the
// frontier table - a real correctness bug, not just a matching
// inconvenience.
//
// Deliberately a plain header-only function (Meyer's-singleton pattern,
// exactly like LivenessTracker::instance()/ReferenceChainTracker::
// instance()'s own lazy-static singletons) rather than a member of either
// singleton class: ReferenceChainTracker already depends on LivenessTracker
// (referenceChains.cpp includes livenessTracker.h and calls into it), so
// putting this counter inside either one and having the other call into it
// would introduce a circular dependency between the two headers.
namespace ClassTagAllocator {

inline volatile jlong &magnitude() {
static volatile jlong m = 1;
return m;
}

// Hands out a fresh negative class tag - see this file's own header comment
// for why negative, and why this must be the only place in the process that
// mints one.
inline jlong next() { return -atomicIncRelaxed(magnitude(), (jlong)1); }

// Test-only: resets the shared counter back to its starting value. Without
// this, gtest cases that assert on exact tag values (e.g. "the first class
// tagged gets -1") would see values keep climbing across every TEST_F in the
// same gtest binary, since this counter is genuinely process-wide (shared
// with LivenessTracker) rather than per-ReferenceChainTracker-instance.
inline void resetForTest() {
// Atomic exchange, matching next()'s atomicIncRelaxed RMW on the same
// variable: a plain volatile store can tear or be lost against a concurrent
// RMW (e.g. a tracker thread from a prior TEST_F not fully quiesced), which
// would mint duplicate negative tags - the cross-subsystem collision this
// shared allocator exists to prevent. Callers must still ensure no tracker
// thread is live (reset in TearDown after tracker->stop()).
__atomic_exchange_n(&magnitude(), (jlong)1, __ATOMIC_RELAXED);
}

} // namespace ClassTagAllocator

#endif
39 changes: 39 additions & 0 deletions ddprof-lib/src/main/cpp/counters.h
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,45 @@
* signal for spotting a recurrence. */ \
X(METADATA_TREE_NULL_CHILD, "metadata_tree_null_child") \
X(METADATA_TREE_DEPTH_EXCEEDED, "metadata_tree_depth_exceeded") \
/* A resolved datadog.ReferenceChain could not be cached in \
* ReferenceChainTracker::_resolved_chains (referenceChains.h): a brand-new \
* leak-candidate klass arrived with the cache already at \
* MAX_RESOLVED_CHAINS, so its chain is dropped rather than evicting some \
* other still-live sample's chain. See that constant's own comment. */ \
X(REFERENCE_CHAIN_EVENTS_DROPPED, "reference_chain_events_dropped") \
/* ReferenceChainTracker::releaseSearchTags() (referenceChains.cpp) failed \
* to call GetObjectsWithTags() for at least one batch - the search's tag \
* release is retried on a later call rather than proceeding, but this \
* counts how often that retry path is taken. */ \
X(REFERENCE_CHAIN_TAG_RELEASE_FAILED, "reference_chain_tag_release_failed") \
/* The profiler-side reference-chain writer could not acquire a \
* sample-record lock within its bounded retry budget and dropped the \
* already-dequeued datadog.ReferenceChain event for this dump - not \
* permanently lost, since ReferenceChainTracker::_resolved_chains (see \
* REFERENCE_CHAIN_EVENTS_DROPPED above) keeps the resolved chain cached \
* and re-emits it on a later dump while the leak candidate is still \
* live. */ \
X(REFERENCE_CHAIN_WRITE_DROPPED, "reference_chain_write_dropped") \
/* 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. */ \
X(REFERENCE_CHAIN_FRONTIER_TABLE_BYTES, "reference_chain_frontier_table_bytes") \
X(REFERENCE_CHAIN_FRONTIER_TABLE_CAPACITY, "reference_chain_frontier_table_capacity") \
X(REFERENCE_CHAIN_CANDIDATE_COUNT, "reference_chain_candidate_count") \
X(REFERENCE_CHAIN_CANDIDATES_FOUND, "reference_chain_candidates_found") \
/* admitStaticFieldRoots() per-class non-static quota: non-STATIC_FIELD \
* edges (CONSTANT_POOL, INTERFACE, SUPERCLASS, CLASS_LOADER, ...) that \
* were dropped because the class already hit \
* STATIC_FIELD_SWEEP_NON_STATIC_CAP_PER_CLASS. Total drops across all \
* classes/laps — compare against kind_counts (k9 total) to gauge how \
* much CP pressure the quota is absorbing. */ \
X(REFERENCE_CHAIN_STATIC_SWEEP_NON_STATIC_DROPPED, "reference_chain_static_sweep_non_static_dropped") \
/* Incremented once per class that hit the non-static cap at least once \
* in a lap (on the first drop for that class). Distinguishes "a few fat \
* outlier classes dropping many edges" from "systematic drops across \
* almost all classes" — if this tracks the total class count per lap, \
* the cap is too low; if it stays near zero, the cap is fine. */ \
X(REFERENCE_CHAIN_STATIC_SWEEP_CLASSES_CAPPED, "reference_chain_static_sweep_classes_capped") \
DD_COUNTER_TABLE_FAULT_INJECTION(X) \
DD_COUNTER_TABLE_FI_DEBUG(X) \
DD_COUNTER_TABLE_SAMPLER_PERF(X) \
Expand Down
9 changes: 5 additions & 4 deletions ddprof-lib/src/main/cpp/flightRecorder.h
Original file line number Diff line number Diff line change
Expand Up @@ -581,20 +581,21 @@ class FlightRecorder {

// Mirrors recordHeapUsage()'s shape exactly - ReferenceChainAbandonedEvent
// is not stack-sample-shaped (no tid/call_trace_id), same as HeapUsage.
// Called from Profiler::writeReferenceChainAbandoned() (profiler.cpp),
// Called from the profiler's dump-time abandoned-event drain,
// wired from Profiler::dump() the same way LivenessTracker::flush() is.
void recordReferenceChainAbandoned(int lock_index,
ReferenceChainAbandonedEvent *event);

// Mirrors recordReferenceChainAbandoned() above exactly, for
// ReferenceChainEvent instead. Called from Profiler::writeReferenceChain()
// (profiler.cpp), itself called from Profiler::dump()'s drain loop over
// ReferenceChainEvent instead. Called from the profiler's dump()-time
// writer, itself called from Profiler::dump()'s drain loop over
// the engine's resolved-chain cache snapshot: the BFS
// scheduling thread only caches resolved chains and each dump re-emits
// the cache, so chain events
// are written on dump()'s own thread, not from the tracker thread, and
// unlike recordReferenceChainAbandoned() (unbounded retry budget per
// event) the batch shares one deadline (writeReferenceChain()'s comment).
// event) the batch shares one deadline (see the writer's contract in
// Profiler - the drain batch, not each event, owns the retry budget).
void recordReferenceChain(int lock_index, ReferenceChainEvent *event);
};

Expand Down
Loading
Loading