Skip to content

Fix reference-chain collector running without a leak signal - #921

Open
jbachorik wants to merge 20 commits into
mainfrom
investigate-reference-chains
Open

jbachorik wants to merge 20 commits into
mainfrom
investigate-reference-chains

Conversation

@jbachorik

@jbachorik jbachorik commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?:

Fixes the live-heap reference-chain collector running whole-heap searches in healthy processes. Two authorization paths bypassed the leak-candidate requirement:

  • hasLeakSignal() returned true unconditionally when generations tracking was off. A liveheap-only session then passed the gate with zero leak candidates, and pollWatchedTargets() fed the search loop the same way. The search cannot exit early — completion requires an untruncated pass and the 30-pass no-progress abandonment is suppressed while urgent — so it ran to the auto-tuned TTL. Staging evidence: frontier table grown to its cap, 0 ReferenceChain events, 0 abandonment events.
  • The urgent-OOM latch entitled one candidate-less search per episode. A warmup heap-floor ramp projected imminent OOM (secondsToOOM() needs only 10 GC samples and has no JVM-age requirement), arming the path from a seconds-long observation window.

The fix:

  • hasLeakSignal() keys on an effective flag — generations OR liveheap tracking drives the same population-trend mechanisms — and requires selectLeakCandidates() > 0. With neither on, the collector stays idle: no pass, restart, or search.
  • Urgency no longer authorizes searches. It drives the ramp's cadence, pause target, and edge budget while a candidate-driven search runs. The per-search urgent entitlement flag was removed as behaviorally dead.
  • The ramp's 100% liveness admission no longer follows the raw projection band alone. Outside the emergency band (<5 min) it requires fresh growth evidence: the newest heap-floor sample must exceed the value observed when the ramp episode began. The projection is a least-squares regression over a 30-sample window, so a single spike sample keeps it inside the band for several recovery GCs after the heap has physically stopped growing; growth evidence is what keeps the admission boost off for such a recovered spike. Inside the emergency band, admission stays immediate.
  • secondsToOOM() returns no projection until the leak signal has been active for a burn-in period (OOM_PROJECTION_BURN_IN_NS, ~300 s), so transient warmup growth cannot latch urgency regardless of how frequently GCs compress the observation window.

Motivation:

Staging recordings of prof-analyzer (PROF-16284) showed the collector churning whole-heap BFS passes with zero leak candidates in healthy processes. Each pass pays a global handshake plus EA-materialization frame deopts (IterateOverReachableObjects → EscapeBarrier → deoptimize_frame), so an uncandidate search is pure recurring overhead on exactly the C2-heavy workloads where it stings.

Additional Notes:

  • Reviewed with an agentic review (/sphinx review --effort=deep): 11 findings, all 8 fix-selections implemented and verified before this PR (liveheap-only initialize() test cluster; observation-window guard replaced with the age-based burn-in; scratch exclusions). 3 informational/dismissed, 3 folded by adjudication.
  • A second sphinx review pass covered the review-fix delta itself (3 fixes sealed, 2 dismissed, all implemented): the recording-boundary reset of the admission growth baseline, DEBUG/UNIT_TEST guarding of the leak-signal test seams, the reference_chain_urgent_no_candidate counter making the candidate-only gate's refused urgent evaluations visible, a positive control against the AggressiveLeak test's burn-in-vacuous assertions, and a cap on the abandonment test's dump retries. Range validation of the gate's equality boundary included.
  • The PROF-16383 in-order flake of ReferenceChainTrackingTest.shouldReconstructReferrerChainThroughUnboundedCacheLeak (the CI note in the previous version of this description) is now fixed in this PR (last two commits). Root cause, from level-2 traces of forced genuine runs: the search's root walk runs once per search and later passes never re-visit walked regions; the fixture's HashMap resizes ~14x while growing, so the walked table/Node path dies and the walked region is never re-admitted; and the BFS thread's own backoff/restart scheduling leaves too few passes to retry discovery. The fix is test-only: the fixture cache is pre-sized past its final load (no resize, walked path survives), both success-path tests drive bounded passes through the debug seams so discovery depends on cumulative frontier progress instead of BFS cadence, and both failure messages carry a canonical diagnostics fingerprint (ReferenceChainAssertions.referenceChainFailureDiagnostics). Verified with forced in-order sweeps (cleanTestSlowDebug; repeated plain invocations are Gradle UP-TO-DATE no-ops): 6/6 + 7/7 passes, isolated pass, and a reproduction on the pre-fix baseline. A dedicated sphinx review of the fix (7 findings, 0 critical/high) sealed 4 fixes and 3 dismissals.
  • CI note: LeakTagCorrelationReferenceChainTest (shouldRotateFrontierLeakHoldersUnderPodLikeTightWindows, shouldCorrelateLeakChainTargetTagWithLiveObjectLeakTag) is separately flaky in-order. Both failures reproduce with this PR's test changes stashed or absent (Rotate also fails in isolation), and the class runs before any of the fix's classes in the shared test JVM — pre-existing, out of scope here, not covered by PROF-16383's fix.
  • The admission growth gate was adversarially design-reviewed before implementation. A iteration-count-based sustain was rejected: the ramp owns the loop cadence, so 30 iterations compress to sub-second at deep projections. The growth-evidence form (newest sample vs entry snapshot) is the minimal variant that actually holds for a recovered spike.
  • The 64-slot TLS pool exhaustion found during the same investigation is tracked separately in PROF-16366 and is not part of this change.

How to test the change?:

C++ unit tests: :ddprof-lib:gtestDebug — 1039 OK / 0 failed, including the new gate tests:

  • LivenessTrackerSessionTest.LeakSignalRequiresGenerationsOrLiveness / LeakSignalDerivedByInitialize / LeakSignalSourcesSeamMatchesDerivation — effective-flag derivation, including liveheap-only sessions.
  • SearchRestartTest.NoSearchWithoutLeakSignalSource / UrgentLatchWithoutCandidateDoesNotStartSearch — collector idles without a signal; urgency never starts a search.
  • OomUrgencyTest.WarmupShortWindowDoesNotLatchUrgency and SecondsToOOMTest burn-in cases — warmup growth cannot latch urgency.
  • OomUrgencyTest.AdmissionBoostRequiresGrowthPastEntryBaseline / RecoveredSpikeStaysUnboostedWhileProjectionLags / EmergencyBandAdmitsWithoutGrowthEvidence / BandExitResetsGrowthBaseline — the admission growth gate, including the smoothed-projection-lags-recovery counterexample.

For Datadog employees:

  • If this PR touches code that signs or publishes builds or packages, or handles
    credentials of any kind, I've requested a security review (run the dd:platform-security-review
    skill, or file a request via the PSEC review form).
    bewaire also runs automatically on every PR.
  • This PR doesn't touch any of that.
  • JIRA: PROF-16284

@jbachorik jbachorik added the AI label Oct 29, 2026
@jbachorik

Copy link
Copy Markdown
Collaborator Author

@DataDog review

@datadog-official datadog-official Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bits Code Review: FAIL

Existing Java integration scenarios still require candidate-free searches. The new admission gate prevents those searches, invalidating their pass-count and abandonment expectations.

Open Bits AI session

🤖 Bits Code Review · Commit 07ab531

@dd-octo-sts

dd-octo-sts Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

CI Test Results

Run: #41111734688 | Commit: 54f112a | Duration: 18m 43s (longest job)

✅ All 32 test jobs passed

Status Overview

JDK glibc-aarch64/debug glibc-amd64/debug musl-aarch64/debug musl-amd64/debug
8 - ✅ - -
8-ibm - ✅ - -
8-j9 ✅ ✅ - -
8-librca - - ✅ ✅
8-orcl - ✅ - -
11 - ✅ - -
11-j9 ✅ ✅ - -
11-librca - - ✅ ✅
17 ✅ ✅ - -
17-graal ✅ ✅ - -
17-j9 ✅ ✅ - -
17-librca - - ✅ ✅
21 ✅ ✅ - -
21-graal ✅ ✅ - -
21-librca - - ✅ ✅
25 ✅ ✅ - -
25-graal ✅ ✅ - -
25-librca - - ✅ ✅

Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled

Summary: Total: 32 | Passed: 32 | Failed: 0


Updated: 2026-10-30 19:23:21 UTC

@dd-octo-sts

dd-octo-sts Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

✅ All 40 integration tests passed

📊 Dashboard · 👷 Pipeline · 📦 e3410fca

@jbachorik
jbachorik force-pushed the investigate-reference-chains branch 2 times, most recently from aa527d2 to 2e0bf07 Compare October 29, 2026 16:09
@jbachorik

Copy link
Copy Markdown
Collaborator Author

Note for CI triage: ReferenceChainTrackingTest.shouldReconstructReferrerChainThroughUnboundedCacheLeak flakes when the class runs in-order (passes in isolation) — pre-existing, bisected on the base commit, not caused by this PR. Tracked in PROF-16383.

@jbachorik
jbachorik marked this pull request as ready for review October 30, 2026 10:00
@jbachorik
jbachorik requested a review from a team as a code owner October 30, 2026 10:00
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T10:04:53.547501Z 51348a4 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 51348a4fc8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@datadog-official datadog-official Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bits Code Review: PASS

More details

The candidate gate prevents urgency from starting empty searches while preserving liveheap-only population signals. Static review found the burn-in lifecycle and recording-boundary resets consistent with the intended warmup protection.

Was this helpful? React 👍 or 👎

Open Bits AI session

🤖 Bits Code Review · Commit 51348a4

@dd-octo-sts

dd-octo-sts Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Reliability & Chaos Results

✅ All reliability & chaos checks passed Pipeline: https://gitlab.ddbuild.io/DataDog/java-profiler/-/pipelines/148870218

The collector ran whole-heap searches in healthy processes through two
authorization paths that bypassed the candidate requirement:

- hasLeakSignal() returned true unconditionally when generations tracking
  was off, so a liveheap-only session passed the gate with zero leak
  candidates; the search cannot exit early (the no-progress abandonment is
  suppressed while urgent) and ran to the auto-tuned TTL.
- The urgent-OOM latch entitled one candidate-less search per episode, and
  a warmup heap-floor ramp projected imminent OOM from a seconds-long
  observation window.

hasLeakSignal() now keys on an effective flag (generations OR liveheap
tracking) and requires selectLeakCandidates() > 0; urgency only sharpens
the OOM ramp while a candidate-driven search runs. secondsToOOM() returns
no projection until the leak signal has been active for a burn-in period,
so transient warmup growth cannot latch urgency. The per-search
urgent-entitlement flag was removed (behaviorally dead).
AggressiveLeakReferenceChainTest asserted that the BFS thread runs passes
from the urgent-OOM projection alone with zero per-klass candidates - the
behavior the gate fix removed. It now asserts the opposite: with the leak
signal armed and the projection deep in the urgent band, the gate stays
closed (live-thread idle window plus a race-free direct shouldRunPass()
evaluation - the removed one-shot entitlement was what made the direct
call order-dependent).

ReferenceChainTrackingTest.shouldReportAbandonedSearchOnTinyFrontierCap
qualifies its candidate only through the debug-only population seams; in
a release build no candidate exists and the search now (correctly) stays
idle, so the method assumes a debug build. The profiler-command comment
still described the removed hasLeakSignal() short-circuit; updated.
The OOM ramp's 100% tracking admission followed the raw projection-band
comparison, but the smoothed 30-sample regression keeps the projection
inside the band for several recovery GCs after the heap has physically
stopped growing. Outside the emergency band (<5min) the boost now requires
the newest heap-floor sample to exceed the value observed at episode entry;
a recovered spike never satisfies that. Emergency-band admission stays
immediate, and the ramp's budget/cadence effects are unchanged.

Also corrects two stale claims about the urgency latch: the ramp
calculation never consults it (its only consumer is no-progress
abandonment suppression), and the budget boost follows the raw band, not
the latch.
stop() and resetSearchStateForTest() now clear _admission_growth_baseline
alongside the urgency latch: a stale baseline would let the next episode's
first urgent observation skip updateUrgentAdmission()'s entry re-snapshot
and admit against the previous recording's heap level.

hasLeakSignal() increments reference_chain_urgent_no_candidate when urgency
is latched but no klass explains the growth, so the candidate-only gate's
refused evaluations are visible in diagnostics instead of silent.

The leak-signal test mutators (setLeakSignalSourcesForTest,
setLeakSignalActivatedNsForTest) move behind DEBUG/UNIT_TEST with JNI
seams (backdateLeakSignalActivationForTest0 and companions) so no release
consumer can flip the gate or backdate the burn-in. Stale comments on
OOM_URGENT_THRESHOLD_S, gcGenerationsEnabled() and secondsToOOM() corrected
to match the candidate-only gate.
- StopClearsAdmissionGrowthBaseline pins the stop() baseline reset.
- AggressiveLeak: the negative search-gate test now asserts the projection
  is actually engaged (backdated burn-in stamp via the new seam), so its
  assertions cannot pass vacuously while secondsToOOM() is inside burn-in;
  cleanup goes through the shared restoreHeapFloorDefaults() helper.
- ReferenceChainTrackingTest: the abandonment retry loop is capped at 3
  dumps with a passesRun probe and paced last dump, bounding the
  O(recording-size) dump I/O; the dead if-guard behind the debug-build
  assumption is removed; a growth-gate equality scenario pins > vs >=.
The latch neither starts searches nor feeds the ramp calculation; its only
consumer is no-progress-abandonment suppression. The admission boost
releases on the raw band comparison, not the latch hysteresis. The
candidate-only gate's refused urgent evaluations are now counted.
- setLeakSignalActivatedNsForTest0 rejects negative arguments instead of
  casting them to a near-UINT64_MAX burn-in stamp that would read as
  burn-in-never-passed.
- evaluateGateAfterSearchReset samples the per-klass candidate table
  before the search reset: a pass decided on an organic candidate can
  straddle the reset (the pass counter increments at pass end), and the
  candidate stays selectable from the not-reset population table, so the
  pre-reset read keeps the candidateSeen bracket honest.
- The abandonment test's warmup dump path is deleted inside the try/finally
  guard, closing the leak window between createTempFile and guard entry.
- The stop()-side reset of the urgency/baseline fields now states the full
  claim: production writes go through the BFS thread only, and the join in
  stopThread() is what makes the control-thread plain stores safe.
- The leak-signal test-seam block now records why UNIT_TEST is required
  beyond DEBUG (gtestRelease compiles with -DNDEBUG and no -DDEBUG) and
  which consumer class needs each arm.
Each fact now has one home: the generations/liveheap equivalence lives on
_leak_signal_enabled, the SPSC acquire pairing on newestHeapFloorSample()'s
definition, the burn-in rationale on OOM_PROJECTION_BURN_IN_NS; the other
sites point instead of restating.
The reference-chain subsystem contract changed: collection stays idle
without a generations or liveheap leak-signal source. Reflect that in the
AGENTS.md subsystem row and the doc/README.md index entries.
Indexes only name the subsystem; activation semantics belong to the
linked pages.
The per-test compile task wired only the test .cpp into sources, while the
.inc test partitions and test-local headers resolve via quoted includes
relative to the test directory and appear in no include path. An .inc-only
edit therefore reported UP-TO-DATE and silently ran a stale binary with the
old tests. Register them as task inputs (not sources: they must not be
compiled standalone) and drop the touch-the-.cpp workaround comment.
Comments must state the mechanism, not how it was observed. Drop the
environment, counter values and recording-size anecdotes; the factual
claim each comment makes is unchanged.
The cache-leak and gc-root tests flake in-order because the search's root
walk runs once per search (no re-visit of walked regions), the fixtures'
HashMap/ArrayList resize churn invalidates the walked path mid-test, and
the BFS thread's own backoff/cooldown scheduling leaves too few passes to
retry discovery.

Fix, test-only:
- Pre-size the fixture cache (1<<17) so the walked table/Node path never
  dies to resize churn.
- Drive bounded passes via the runReferenceChainPass0() debug seams in
  both success-path tests' retry loops (release stubs are no-ops), so
  discovery depends on cumulative frontier progress, not BFS cadence.
- Widen both grace loops to 10 attempts; add failure diagnostics
  (leak-signal candidates, BFS pass count, representative frontier
  state, observed chain[0]s) plus the observedChainHeads() helper.
- Correct stale comments (dump() drain semantics, nonexistent
  writeReferenceChain symbol, MIN_EFFECTIVE_BUDGET floor).

Verified: 6/6 + 7/7 forced genuine in-order sweeps (cleanTestSlowDebug),
isolated pass; the unmodified baseline reproduces the original flake.
Review follow-ups (sphinx, 4 accepted LOW findings):
- Extract ReferenceChainAssertions.referenceChainFailureDiagnostics(): one
  canonical failure fingerprint shared by both discovery tests, closing the
  gc-root test's missing observedChain0s segment.
- Move the debug-build gate inside driveReferenceChainPasses(); call sites
  no longer repeat it.
- observedChainHeads() now reports <empty-chain> / <non-array-chain:...>
  sentinels instead of silently skipping malformed events; javadoc matches.
- Pin the fingerprint format in ReferenceChainTestSeamsTest.
@jbachorik
jbachorik force-pushed the investigate-reference-chains branch from 522ee15 to 9f0aeca Compare October 30, 2026 15:13

@rkennke rkennke left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the gating change and the new urgency/admission logic. The main concerns:

  1. Overhead regression for liveheap-only users: the leak-signal flag isn't tied to reference chains being enabled.
  2. The 5-minute burn-in also turns off the OOM ramp/emergency admission, not only the urgent latch.
  3. updateUrgentAdmission() baseline handling: it never advances, and it isn't seeded from the emergency band.

The rest are nits. I also checked removing the urgent candidate-less bypass: since pollWatchedTargets() only builds chains for selectLeakCandidates() results, I agree the old bypass couldn't produce chains on its own, so no objection there.

// table itself persists across recordings.
_gc_generations.store(args._gc_generations, std::memory_order_relaxed);
// See _leak_signal_enabled's own comment for why either source suffices.
_leak_signal_enabled.store(args._gc_generations || args._record_liveness,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_leak_signal_enabled now follows generations || liveness regardless of whether reference chains are enabled. A plain liveheap session (memory=N:l, no referencechains) therefore starts paying for:

  • per-klass population tracking in cleanup_table() (resolveKlassId() JNI round-trips + foldKlassCountsLocked() under _table_lock),
  • a heap-floor sample on every GC in onGC() (resolvePostGcHeapUsage() plus an OS::getContainerMemoryUsage() cgroup read when the container limit is resolved),
  • maybeForceCleanup() forced sweeps every 30s.

Before this PR those were gated on _gc_generations, so liveheap-only users didn't pay. The only consumers are ReferenceChainTracker and the test seams, so this looks like it should also require reference chains to be enabled (e.g. fold args._reference_chains into the flag, or have the tracker set it when it starts).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, and embarrassing that three review rounds did not surface it. Fixed in d8e594993: the flag is now generations || (record_liveness && reference_chains), so plain memory=N:l keeps its old cheap profile — track() still feeds the liveness events, but the klass folding, heap-floor ring and forced sweeps stay off. The truth-table tests now pin exactly this: liveheap-only disarms the gate and its consumers, liveheap + referencechains=true arms it.

// modular; production stamps are always <= now.
u64 activated_ns = _leak_signal_activated_ns.load(std::memory_order_acquire);
u64 since_ns = OS::nanotime() - activated_ns;
if (activated_ns == 0 || since_ns < OOM_PROJECTION_BURN_IN_NS) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question on the burn-in tradeoff: since _leak_signal_activated_ns is stamped on first activation, for the first 5 minutes secondsToOOM() returns -1. That disables everything keyed on it, not just the urgent latch: the threadLoop() cadence/pause-target ramp, the budget boost, updateUrgentAdmission() (including the emergency band, whose comment says it admits "without waiting"), and the no-progress-abandonment suppression.

So attaching to a JVM that is already leaking, or a process that OOMs 3–4 minutes after start, gets none of the end-of-life capture. I get why warmup growth needs suppressing for the urgent latch. Could the burn-in apply only to isUrgent() and leave the ramp running? Alternatively, it could be released early once the projection is backed by a real candidate.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Burn-in stays as is. The two exposure cases do not justify a change: we do not attach mid-life today, and if we add it, a 5-minute blind window on an already-leaking heap is an acceptable tradeoff. Early release on candidates fails either way — a warming cache produces the same monotone per-klass survivor growth a leak does, so it re-opens warmup-driven searching. A process OOMing within ~4 minutes of start is not salvageable by widening the search window; that case belongs to a different mechanism — heap dump on OOME plus post-hoc extraction of the interesting parts — which is separate work.

// that means "always true" here.
bool ReferenceChainTracker::updateUrgentAdmission(double seconds_to_oom, bool urgent) {
if (!urgent) {
// Left set after an episode so the next episode re-snapshots against that

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment contradicts the next line: it says the baseline is "Left set after an episode", but the code clears it. I think it should say something like "Cleared when the episode ends so the next episode re-snapshots against its own entry state."

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that comment says the opposite of what the code does. Rewrote it to what's actually happening: cleared when the episode ends so the next episode re-snapshots against its own entry state (d8e594993).

// last chapter can before the process dies, without waiting for growth
// evidence. OOM_URGENT_THRESHOLD_S is isUrgent()'s own latch threshold, so
// the emergency band and the hysteretic urgent episode cover the same range.
if (seconds_to_oom < OOM_URGENT_THRESHOLD_S) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The emergency-band early return never touches _admission_growth_baseline. If an episode's first projection is already < 300s (likely, since the burn-in can hide the projection until it is already short), baseline is still 0 here. The first time the projection rises above 300s (e.g. 295s → 305s while still in the 1800s ramp band), the next branch treats it as a "first observation": it snapshots and returns false. 100% admission then switches off until a newer GC sample exceeds that snapshot, even though the heap is still growing. Consider seeding the baseline here too (when it's 0 and a sample exists).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice trace. Fixed — the emergency band now seeds the baseline when it's still 0 and a sample exists, so the ramp-band transition compares against the entry level instead of re-snapshotting (d8e594993). Covered by a new test (EmergencyBandEntrySeedsGrowthBaseline) in both directions: growing past the seed admits, flat doesn't.

_admission_growth_baseline = newest_used;
return false;
}
return newest_used > _admission_growth_baseline;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The baseline is captured once per episode and never moves, so this compares against the episode's entry level, not against recent growth. Example: baseline 1.0 GB at band entry, a spike to 1.3 GB, then the heap settles flat at 1.1 GB. The projection window still contains the earlier rise, so it can stay inside the 1800s band for several GCs, and 1.1 GB > 1.0 GB keeps setUrgentTracking(true) the whole time. The comment above says admission happens "never for a heap that has stopped growing", but here it does. Comparing against the previous sample (or advancing the baseline to each newly observed sample) would match the stated intent.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also right. The baseline is now a high-water mark — every admitted sample raises it. So the 1.0 → 1.3 → 1.1 example drops the boost at the first settled sample and only re-admits when growth passes the previous peak. New test PlateauAboveEntryBaselineLosesBoost (d8e594993).

// the no-op cheap. Read-only; this accessor never toggles the flag.
// Read-only observation point for the explicit generations option (see
// _gc_generations's own comment); no production code keys on it.
bool gcGenerationsEnabled() const {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: after this PR, gcGenerationsEnabled() (and effectively _gc_generations, apart from a TEST_LOG) is no longer read by production code. Keeping a public accessor for the wrong gate invites future code to key on it, which is the bug this PR fixes. Consider removing it and having the tests assert on leakSignalEnabled(), or moving it under DEBUG || UNIT_TEST.

A few comments in the PR also describe history rather than current behaviour; these go stale:

  • hasLeakSignal(): "…when this branch used to return true"
  • setGcGenerationsForTest: "Kept for existing callers…"
  • AggressiveLeakReferenceChainTest javadoc: "That bypass and its one-shot urgency entitlement were removed…"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with guarding it under DEBUG || UNIT_TEST instead of removing — the truth tables assert both gates independently, and I'd rather keep that pinned. History comments fixed too (d8e594993).

)
}

// Test .inc/.h files resolve via quoted includes relative to the test

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (AGENTS.md: "If modifying a file that does not contain Datadog copyright, add one"): this file has no Datadog copyright header. The same applies to ddprof-lib/src/test/cpp/referenceChainsOomTests.inc and referenceChainsLifecycleTests.inc, which this PR also modifies.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added, same for the two .inc files (d8e594993).

- Derive the leak signal from generations || (liveheap && referencechains):
  the collector is the signal's only consumer, so a plain liveheap session
  keeps its cheaper profile (no per-epoch klass folding, heap-floor ring
  or forced sweeps).
- Seed the urgent-admission growth baseline at emergency-band entry so the
  first ramp-band observation compares against the episode entry level
  instead of re-snapshotting and dropping the boost mid-growth.
- Advance the baseline per admitted sample (high-water mark): a plateau
  above the entry level loses the boost until growth resumes.
- Guard gcGenerationsEnabled() as test-only; rewrite comments that narrated
  history or an internal observation instead of current behavior.
- Add copyright headers to the files this PR modifies that lacked one.
- StopClearsAdmissionGrowthBaseline no longer races the BFS thread:
  the test pins bfsThreadRunning() == false before driving the
  updateUrgentAdmission accessor directly.
- threadLoop() computes secondsToOOM() once per iteration and threads
  the value through shouldRunPass/canAffordNewSearch/hasLeakSignal/
  isUrgent; one-arg wrappers read once and forward.
- New publishUrgencyState() test drives the real admission gate and
  asserts _urgent_tracking follows the gated decision.
- AggressiveLeak's candidate-free check retries once before failing,
  tolerating a transient candidate in the shared test JVM.
- setLeakSignalActivatedNsForTest0 negative-argument guard now has a
  test; the stamp is restored in finally.
- GtestTaskBuilder declares PathSensitivity.RELATIVE on the test
  include file tree as a named task input.
- Ownership comments on _admission_growth_baseline/_oom_ramp_active
  amended to name the non-BFS writers.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants