Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5935c5c
Add repro/sweep tooling and chaos-harness build wiring for reference …
jbachorik Aug 6, 2026
dc8071d
Implement reference chains for surviving live-heap samples
jbachorik Aug 6, 2026
1a3165d
Add C++ unit tests for reference-chain tracking
jbachorik Aug 6, 2026
6f1fd14
Add Java integration tests and chaos/repro harness for reference chains
jbachorik Aug 6, 2026
4993cb6
Add reference-chains architecture and design docs
jbachorik Aug 6, 2026
ee58647
Address reference-chains review findings
jbachorik Aug 6, 2026
3540b64
Migrate ReferenceChainTrackingTest to the JfrEvents API
jbachorik Aug 6, 2026
f6e57d6
Address reference-chains review findings
jbachorik Aug 11, 2026
5adb328
Add heap-wide time-to-OOM predictor to fast-path leak detection
jbachorik Aug 12, 2026
321ed23
Add debug seams and test for reference-chain OOM-bypass gate
jbachorik Aug 12, 2026
21f256e
Fix AggressiveLeakReferenceChainTest race with real GC heap-floor sam…
jbachorik Aug 13, 2026
628db7f
Fix non-atomic cross-thread writes in AggressiveLeak test seams
jbachorik Aug 13, 2026
84c703b
Add backward-compatible setContextValue(int, CharSequence) overload
jbachorik Aug 14, 2026
d8243f4
Remove backward-compatible setContextValue(int, CharSequence) overload
jbachorik Aug 14, 2026
5fd5318
Reset pain budget in resetSearchStateForTest to fix musl CI failures
jbachorik Aug 14, 2026
facdc70
Auto-tune reference chain defaults based on heap size and CPU count
jbachorik Aug 17, 2026
8e44ff2
Replace TTL with progress-based termination + urgency-driven budget e…
jbachorik Aug 18, 2026
1f25f8a
Add reference chain canary search design doc
jbachorik Aug 18, 2026
3777ae1
Revert "Add reference chain canary search design doc"
jbachorik Aug 18, 2026
1d6a65d
Implement canary search: pruned BFS from GC roots to leaked candidates
jbachorik Aug 18, 2026
c1c08f1
Remove planning doc from tracked files
jbachorik Aug 18, 2026
e7cf195
Run passes back-to-back; frontier cap as memory bound only
jbachorik Aug 18, 2026
7e5c97d
Remove planning doc from tracked files
jbachorik Aug 18, 2026
f745651
Fix canary chain reconstruction: frontier table lookup rejects negati…
jbachorik Aug 19, 2026
b5dc65f
Add canary search counters: candidate_count and candidates_found
jbachorik Aug 19, 2026
19b431e
Fix JavaProfiler.java merge conflict resolution
jbachorik Aug 19, 2026
78c2aad
Fix canary pruning: check marker tag before class-tag check
jbachorik Aug 19, 2026
eac95a6
Fix canary chain: store candidate's own klass, not referrer's klass
jbachorik Aug 19, 2026
6be9f56
Fix canary pruning: handle root-referenced candidates
jbachorik Aug 19, 2026
117d6b3
Fix canary pre-tagging: run when candidates appear, not only on first…
jbachorik Aug 20, 2026
e696f72
Add debug logging to hasQualifyingGrowth for leak detection diagnosis
jbachorik Aug 20, 2026
288906f
Replace thirds trend with full-window linear regression
jbachorik Aug 20, 2026
6acf9b0
Add debug logging to heapFloorRising for leak detection diagnosis
jbachorik Aug 20, 2026
0448ef8
Add comprehensive debug logging to leak detection pipeline
jbachorik Aug 20, 2026
7aed7f8
Add debug logging to recordHeapFloorSample to diagnose empty heap flo…
jbachorik Aug 20, 2026
9776776
Fix heap floor reading committed size on JDK 26+ ZGC
jbachorik Aug 20, 2026
7a050c4
Cache ZGC JDK26+ check once in initialize, use in resolvePostGcHeapUsage
jbachorik Aug 20, 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
12 changes: 11 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,14 @@ doc/temp/
# CLAUDE.md is auto-generated from AGENTS.md bootstrap instructions
CLAUDE.md

.sphinx
# OS/editor cruft
.DS_Store

# AI review/agent tooling scratch state
.sphinx/
.skill-builder-temp/
.claude/scheduled_tasks.lock

# Python bytecode cache
__pycache__/
*.pyc
113 changes: 113 additions & 0 deletions ddprof-lib/src/main/cpp/arguments.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "arguments.h"
#include "vmEntry.h"

#include <algorithm>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
Expand Down Expand Up @@ -81,6 +82,26 @@ static const Multiplier UNIVERSAL[] = {
// and keep the liveness track of 10% of the allocation
// samples
// generations - track surviving generations
// referencechains[=BOOL[:hops=N][:budget=N][:ttl=N][:framecap=N][:pausetarget=N][:painbudget=N][:firstpassbudget=N]]
// - (PROF-15341, off by default) tag/BFS-walk live-heap
// samples' referrer chains back toward a GC root.
// pausetarget=N (ms) is the pause-time-SLO ceiling
// ReferenceChainTracker::updatePacing() adapts the
// effective budget/cadence toward (pause-time pacing
// controller). painbudget=N (percent) bounds how much
// wall-clock time a *restarted* search (one begun
// after a prior search already completed/abandoned)
// may spend on average - see PainBudget (painBudget.h).
// firstpassbudget=N overrides just the search's
// one-shot, root-seeded first pass's edge budget
// (default 0 - auto-scales from budget=N instead,
// see ReferenceChainTracker::AUTO_FIRST_PASS_BUDGET_*
// in referenceChains.h) since that pass alone
// decides which GC roots ever enter the frontier at
// all, unlike every later pass's cheap, incremental
// per-node expansion.
// Sub-options are placeholders pending future tuning;
// see doc/architecture/LiveHeapReferenceChains*.md
// lightweight[=BOOL] - enable lightweight profiling - events without
// stacktraces (default: true)
// remotesym[=BOOL] - enable remote symbolication for native frames
Expand Down Expand Up @@ -444,6 +465,98 @@ Error Arguments::parse(const char *args) {
_nativesocket = true;
}

CASE("referencechains")
{
// Sub-options are colon-delimited key=value pairs after the boolean,
// e.g. "referencechains=true:hops=64:budget=2000". Parsed manually
// (not via strtok) because the outer arg loop above is itself mid
// strtok(..., ",") over the same buffer - a nested strtok call would
// clobber its saved state.
char *config = value ? strchr(value, ':') : nullptr;
if (config) {
*(config++) = 0;
}
if (value != NULL) {
switch (value[0]) {
case 'n': // no
case 'f': // false
case '0': // 0
_reference_chains = false;
break;
default:
_reference_chains = true;
}
} else {
_reference_chains = true;
}
char *cursor = config;
while (cursor != NULL) {
char *next = strchr(cursor, ':');
if (next) {
*(next++) = 0;
}
char *eq = strchr(cursor, '=');
if (eq) {
*(eq++) = 0;
// Floor every sub-option at the parse boundary rather than
// trusting a downstream cast/clamp to make an operator-supplied
// negative value safe: a negative hops value in particular gets
// compared as `depth >= (u32)ctx->hop_cap` (referenceChains.cpp),
// so an unclamped negative wraps to ~4e9 and silently disables
// the hop cap entirely - the opposite of the flag's intent, and
// it removes the one guard that otherwise bounds how long a
// single reference chain (and therefore its
// datadog.ReferenceChain JFR event) can grow. A negative budget
// similarly collapses ReferenceChainTracker::_effective_budget
// to 0 (updatePacing()'s own PID-clamp logic), which truncates
// every pass immediately and leaves the search RUNNING
// (re-walking the whole graph each cadence) until TTL instead of
// making progress. A negative framecap is handed straight to
// FrontierTable's constructor, which floors it to a
// zero-capacity table (that class's own std::max(max_cap, 0)),
// silently disabling tracking rather than erroring. ttl/
// pausetarget/painbudget already have incidental downstream
// clamps (runPass()'s `_ttl_ms > 0` gate, this class's own
// PidController/PainBudget std::max(..., 0) calls) but are
// floored here too so every sub-option's validation lives at one
// boundary instead of being split between here and several
// unrelated call sites. hops/budget/framecap are also ceiling-
// clamped (MAX_REFERENCE_CHAINS_HOP_CAP/_BUDGET/_FRONTIER_CAP,
// arguments.h) for the same reason painbudget/firstpassbudget
// are below: an unbounded operator-supplied value would otherwise
// flow straight into a loop bound or FrontierTable's allocation.
if (strcasecmp(cursor, "hops") == 0) {
_reference_chains_hop_cap =
std::min(std::max(atoi(eq), 1), MAX_REFERENCE_CHAINS_HOP_CAP);
_reference_chains_tuned_mask |= REF_CHAINS_TUNED_HOP_CAP;
} else if (strcasecmp(cursor, "budget") == 0) {
_reference_chains_budget =
std::min(std::max(atoi(eq), 1), MAX_REFERENCE_CHAINS_BUDGET);
_reference_chains_tuned_mask |= REF_CHAINS_TUNED_BUDGET;
} else if (strcasecmp(cursor, "ttl") == 0) {
_reference_chains_ttl_ms = std::max(atol(eq), 0L);
_reference_chains_tuned_mask |= REF_CHAINS_TUNED_TTL;
} else if (strcasecmp(cursor, "framecap") == 0) {
_reference_chains_frontier_cap = std::min(
std::max(atoi(eq), 1), MAX_REFERENCE_CHAINS_FRONTIER_CAP);
_reference_chains_tuned_mask |= REF_CHAINS_TUNED_FRONTIER_CAP;
} else if (strcasecmp(cursor, "pausetarget") == 0) {
_reference_chains_pause_target_ms = std::max(atol(eq), 0L);
_reference_chains_tuned_mask |= REF_CHAINS_TUNED_PAUSE_TARGET;
} else if (strcasecmp(cursor, "painbudget") == 0) {
_reference_chains_pain_budget_percent =
std::min(std::max(atoi(eq), 0), 100);
_reference_chains_tuned_mask |= REF_CHAINS_TUNED_PAIN_BUDGET;
} else if (strcasecmp(cursor, "firstpassbudget") == 0) {
_reference_chains_first_pass_budget = std::min(
std::max(atoi(eq), 0), MAX_REFERENCE_CHAINS_FIRST_PASS_BUDGET);
_reference_chains_tuned_mask |= REF_CHAINS_TUNED_FIRST_PASS_BUDGET;
}
}
cursor = next;
}
}

DEFAULT()
if (_unknown_arg == NULL)
_unknown_arg = arg;
Expand Down
140 changes: 140 additions & 0 deletions ddprof-lib/src/main/cpp/arguments.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,124 @@
#include <string>
#include <vector>

#include "arch.h"

const long DEFAULT_CPU_INTERVAL = 10 * 1000 * 1000; // 10 ms
const long DEFAULT_WALL_INTERVAL = 50 * 1000 * 1000; // 50 ms
const long DEFAULT_ALLOC_INTERVAL = 524287; // 512 KiB
const int DEFAULT_WALL_THREADS_PER_TICK = 16;
const int DEFAULT_JSTACKDEPTH = 2048;

// Every constant below is a provisional default pending empirical
// tuning (see doc/architecture/LiveHeapReferenceChains-ImplementationPlan.md)
// - none of these values are backed by a benchmark run against this
// codebase. Each is chosen conservatively from cited precedent or from the
// shape of an existing, already-tuned subsystem, per the rationale below;
// a future JMH/async-profiler benchmark matrix (see
// doc/architecture/LiveHeapReferenceChains-BenchmarkPlan.md) is the intended
// path to replacing them with measured values.
//
// Hop cap: mirrors HotSpot's own JFR leak-profiler chain cap (~200 hops,
// split 100/100 from leaf and from root), cited in
// doc/architecture/LiveHeapReferenceChains.md's "Approach B" section - the
// closest real-world precedent for "how many hops does a referrer-type
// chain typically need" that this codebase can cite without measuring it
// itself.
// Sub-option bitmask for the auto-tuner: tracks which referencechains
// sub-options were explicitly set by the operator, so the auto-tuner
// only overrides defaults that weren't.
constexpr u8 REF_CHAINS_TUNED_HOP_CAP = 1 << 0;
constexpr u8 REF_CHAINS_TUNED_BUDGET = 1 << 1;
constexpr u8 REF_CHAINS_TUNED_TTL = 1 << 2;
constexpr u8 REF_CHAINS_TUNED_FRONTIER_CAP = 1 << 3;
constexpr u8 REF_CHAINS_TUNED_PAUSE_TARGET = 1 << 4;
constexpr u8 REF_CHAINS_TUNED_PAIN_BUDGET = 1 << 5;
constexpr u8 REF_CHAINS_TUNED_FIRST_PASS_BUDGET = 1 << 6;

const int DEFAULT_REFERENCE_CHAINS_HOP_CAP = 200;
// Per-pass edge budget: no cited precedent gives a number for this (JFR's
// leak profiler does not bound itself by a per-pass edge count - it runs to
// completion inside one already-scheduled GC pause). Chosen as a round,
// conservative middle value intended to keep a single FollowReferences-
// triggered safepoint short without so small a budget that a search needs
// an impractical number of passes to make progress. A future benchmark pass
// should measure per-pass wall-clock pause distribution at this value and adjust.
const int DEFAULT_REFERENCE_CHAINS_BUDGET = 1000; // edges expanded per BFS pass
// Per-search TTL: a conservative round number (one minute) chosen so a
// slow-moving or stalled search is bounded to a human-noticeable but not
// excessive lifetime, in the absence of any measured "passes needed to
// reach a target sample at various depths" data (a future benchmark's stated goal).
const long DEFAULT_REFERENCE_CHAINS_TTL_MS = 60000; // per-search wall-clock TTL
// Frontier-size cap: sized relative to LivenessTracker's own tuned ceiling
// (MAX_TRACKING_TABLE_SIZE = 262144, livenessTracker.h) rather than derived
// from any BFS-specific measurement - the design doc explicitly flags that
// LivenessTracker's allocation-sample-rate sizing formula does not transfer
// to a graph-search frontier (Open Question 2), so this only borrows the
// same order of magnitude, quartered as a conservative starting point since
// a FrontierEntry is smaller than a TrackingEntry but per-hop fan-out could
// still be large. Not a scaled/derived value - just a conservative guess
// pending a future frontier-table peak-occupancy measurement.
const int DEFAULT_REFERENCE_CHAINS_FRONTIER_CAP = 65536; // max live frontier entries per search
// Pause-time-SLO ceiling (pause-time pacing controller, doc/architecture/
// LiveHeapReferenceChains-RemainingWorkPlan.md): target ceiling, per pass, on
// wall-clock time spent inside the safepoint-triggering
// FollowReferences/GetObjectsWithTags call
// (ReferenceChainTracker::updatePacing(), referenceChains.cpp). Like every
// other constant in this block this is a round, provisional default with no
// benchmark behind it - picking the real number is explicitly a future
// measurement question (design doc's Open Question 2), not a value to guess
// here; this only exists so the feedback loop this ceiling drives has
// something to target end-to-end before that measurement happens.
const long DEFAULT_REFERENCE_CHAINS_PAUSE_TARGET_MS = 5; // ms per pass
// Pain budget refill rate (ReferenceChainTracker::PainBudget, painBudget.h):
// the fraction of wall-clock time a *restarted* search is allowed to spend
// inside FollowReferences/GetObjectsWithTags safepoints, on average, before a
// later restart must wait for the debt from the previous search's cost to
// drain. Expressed as an integer percent (1 = 1%) for readability - see
// PainBudget's own header comment for why this single ratio needs no
// benchmark-derived tuning the way the per-pass constants above do, only a
// choice of how much background cost is acceptable. Round, provisional
// default like every other constant in this block.
const int DEFAULT_REFERENCE_CHAINS_PAIN_BUDGET_PERCENT = 1;
// First-pass edge budget override: the search's one-and-only root-seeded
// FollowReferences(0, nullptr, nullptr, ...) call (ReferenceChainTracker::runPass()'s
// !_search_started branch) enumerates every GC root in one JVMTI-controlled
// traversal order and stops admitting once this budget is spent - any root
// FollowReferences had not yet reached is excluded from the frontier for the
// rest of that search (every later pass only expands forward from already-
// admitted frontier entries, see expandFrontier()'s own comment). Unlike
// DEFAULT_REFERENCE_CHAINS_BUDGET, which bounds every pass including the many
// cheap, per-node expansion passes that follow, this only ever spends once
// per search, so a much larger one-time ceiling is affordable. 0 (the
// default) means "no override - use the same budget as every other pass",
// preserving prior behavior for anyone not setting this explicitly.
const int DEFAULT_REFERENCE_CHAINS_FIRST_PASS_BUDGET = 0;
// Upper clamp for an explicit firstpassbudget override: like painbudget just
// above, firstpassbudget was previously only floored at 0 with no ceiling.
// Unlike painbudget (a percentage, naturally bounded at 100), this is a raw
// edge count, so the ceiling is expressed relative to
// DEFAULT_REFERENCE_CHAINS_BUDGET (the per-pass budget every later pass is
// bounded by) rather than as its own standalone guess: a generous but finite
// multiple still lets the one-time root pass be far larger than a normal
// pass (its intended purpose) while keeping an operator from disabling the
// safepoint-pause-bounding mechanism entirely for that first FollowReferences
// call.
const int MAX_REFERENCE_CHAINS_FIRST_PASS_BUDGET =
DEFAULT_REFERENCE_CHAINS_BUDGET * 1000;
// Upper clamps for hops/budget/framecap: like MAX_REFERENCE_CHAINS_FIRST_PASS_BUDGET
// just above, these were previously only floored at 1 with no ceiling, so an
// operator typo (an extra digit) or a mistaken value flows straight into a
// loop bound (hops), a per-pass edge count (budget), or FrontierTable's
// capacity (framecap) unchecked. Same generous-but-finite-multiple-of-the-
// default approach as the first-pass budget clamp: large enough that no
// legitimate configuration should ever hit the ceiling, small enough to
// still fail a badly mistyped value safely instead of feeding it straight
// into an allocation or loop bound.
const int MAX_REFERENCE_CHAINS_HOP_CAP = DEFAULT_REFERENCE_CHAINS_HOP_CAP * 1000;
const int MAX_REFERENCE_CHAINS_BUDGET = DEFAULT_REFERENCE_CHAINS_BUDGET * 1000;
const int MAX_REFERENCE_CHAINS_FRONTIER_CAP =
DEFAULT_REFERENCE_CHAINS_FRONTIER_CAP * 1000;

const char *const EVENT_NOOP = "noop";
const char *const EVENT_CPU = "cpu";
const char *const EVENT_ALLOC = "alloc";
Expand Down Expand Up @@ -177,6 +289,25 @@ class Arguments {
double _live_samples_ratio;
bool _record_heap_usage;
bool _gc_generations;
// Reference-chain tracking (PROF-15341 - see
// doc/architecture/LiveHeapReferenceChains-ImplementationPlan.md and
// -RemainingWorkPlan.md). Read by ReferenceChainTracker::start()
// (referenceChains.cpp) to size the frontier table and seed the per-search
// hop/budget/TTL tunables and the pause-time-SLO ceiling that
// updatePacing() adapts the effective budget/cadence toward.
bool _reference_chains;
int _reference_chains_hop_cap;
int _reference_chains_budget;
long _reference_chains_ttl_ms;
int _reference_chains_frontier_cap;
long _reference_chains_pause_target_ms;
int _reference_chains_pain_budget_percent;
int _reference_chains_first_pass_budget;
// Bitmask of REF_CHAINS_TUNED_*: which sub-options were explicitly
// set by the operator, so the auto-tuner knows which defaults it may
// override. 0 = all defaults, none explicitly set.
u8 _reference_chains_tuned_mask;
// Explicit opt-in for the legacy whole-graph JVMTI FollowReferences walk.
long _nativemem;
int _jstackdepth;
int _safe_mode;
Expand Down Expand Up @@ -219,6 +350,15 @@ class Arguments {
_live_samples_ratio(0.1), // default to liveness-tracking 10% of the allocation samples
_record_heap_usage(false),
_gc_generations(false),
_reference_chains(false),
_reference_chains_hop_cap(DEFAULT_REFERENCE_CHAINS_HOP_CAP),
_reference_chains_budget(DEFAULT_REFERENCE_CHAINS_BUDGET),
_reference_chains_ttl_ms(DEFAULT_REFERENCE_CHAINS_TTL_MS),
_reference_chains_frontier_cap(DEFAULT_REFERENCE_CHAINS_FRONTIER_CAP),
_reference_chains_pause_target_ms(DEFAULT_REFERENCE_CHAINS_PAUSE_TARGET_MS),
_reference_chains_pain_budget_percent(DEFAULT_REFERENCE_CHAINS_PAIN_BUDGET_PERCENT),
_reference_chains_first_pass_budget(DEFAULT_REFERENCE_CHAINS_FIRST_PASS_BUDGET),
_reference_chains_tuned_mask(0),
_nativemem(-1),
_jstackdepth(DEFAULT_JSTACKDEPTH),
_safe_mode(0),
Expand Down
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
Loading
Loading