Skip to content

Add deadlock event with JVMTI-captured stacktraces - #470

Draft
jbachorik wants to merge 4 commits into
mainfrom
jb/deadlock-stacktraces
Draft

Add deadlock event with JVMTI-captured stacktraces#470
jbachorik wants to merge 4 commits into
mainfrom
jb/deadlock-stacktraces

Conversation

@jbachorik

@jbachorik jbachorik commented Apr 14, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?:

Adds a native JFR deadlock event (datadog.DeadlockedThread) that captures real
JVMTI stacktraces for both the deadlocked thread and the lock owner. This provides
proper navigable stacktraces in JFR tools — a significant improvement over the
string-based stacks in the dd-trace-java fallback implementation.

Key components:

  • New T_DEADLOCK JFR event type with fields: deadlockId, eventThread, stackTrace,
    lockName, lockOwnerThread, lockOwnerStackTrace
  • JVMTI GetStackTrace() capture for both threads in the deadlock pair, stored in
    the profiler's CallTraceStorage as real stacktrace constant pool references
  • DeadlockDetector Java class using ThreadMXBean.findDeadlockedThreads() with
    periodic polling on a daemon thread
  • JavaProfiler.startDeadlockDetector() / stopDeadlockDetector() lifecycle API

Motivation:

PROF-11534 — The existing deadlock event in dd-trace-java uses JDK JFR with string
stacktraces. This PR adds the improved native implementation in java-profiler that
captures real stacktraces via JVMTI, which dd-trace-java can use when the profiler
is loaded.

Additional Notes:

  • dd-trace-java keeps its existing DeadlockEventFactory as a fallback for
    environments where the profiler isn't loaded
  • Works on JDK 8+ (all APIs are JDK 1.5+; JVMTI is always available)
  • One event per deadlocked thread; events in the same cycle share a deadlockId
  • Lock name truncated to 160 chars to fit within single-byte JFR event size prefix
  • Supports both monitor and java.util.concurrent lock deadlocks (falls back to
    findMonitorDeadlockedThreads() when synchronizer usage is not supported)

How to test the change?:

./gradlew ddprof-test:testRelease --tests '*DeadlockDetectionTest*'

The test creates a real deadlock between two threads, starts the detector with a
100ms interval, and verifies that datadog.DeadlockedThread events appear in the
profiler's JFR output with non-empty stacktraces and matching deadlock IDs.

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 review from @DataDog/security-design-and-guidance.
  • This PR doesn't touch any of that.
  • JIRA: PROF-11534

🤖 Generated with Claude Code via muse implement

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jbachorik jbachorik added the AI label Apr 14, 2026
Comment thread ddprof-lib/src/main/cpp/flightRecorder.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/profiler.cpp
- Simplify lock_name truncation with inline min()
- Add JVMTI safepoint warning on captureStackTrace
- Move DeadlockDetector to test-only; profiler exposes only recordDeadlock API
- Remove startDeadlockDetector/stopDeadlockDetector from JavaProfiler
- Use before()/after() hooks in test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jbachorik
jbachorik marked this pull request as ready for review April 14, 2026 15:28
@jbachorik
jbachorik requested a review from a team as a code owner April 14, 2026 15:28
@dd-octo-sts

dd-octo-sts Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

CI Test Results

Run: #24452346678 | Commit: 3b3f1e0 | Duration: 23m 24s (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-04-15 12:05:20 UTC

@jbachorik
jbachorik marked this pull request as draft April 14, 2026 16:52
jbachorik and others added 2 commits April 15, 2026 13:01
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jbachorik
jbachorik requested a review from Copilot May 20, 2026 09:40
@jbachorik

Copy link
Copy Markdown
Collaborator Author

@codex review

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

ℹ️ 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/profiler.cpp

Copilot AI 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.

Pull request overview

Adds a new native JFR event (datadog.DeadlockedThread) intended to record detected deadlocks with JVMTI-captured stack traces, plus a test that creates a real deadlock and validates events are emitted.

Changes:

  • Introduces a new JFR event type (T_DEADLOCK) and metadata schema for datadog.DeadlockedThread.
  • Adds a JNI entrypoint and native implementation to capture JVMTI stack traces for the deadlocked thread and the lock owner, storing them in CallTraceStorage.
  • Adds a test-only deadlock detector and a JUnit test that triggers a deadlock and verifies the event appears in the produced JFR.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
ddprof-test/src/test/java/com/datadoghq/profiler/deadlock/DeadlockDetector.java Test-only polling detector that finds deadlocks via ThreadMXBean and calls JavaProfiler.recordDeadlock.
ddprof-test/src/test/java/com/datadoghq/profiler/deadlock/DeadlockDetectionTest.java Creates a real 2-thread monitor deadlock and asserts datadog.DeadlockedThread events exist with stack traces and consistent deadlock IDs.
ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java Adds public recordDeadlock(...) API and declares native recordDeadlock0(...).
ddprof-lib/src/main/cpp/profiler.h Declares Profiler::recordDeadlock(...) and Profiler::recordDeadlockWithCapture(...).
ddprof-lib/src/main/cpp/profiler.cpp Implements JVMTI GetStackTrace capture and deadlock recording plumbing into the flight recorder.
ddprof-lib/src/main/cpp/jfrMetadata.h Adds T_DEADLOCK enum constant.
ddprof-lib/src/main/cpp/jfrMetadata.cpp Registers datadog.DeadlockedThread event metadata and fields.
ddprof-lib/src/main/cpp/javaApi.cpp Adds JNI bridge JavaProfiler_recordDeadlock0 to invoke native capture+record.
ddprof-lib/src/main/cpp/flightRecorder.h Declares deadlock recording methods in Recording/FlightRecorder.
ddprof-lib/src/main/cpp/flightRecorder.cpp Serializes the deadlock event payload and ensures involved threads are added to the recording.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1655 to +1657
// Truncate to fit within the single-byte event size prefix (max 255 bytes total)
buf->putUtf8(lock_name, min((size_t)160, strlen(lock_name)));
buf->putVar64(lock_owner_tid);
Comment on lines +78 to +82
bothLocked.await();
Thread.sleep(200); // Let the deadlock form

new DeadlockDetector(profiler).check();
}
Comment on lines +108 to +130
IMemberAccessor<String, IItem> lockNameAccessor =
LOCK_NAME.getAccessor(items.getType());
IMemberAccessor<IMCStackTrace, IItem> stackTraceAccessor =
STACK_TRACE.getAccessor(items.getType());

for (IItem item : items) {
if (deadlockIdAccessor != null) {
IQuantity id = deadlockIdAccessor.getMember(item);
if (id != null) {
deadlockIds.add(id.longValue());
}
}
if (lockNameAccessor != null) {
String lockName = lockNameAccessor.getMember(item);
assertNotNull(lockName, "Lock name should not be null");
assertFalse(lockName.isEmpty(), "Lock name should not be empty");
}
if (stackTraceAccessor != null) {
IMCStackTrace st = stackTraceAccessor.getMember(item);
if (st != null && st.getFrames() != null && !st.getFrames().isEmpty()) {
hasStackTraces = true;
}
}
Comment on lines 228 to +239
/**
* Records a deadlock event for a pair of deadlocked threads.
* The native side captures real stacktraces via JVMTI for both threads.
*
* @param thread the deadlocked thread
* @param lockName the lock this thread is waiting on
* @param lockOwnerThread the thread holding the lock
* @param deadlockId correlation ID grouping events in the same deadlock cycle
*/
public void recordDeadlock(Thread thread, String lockName, Thread lockOwnerThread, long deadlockId) {
recordDeadlock0(thread, lockName, lockOwnerThread, deadlockId);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2 participants