Skip to content

Reference-chain tracker and leak-signal engine - #797

Open
jbachorik wants to merge 41 commits into
mainfrom
jb/rc-3-refchain-tracker
Open

jbachorik wants to merge 41 commits into
mainfrom
jb/rc-3-refchain-tracker

Conversation

@jbachorik

@jbachorik jbachorik commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?:
Implements the reference-chain engine itself:

  • ReferenceChainTracker (referenceChains.*): per-klass class tags, the frontier table of retained references, the BFS expansion thread, chain resolution/improvement into per-sample ReferenceChainEvent payloads, leak-tag correlation, and the search-gate/pain-budget scheduling.
  • LivenessTracker (livenessTracker.*): per-klass population table, heap-floor ring with the time-to-OOM projection, and leak-candidate selection feeding the search gate.
  • Support: referencechains Arguments consumption, string-dictionary generation counter for class-tag cache invalidation, container-memory/os queries the projection needs, and the new counters/rcDebugLevel/painBudget/classTagAllocator headers.

Motivation:
Core of PROF-15341: attribute surviving live-heap samples to reference chains so leak candidates carry an actionable retention path.

Additional Notes:
Stacked on #796. Compiles and links standalone against the profiler API (only pre-existing Profiler methods are called); the lifecycle wiring lands in the next PR of the stack. The largest review chunk in the series — referenceChains.cpp is the engine, referenceChains.h documents its invariants.

How to test the change?:
buildDebug -Pskip-tests compiles and links this layer; the C++ unit tests for all of the above are the next PRs in the stack.

For Datadog employees:

  • This PR doesn't touch any of that.
  • JIRA: PROF-15341

@dd-octo-sts

dd-octo-sts Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Scan-Build Report

User:runner@runnervmlun5p
Working Directory:/home/runner/work/java-profiler/java-profiler/ddprof-lib/src/test/make
Command Line:make -j4 all
Clang Version:Ubuntu clang version 18.1.3 (1ubuntu1)
Date:Thu Sep 17 20:07:29 2026

Bug Summary

Bug TypeQuantityDisplay?
All Bugs4
C++ move semantics
Use-after-move1
Logic error
Dereference of null pointer1
Result of operation is garbage or undefined1
Unused code
Dead increment1

Reports

Bug Group Bug Type ▾ File Function/Method Line Path Length
Unused codeDead incrementreferenceChains.cppcollectStaticFieldAnchorsForRotation36771
Logic errorDereference of null pointerfaultInjection.cppcrashNow242
Logic errorResult of operation is garbage or undefinedlivenessTracker.cppsecondsToOOM138324
C++ move semanticsUse-after-movereferenceChains.cppbuildCanaryChainEvent608368

@dd-octo-sts

dd-octo-sts Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

CI Test Results

Run: #36580839436 | Commit: de1efc4 | Duration: 17m 37s (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-09-29 14:32:15 UTC

@dd-octo-sts

dd-octo-sts Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

✅ All 40 integration tests passed

📊 Dashboard · 👷 Pipeline · 📦 39a8c3c7

@datadog-prod-us1-5

This comment has been minimized.

@jbachorik
jbachorik added this pull request to stack #803 September 17, 2026 20:02
@jbachorik
jbachorik marked this pull request as ready for review September 17, 2026 20:03
@jbachorik
jbachorik requested a review from a team as a code owner September 17, 2026 20:03
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 17, 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-09-23T06:13:18.837327Z 9f141e3 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.

@jbachorik
jbachorik marked this pull request as draft September 17, 2026 20:04
@jbachorik
jbachorik force-pushed the jb/rc-3-refchain-tracker branch from f01e23f to 00c4c92 Compare September 17, 2026 20:05

@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: f01e23fc6a

ℹ️ 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".

Comment thread ddprof-lib/src/main/cpp/livenessTracker.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/livenessTracker.cpp
Comment thread ddprof-lib/src/main/cpp/referenceChains.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/referenceChains.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/livenessTracker.cpp Outdated

@datadog-prod-us1-5 datadog-prod-us1-5 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.

Datadog Autotest: FAIL

A second recording can give one leak tag to two live entries. Later cleanup can write past the free-tag array and corrupt native memory.

Open Bits AI session

🤖 Datadog Autotest · Commit f01e23f · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

Comment thread ddprof-lib/src/main/cpp/referenceChains.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/referenceChains.cpp
Comment thread ddprof-lib/src/main/cpp/livenessTracker.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/referenceChains.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/referenceChains.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/referenceChains.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/referenceChains.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/livenessTracker.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/os_linux.cpp
Comment thread ddprof-lib/src/main/cpp/livenessTracker.cpp Outdated
@jbachorik
jbachorik force-pushed the jb/rc-3-refchain-tracker branch 2 times, most recently from 00c4c92 to 4804259 Compare September 17, 2026 20:19
@jbachorik
jbachorik force-pushed the jb/rc-3-refchain-tracker branch 2 times, most recently from a5c60d0 to cfa59db Compare September 17, 2026 21:01
@jbachorik
jbachorik force-pushed the jb/rc-3-refchain-tracker branch from c71e5ab to 5fc0326 Compare September 17, 2026 22:15
@jbachorik
jbachorik force-pushed the jb/rc-3-refchain-tracker branch 2 times, most recently from b2dafa0 to 1752c4f Compare September 18, 2026 08:04
@jbachorik
jbachorik force-pushed the jb/rc-3-refchain-tracker branch from 959a2ef to 6dd03d9 Compare September 18, 2026 12:59
The tag guard accepted tag == 2^31 (idx == INT_MAX), letting
growLocked(idx + 1) overflow to INT_MIN, bypass the capacity check and
write far out of bounds. Reject tag - 1 >= INT_MAX everywhere the guard
appears.

FrontierTable::insert() now advances _table_size under the same
exclusive lock as the slot write, so a lock-ordered reader can never
pass its idx < _table_size check on an unwritten, zeroed slot.
The leak-tag free list and info entries are now guarded by a dedicated
SpinLock (lock order: _table_lock before _leak_tag_pool_lock, never the
reverse) - acquireLeakTag() runs under the SHARED table lock while
releaseLeakTag() runs under the EXCLUSIVE one, and getLeakTagInfo()
takes no table lock at all, so the pool must not depend on either.

cleanup_table() resolves at most 256 surviving entries' classes per
sweep; entries past the budget fall back to their cached class id (the
semantics the allow_resolve=false path already uses), bounding the
exclusive-lock window instead of scaling it with survivor count.

Document the enforced LT -> RCT lock order at the
correlateAdmittedLeakTag() call site and the single-writer invariant
for _table[i].leak_tag; fix the RingThirdsStats / corroborateRecentHalf
comments to describe the regression actually computed; cache the
heapFloorRising() result instead of re-evaluating it in the trailing
TEST_LOG; make ClassTagAllocator::resetForTest() an atomic exchange.
buildCanaryChainEvent()'s parent_tag walk now bounds at the frontier's
maxCapacity() like reconstructChain() and returns false on a cyclic or
corrupt chain instead of spinning the poll thread; the walk's
event._root_kind now comes from the terminal root-attached entry rather
than the parent-side interior entry (always 0), matching the
appendStaticFieldRootType(terminal) call and suppressChainEvent()'s
transient-root gate.

Remove the retired canary marker-tag mechanism end to end: nothing
pre-tags candidates anymore, so the decode branches in
heapReferenceCallback() and pollWatchedTargets() were unreachable - and
runPass()'s marker-release loop was worse than dead: with every
_candidate_tags[i] left at 0 it asked GetObjectsWithTags to enumerate
ALL untagged objects once per candidate slot on every search stop.

Also: read the container usage file from the hierarchy that supplied
the winning limit instead of probing v2/v1 filenames in order; clamp
PainBudget::drain() against a backward clock step; refuse symlinked or
non-owned rc-debug override files under /tmp; resolve each container
interface tag independently instead of failing the whole call; reset
consume_tier_fair()'s cursor on a completed-but-unproductive lap; drop
the pod fixture's dead placeholder assignment and assert the
representative carries its leak tag. Tests cover the bounded walk, the
deep-chain reconstruction, the symlink rejection and the cycle
fail-safe.
Test .cpp files #include sibling .inc files; NativeCompileTask hashes
inputs by content, so an .inc-only edit left the test binary stale and
the change silently never ran.
A renamer thread cycles the knob path through valid/garbage/symlink/
directory/missing states via rename() while three readers hammer
readRcDebugLevelFile(): outcomes stay in {-1,0,1,2}, both a valid level
and a rejection are observed, and the open-descriptor count returns to
baseline (all early-return paths close the fd). The foreign-owner fstat
branch needs privileges to stage and stays covered statically.
Op-sequence libFuzzer target driving every public FrontierTable method
directly, including paths a well-formed BFS pass never produces: invalid
tags, capacity-schedule exhaustion, cycling/dangling parent chains, and
predicate-violating re-parent attempts. Asserts the capacity contract,
lookup fidelity (including the stale-bytes window after resetForRestart),
reconstructChain soundness, and the improveChain/reparentToDurableRoot
mutation guards, all against a model replica of the documented behavior.
A pass thread drives runPass()/restartSearch() the way the BFS threadLoop
does, while GC callbacks run on their own thread and a third thread holds
stopThread-style abort requests in 1ms windows. Asserts restarted searches
start from zeroed accounting (passes, leak-tag counters, frontier size),
the recording boundary clears resolved chains, GC epochs stay monotone,
and the DEBUG t_inGCCallback asserts survive concurrent walks.
Drive isUrgent()'s latch/release machine with synthetic heap-floor ramps
(ring seeding via the existing LivenessTracker test hooks): mid-band
oscillation around the 300s threshold never flaps, release requires five
consecutive clear-or-unknown observations, and a re-latch after release
restores the urgency search entitlement. Assert shouldRunPass() raises the
CPU pain-budget refill 100x exactly while a canary chase is open, and that
the backoff gate holds passes unless the OOM ramp is active.
start() clears the resolved-chain cache and the pending abandoned-event
queue so a new recording never re-emits the previous one's chains with
class-dictionary ids from a wiped generation - seed both states and assert
the real stop()/start() path drops them while the resolve path still works
in the new recording.
Each consecutive CANARY_STUCK restart doubles the detector's pass limit
(30 -> 60 -> 120, capped), and any other terminal outcome resets the
escalation. Reachable only while urgent - the ordinary whole-graph stall
branch has the same frontier-stall precondition and is checked first - so
the test seeds a 224s-to-OOM projection the same way the OOM tests do.
Adds abandon-reason, pending-abandoned-count, and stuck-limit test
accessors.
The urgency latch survives the test (it releases only after five clear
readings that never come) and keeps bypassing candidate gates for any test
that runs after it under shuffled order; restartSearch() also leaves the
seeded watched-leak-klass list in place. Reset the latch, watched list,
and search state at the end. Also raise the compiler-availability probe
timeout from 5s to 30s: on a loaded macOS host the /usr/bin/clang++ Xcode
shim can exceed 5s to answer --version and the probe failed intermittently
from invocations that accepted the same path minutes earlier.
fuzz_infra only runs on scheduled/manual pipelines and is allow_failure, so
a fuzz target that stopped compiling survived an entire PR unnoticed - PR
797 broke fuzz_callTraceStorage that way, caught only by a manual run. Run
:ddprof-lib:fuzz:buildFuzz on every branch push (failing if no binaries are
produced, so a failed libFuzzer probe cannot silently void the gate), and
soak the concurrency-dependent chaos suites x50 under TSan in the same job.
The fuzz binaries link to bin/fuzz/<name>/<name> - one directory deeper than
the gate's find maxdepth allowed, so the gate failed after a fully
successful buildFuzz (11 binaries linked). Mirror the Dockerfile.fuzz
lookup (-maxdepth 2) that the fuzz_infra image build has always used.
The abort thread's fixed usleep holds made the flag/pass overlap a race: a
fast runner pushes the pass thread through all 40 searches before the
abort thread's first store lands, every pass's post-pass read sees the
flag clear, and the abort-was-exercised guard fails with aborted_passes=0
(seen on the arm64 release CI runner; the walk itself was never at fault).
Hold each abort window until the guard's own counter moves (bounded spin,
early exit when the pass thread is done), and open the first window before
the pass thread starts, so at least one post-pass read observes the
request by construction.
@jbachorik
jbachorik force-pushed the jb/rc-3-refchain-tracker branch from d51ab55 to 39a8c3c Compare September 29, 2026 14:10
@dd-octo-sts

dd-octo-sts Bot commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Reliability & Chaos Results

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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants