Skip to content

Restore priming - #713

Draft
zhengyu123 wants to merge 15 commits into
mainfrom
zgu/thread_priming
Draft

Restore priming#713
zhengyu123 wants to merge 15 commits into
mainfrom
zgu/thread_priming

Conversation

@zhengyu123

Copy link
Copy Markdown
Contributor

What does this PR do?:

Motivation:

Additional Notes:

How to test the change?:

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: [JIRA-XXXX]

Unsure? Have a question? Request a review!

Copilot AI review requested due to automatic review settings August 3, 2026 19:56
@dd-octo-sts

dd-octo-sts Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Scan-Build Report

User:runner@runnervmvrwv9
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:Tue Aug 4 01:40:21 2026

Bug Summary

Bug TypeQuantityDisplay?
All Bugs1
Logic error
Dereference of null pointer1

Reports

Bug Group Bug Type ▾ File Function/Method Line Path Length
Logic errorDereference of null pointerprofiler.hfindLibraryByAddress51714

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

This PR reintroduces “priming” for ProfiledThread access in async/signal-handling stack-walk paths by adding a preallocated ThreadLocalDataPool and switching key sampling sites to acquire and cache a ProfiledThread without per-signal allocation.

Changes:

  • Added ThreadLocalDataPool plus ProfiledThread::acquire_current() to acquire/carry a reusable ProfiledThread in signal context.
  • Updated stack walking and sampling code paths (StackWalker/HotSpot) to use acquire_current() and track drops when TLS cannot be acquired.
  • Rewired a number of translation units to include threadLocalData.inline.h (moving the inline TLS accessors out of threadLocalData.h).

Reviewed changes

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

Show a summary per file
File Description
ddprof-lib/src/main/cpp/wallClock.h Switch to threadLocalData.inline.h include for inlined TLS access.
ddprof-lib/src/main/cpp/threadLocalDataPool.h New pool API for reusing ProfiledThread instances.
ddprof-lib/src/main/cpp/threadLocalDataPool.cpp New pool implementation: allocate/claim/unclaim pooled ProfiledThread slots.
ddprof-lib/src/main/cpp/threadLocalData.inline.h New header providing inline definitions of ProfiledThread::current() and acquire_current().
ddprof-lib/src/main/cpp/threadLocalData.h Adds claimed flag/state and declares new inline TLS accessors.
ddprof-lib/src/main/cpp/threadLocalData.cpp Routes TLS destructor cleanup through the pool when applicable.
ddprof-lib/src/main/cpp/stackWalker.cpp Uses acquire_current() and increments drop counter when TLS cannot be acquired.
ddprof-lib/src/main/cpp/refCountGuard.cpp Switch include to threadLocalData.inline.h.
ddprof-lib/src/main/cpp/perfEvents_linux.cpp Switch include to threadLocalData.inline.h.
ddprof-lib/src/main/cpp/jvmThread.h Adds supportPriming() decision helper (musl vs glibc TLS key range).
ddprof-lib/src/main/cpp/jvmSupport.cpp Initializes ThreadLocalDataPool when priming is supported.
ddprof-lib/src/main/cpp/javaApi.cpp Switch include to threadLocalData.inline.h.
ddprof-lib/src/main/cpp/itimer.cpp Switch include to threadLocalData.inline.h.
ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp Switch include to threadLocalData.inline.h.
ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp Uses acquire_current() in HotSpot stack-walk paths.
ddprof-lib/src/main/cpp/guards.cpp Switch include to threadLocalData.inline.h.
ddprof-lib/src/main/cpp/flightRecorder.cpp Switch include to threadLocalData.inline.h.
ddprof-lib/src/main/cpp/ctimer_linux.cpp Switch include to threadLocalData.inline.h.
ddprof-lib/src/main/cpp/context_api.cpp Switch include to threadLocalData.inline.h.
Suppressed comments (1)

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:64

  • initialize() publishes the pool pointer unconditionally; if construction failed (e.g., _threads == nullptr), subsequent acquire()/release() calls can hit UB. Only publish the pool if it is usable; otherwise keep _pool null.
void ThreadLocalDataPool::initialize() {
    ThreadLocalDataPool* pool = new ThreadLocalDataPool();
    __atomic_store_n(&_pool, pool, __ATOMIC_RELEASE);
}

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

Comment thread ddprof-lib/src/main/cpp/threadLocalDataPool.h Outdated
Comment thread ddprof-lib/src/main/cpp/threadLocalDataPool.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/threadLocalDataPool.cpp
Comment thread ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 3, 2026 20:10

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

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.

Suppressed comments (7)

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:18

  • ThreadLocalDataPool doesn’t initialize _threads when malloc fails, leaving it indeterminate. That can lead to invalid free() in the destructor and crashes in claim()/contains(). Initialize _threads to nullptr in the ctor initializer list.
ThreadLocalDataPool::ThreadLocalDataPool(uint64_t capacity) : _capacity(capacity), _used(0) {
    size_t malloc_size = capacity * sizeof(ProfiledThread);
    void* p = malloc(malloc_size);
    if (p != nullptr) {
      _threads = reinterpret_cast<ProfiledThread*>(p);

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:48

  • On probe failure, claim() currently assert(false) after scanning the whole pool. Under races (or if _threads is unexpectedly null), this can abort the process from a signal handler. Prefer to roll back _used and return nullptr (dropping the sample) instead of asserting.
    do {
        if (_threads[index].claim_acquire(tid)) {
            return &_threads[index];
        }
        index = (index + 1) % _capacity;
    } while (index != start_pos);
    assert(false && "Should not reach here");
    return nullptr;

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:56

  • unclaim() has the same unsigned-decrement issue as claim() (using __atomic_fetch_add(..., -1, ...) on a uint16_t). Use __atomic_fetch_sub(..., 1, ...) so _used doesn’t wrap.
        new (t)ProfiledThread(0);
        uint16_t used = __atomic_fetch_add(&_used, -1, __ATOMIC_RELEASE);
        assert(used > 0);
        return true;

ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp:1226

  • The previous code restored ThreadLocalData::_unwinding_Java after a recovered siglongjmp because siglongjmp bypasses AsyncSampleMutex destructors. That restore was removed, so a crash recovery can leave _unwinding_Java stuck true, preventing future Java stack walks on the thread.
  if (sigsetjmp(crash_protection_ctx, 1) != 0) {
    // checkFault() does a siglongjmp from inside segvHandler, bypassing
    // segvHandler's SignalHandlerScope destructor. Compensate.
    SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP();
    prof_thread->setJmpCtx(prev_jmp_buf);

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:55

  • unclaim() reconstructs ProfiledThread via placement-new while other threads may concurrently probe/claim the same slot. Because ProfiledThread uses atomic operations on _misc_flags, reinitializing it with non-atomic stores (constructor/placement-new) can race with those atomics (UB) and also makes it possible to observe a partially-reset object. Consider adding an explicit “reset for pool reuse” routine that keeps the slot in a claimed state while resetting fields, then clears FLAG_CLAIMED with a release store as the final step.
bool ThreadLocalDataPool::unclaim(ProfiledThread* t) {
    if (contains(t)) {
        new (t)ProfiledThread(0);
        uint16_t used = __atomic_fetch_add(&_used, -1, __ATOMIC_RELEASE);
        assert(used > 0);

ddprof-lib/src/main/cpp/threadLocalData.inline.h:18

  • ProfiledThread::current() is defined here, but if it’s also defined inline in threadLocalData.h (to keep existing include sites compiling), this becomes a duplicate definition across TUs. Keep only one definition (e.g., define current() in threadLocalData.h and leave only acquire_current() here).
ProfiledThread* ProfiledThread::current() {
    if (!isThreadKeyValid()) {
      return nullptr;
    }
    return _current_thread.get();
}

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:37

  • __atomic_fetch_add(&_used, -1, ...) is performed on a uint16_t. The -1 is converted to uint16_t (65535), so this increments by 65535 (wraps) rather than decrementing. Use __atomic_fetch_sub(..., 1, ...) (and similarly in unclaim).

This issue also appears on line 53 of the same file.

    uint16_t used = __atomic_fetch_add(&_used, 1, __ATOMIC_RELAXED);
    if (used >= _capacity) {
        __atomic_fetch_add(&_used, -1, __ATOMIC_RELAXED);
        return nullptr;

Comment thread ddprof-lib/src/main/cpp/threadLocalData.h
Comment thread ddprof-lib/src/main/cpp/threadLocalDataPool.cpp
Comment thread ddprof-lib/src/main/cpp/threadLocalDataPool.cpp
@dd-octo-sts

dd-octo-sts Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

CI Test Results

Run: #30897840382 | Commit: 783e4ab | Duration: 21m 28s (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-08-04 10:11:40 UTC

@dd-octo-sts

dd-octo-sts Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (commit 856ee13)

Pipeline: https://gitlab.ddbuild.io/DataDog/apm-reliability/benchmarking-platform/-/pipelines/128699821 Commit: 856ee133a68fe770ffcfa01dd89dffccb4305ee2

⚠️ Significant outliers

  • 🟢 fj-kmeans (JDK 21): runtime -4.5% (2778→2653 ms)
Runtime details (per benchmark × JDK)
Benchmark JDK Latest Dev Δ (dev vs latest) Issues L/D
akka-uct 21 ✅ 10326 ms (21 iters) ✅ 10362 ms (21 iters) ≈ +0.3% (±11.4%) — / —
finagle-chirper 21 ✅ 5952 ms (33 iters) ✅ 5955 ms (33 iters) ≈ +0.1% (±25.5%) ⚠️ W:3 / ⚠️ W:3
finagle-chirper 25 ✅ 5484 ms (36 iters) ✅ 5471 ms (36 iters) ≈ -0.2% (±24.1%) ⚠️ W:3 / ⚠️ W:3
fj-kmeans 21 ✅ 2778 ms (67 iters) ✅ 2653 ms (71 iters) 🟢 -4.5% — / —
fj-kmeans 25 ✅ 2764 ms (68 iters) ✅ 2759 ms (68 iters) ≈ -0.2% (±2.8%) — / —
future-genetic 21 ✅ 2060 ms (90 iters) ✅ 2114 ms (87 iters) ≈ +2.6% (±2.7%) — / —
future-genetic 25 ✅ 2053 ms (90 iters) ✅ 2009 ms (93 iters) ≈ -2.1% (±2.5%) — / —
naive-bayes 21 ✅ 1268 ms (135 iters) ✅ 1298 ms (132 iters) ≈ +2.4% (±33.2%) — / —
reactors 21 ✅ 16232 ms (15 iters) ✅ 16628 ms (16 iters) ≈ +2.4% (±8.8%) — / —
Internal counter details (ddprof)

ddprof internal counters, latest / dev (✅ = 0, · = unavailable):

Benchmark JDK Dropped rec Dropped jvmti Dropped trace Skipped WC AGCT fail Unwind fail
akka-uct 21 ✅ / ✅ ✅ / ✅ 5 / 3 2054 / 1956 ✅ / ✅ ✅ / ✅
finagle-chirper 21 ✅ / ✅ ✅ / ✅ 2 / 2 8800 / 8383 ✅ / ✅ ✅ / ✅
finagle-chirper 25 ✅ / ✅ ✅ / ✅ 1 / 1 8585 / 8202 ✅ / ✅ ✅ / ✅
fj-kmeans 21 ✅ / ✅ ✅ / ✅ ✅ / ✅ ✅ / ✅ ✅ / ✅ ✅ / ✅
fj-kmeans 25 ✅ / ✅ ✅ / ✅ ✅ / ✅ 1279 / 1277 ✅ / ✅ ✅ / ✅
future-genetic 21 ✅ / ✅ ✅ / ✅ 1 / 4 2914 / 2956 ✅ / ✅ ✅ / ✅
future-genetic 25 ✅ / ✅ ✅ / ✅ 1 / ✅ 2797 / 2885 ✅ / ✅ ✅ / ✅
naive-bayes 21 ✅ / ✅ ✅ / ✅ 7 / 2 3545 / 3557 ✅ / ✅ ✅ / ✅
reactors 21 ✅ / ✅ ✅ / ✅ 1 / ✅ 1581 / 1852 ✅ / ✅ ✅ / ✅

Copilot AI review requested due to automatic review settings August 3, 2026 20:57
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@datadog-datadog-us1-prod

This comment has been minimized.

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

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.

Suppressed comments (3)

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:58

  • unclaim() reconstructs the slot with placement-new (new (t) ProfiledThread(0)), which writes _misc_flags (and other fields) non-atomically while other threads may concurrently read _misc_flags via __atomic_* in claim_acquire(). This mixes atomic and non-atomic accesses to the same object and can also momentarily clear FLAG_CLAIMED before the slot reset is fully complete.
    return nullptr;
}

bool ThreadLocalDataPool::unclaim(ProfiledThread* t) {
    if (contains(t)) {

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:15

  • If malloc() fails in ThreadLocalDataPool's constructor, _threads is left uninitialized, but later code (including the destructor and contains()) assumes it is either a valid pointer or nullptr. This can lead to undefined behavior.
ThreadLocalDataPool::ThreadLocalDataPool(uint64_t capacity)

ddprof-lib/src/main/cpp/threadLocalData.h:125

  • The assertion message in ProfiledThread::unclaim() is inverted: if the assert fires, the slot was not claimed, but the message says it "has been claimed".
    assert(isClaimed() && "Slot has been claimed");

Comment thread ddprof-lib/src/main/cpp/threadLocalDataPool.cpp
Comment thread ddprof-lib/src/main/cpp/jvmThread.h Outdated
Copilot AI review requested due to automatic review settings August 3, 2026 21:03
@datadog-datadog-us1-prod

datadog-datadog-us1-prod Bot commented Aug 3, 2026

Copy link
Copy Markdown

Bits has a CI fix ready

🟢 Investigated · 🟢 Fix prepared · ⚪ Validation skipped · 🟠 Ready

threadLocalDataPool.cpp had a missing used >= _capacity condition in ThreadLocalDataPool::claim, leaving the full-pool cleanup statements outside a block and causing native compilation errors. Restored the guard so full pools decrement their reservation and return safely.

Commit fix to this PR


View in Datadog | Reviewed commit f4e9eec · Any feedback? Reach out in #deveng-pr-agent

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

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:43

  • ThreadLocalDataPool::claim() currently has a broken/missing capacity guard: the code unconditionally decrements _used and returns nullptr, and the braces are unbalanced, so this won’t compile and the pool can never hand out slots. Add the intended used >= _capacity check and close the block correctly.
    uint16_t used = __atomic_fetch_add(&_used, 1, __ATOMIC_RELAXED);
        __atomic_fetch_add(&_used, -1, __ATOMIC_RELAXED);
        return nullptr;
    }

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:60

  • unclaim() reconstructs the slot with placement-new (ProfiledThread(0)), which clears FLAG_CLAIMED via a non-atomic write to _misc_flags. That allows another thread to observe the slot as unclaimed and race in while the object is mid-reset. Prefer clearing the claimed bit atomically (the class already provides ProfiledThread::unclaim() for this) and let the next acquire() reinitialize the object.
    if (contains(t)) {
        new (t)ProfiledThread(0);
        uint16_t used = __atomic_fetch_add(&_used, -1, __ATOMIC_RELEASE);

ddprof-lib/src/main/cpp/threadLocalData.inline.h:20

  • ProfiledThread::acquire_current() is defined in a header included by multiple translation units but is not marked inline, which can produce multiple-definition linker errors. Mark it inline.
ProfiledThread* ProfiledThread::acquire_current() {

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:12

  • This file uses placement-new (new (&_threads[index]) ...) but does not include <new>, which is required to declare placement-new in standard C++. Add the missing include to avoid build failures on stricter toolchains.
#include <cassert>
#include <stdlib.h>

Comment thread ddprof-lib/src/main/cpp/threadLocalData.inline.h Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 3, 2026 21:17

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

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.

Suppressed comments (8)

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:62

  • _used was widened in the header, but this local variable is still uint16_t, which will truncate the atomic counter and can underflow/wrap incorrectly.
        uint16_t used = __atomic_fetch_add(&_used, -1, __ATOMIC_RELEASE);

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:63

  • unclaim() reconstructs the ProfiledThread in-place with placement-new while other threads may concurrently read/modify the slot (via claim_acquire() / _misc_flags). Re-ending/restarting an object’s lifetime and doing non-atomic writes to the same storage other threads touch is undefined behavior and can lead to double-claim or corrupted state. Consider keeping slot ownership state separate from the ProfiledThread object (e.g., a dedicated std::atomic<uint32_t> claim word per slot) and avoid placement-new on shared objects; reset per-thread state only after exclusive ownership is established.
bool ThreadLocalDataPool::unclaim(ProfiledThread* t) {
    if (contains(t)) {
        new (t)ProfiledThread(0);
        uint16_t used = __atomic_fetch_add(&_used, -1, __ATOMIC_RELEASE);
        assert(used > 0);

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:83

  • acquire() reconstructs a claimed ProfiledThread in-place. Even though the slot is "claimed", other threads may still probe its claim state concurrently (via _misc_flags), and reconstructing the object restarts its lifetime while concurrent reads are possible. This is undefined behavior in C++ and can manifest as intermittent races. Prefer a separate per-slot claim flag (outside the object being reconstructed), or avoid placement-new and instead reset fields under exclusive ownership without touching memory concurrently accessed by other threads.
        ProfiledThread* t = pool->claim(tid);
        if (t != nullptr) {
            new (t)ProfiledThread(tid, true /* claimed */);
        }
        return t;

ddprof-lib/src/main/cpp/threadLocalData.h:183

  • ProfiledThread::current() is declared inline here but no longer defined in this header. Several existing translation units still include threadLocalData.h (not threadLocalData.inline.h) and call ProfiledThread::current(), which will fail to compile. Either keep current() defined here (as before) or make threadLocalData.h include the inline definitions.
  // Signal-handler friendly (no allocation): returns existing TLS or nullptr.
  static inline ProfiledThread *current();
  // signal-handler friendly with priming: return existing TLS or acquire and set
  // ProfiledThread from ThreadLocalDataPool.
  static inline ProfiledThread* acquire_current();

ddprof-lib/src/main/cpp/threadLocalData.inline.h:13

  • With ProfiledThread::current() defined back in threadLocalData.h, this out-of-class definition becomes a duplicate definition when threadLocalData.inline.h is included (directly or indirectly). Remove it to avoid redefinition errors.
inline ProfiledThread* ProfiledThread::current() {

ddprof-lib/src/main/cpp/threadLocalDataPool.h:20

  • _used is a 16-bit counter but _capacity is 64-bit; if the pool capacity is ever increased beyond 65535, _used will wrap and the full/empty checks become incorrect. Use a wider counter type that can represent _capacity.
    const uint64_t      _capacity;
    volatile uint16_t   _used;
    ProfiledThread*     _threads;

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:41

  • _used was widened in the header, but this local variable is still uint16_t, which will truncate the atomic counter and break capacity checks once _used exceeds 65535.

This issue also appears on line 62 of the same file.

    uint16_t used = __atomic_fetch_add(&_used, 1, __ATOMIC_RELAXED);

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:72

  • This PR introduces a new concurrent, signal-path critical allocation strategy (ThreadLocalDataPool + ProfiledThread::acquire_current()), but there are no accompanying C++ unit tests validating pool exhaustion behavior, claim/release correctness, or the interaction with TLS teardown (ProfiledThread::freeValue). The repo has an existing gtest suite under ddprof-lib/src/test/cpp/; please add targeted tests to lock in correctness.
void ThreadLocalDataPool::initialize() {
    ThreadLocalDataPool* pool = new ThreadLocalDataPool();
    __atomic_store_n(&_pool, pool, __ATOMIC_RELEASE);
}

Comment thread ddprof-lib/src/main/cpp/threadLocalDataPool.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/jvmSupport.cpp Outdated
@dd-octo-sts

dd-octo-sts Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (commit fceea70)

Pipeline: https://gitlab.ddbuild.io/DataDog/apm-reliability/benchmarking-platform/-/pipelines/128719860 Commit: fceea7073757acb8e1b5aa44b9ab2582d73d1f5a

⚠️ Significant outliers

  • 🟢 fj-kmeans (JDK 21): runtime -4.1% (2786→2671 ms)
Runtime details (per benchmark × JDK)
Benchmark JDK Latest Dev Δ (dev vs latest) Issues L/D
akka-uct 21 ✅ 10321 ms (21 iters) ✅ 10192 ms (21 iters) ≈ -1.2% (±10.9%) — / —
akka-uct 25 ✅ 8886 ms (24 iters) ✅ 8832 ms (24 iters) ≈ -0.6% (±9.6%) — / —
finagle-chirper 21 ✅ 5957 ms (33 iters) ✅ 5974 ms (33 iters) ≈ +0.3% (±25.3%) ⚠️ W:3 / ⚠️ W:3
finagle-chirper 25 ✅ 5519 ms (36 iters) ✅ 5498 ms (36 iters) ≈ -0.4% (±24.8%) ⚠️ W:3 / ⚠️ W:3
fj-kmeans 21 ✅ 2786 ms (67 iters) ✅ 2671 ms (69 iters) 🟢 -4.1% — / —
future-genetic 25 ✅ 2047 ms (91 iters) ✅ 2011 ms (93 iters) ≈ -1.8% (±2.6%) — / —
naive-bayes 21 ✅ 1284 ms (134 iters) ✅ 1311 ms (131 iters) ≈ +2.1% (±32.7%) — / —
naive-bayes 25 ✅ 1025 ms (167 iters) ✅ 1020 ms (168 iters) ≈ -0.5% (±31.3%) — / —
reactors 21 ✅ 16732 ms (15 iters) ✅ 16102 ms (15 iters) ≈ -3.8% (±7.1%) — / —
reactors 25 ✅ 18448 ms (15 iters) ✅ 18579 ms (15 iters) ≈ +0.7% (±4.5%) — / —
Internal counter details (ddprof)

ddprof internal counters, latest / dev (✅ = 0, · = unavailable):

Benchmark JDK Dropped rec Dropped jvmti Dropped trace Skipped WC AGCT fail Unwind fail
akka-uct 21 ✅ / ✅ ✅ / ✅ 7 / 3 1976 / 1941 ✅ / ✅ ✅ / ✅
akka-uct 25 ✅ / ✅ ✅ / ✅ 3 / 2 2265 / 2224 ✅ / ✅ ✅ / ✅
finagle-chirper 21 ✅ / ✅ ✅ / ✅ 1 / 2 8312 / 8889 ✅ / ✅ ✅ / ✅
finagle-chirper 25 ✅ / ✅ ✅ / ✅ 1 / 2 8627 / 8192 ✅ / ✅ ✅ / ✅
fj-kmeans 21 ✅ / ✅ ✅ / ✅ 3 / 2 1269 / 1259 ✅ / ✅ ✅ / ✅
fj-kmeans 25 ✅ / ✅ ✅ / ✅ 3 / 2 1274 / 1254 ✅ / ✅ ✅ / ✅
future-genetic 25 ✅ / ✅ ✅ / ✅ 2 / 2 2948 / 2844 ✅ / ✅ ✅ / ✅
naive-bayes 21 ✅ / ✅ ✅ / ✅ 2 / 3 3523 / 3540 ✅ / ✅ ✅ / ✅
naive-bayes 25 ✅ / ✅ ✅ / ✅ 2 / 4 3486 / 3482 ✅ / ✅ ✅ / ✅
reactors 21 ✅ / ✅ ✅ / ✅ ✅ / 3 1673 / 1585 ✅ / ✅ ✅ / ✅
reactors 25 ✅ / ✅ ✅ / ✅ 3 / ✅ 1903 / 1821 ✅ / ✅ ✅ / ✅

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 3, 2026 22:35

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

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:63

  • ThreadLocalDataPool::unclaim currently uses placement-new to reset the slot, which clears _misc_flags (including FLAG_CLAIMED) via non-atomic stores and before decrementing _used. This can race with claim_acquire() (undefined behavior due to atomic/non-atomic access to the same word) and can also cause claim() to spuriously reject acquisitions when _used is still at capacity even though a slot has been freed.
bool ThreadLocalDataPool::unclaim(ProfiledThread* t) {
    if (contains(t)) {
        new (t)ProfiledThread(0);
        uint16_t used = __atomic_fetch_add(&_used, -1, __ATOMIC_RELEASE);
        assert(used > 0);

ddprof-lib/src/main/cpp/threadLocalData.h:183

  • JVMSupport::initialize() needs to gate priming based on the ProfiledThread::_current_thread TLS key index (the one used by _current_thread.set() in acquire_current()), but ProfiledThread currently doesn't expose that key. Adding a small accessor keeps ThreadLocal encapsulated and allows the priming check to target the correct pthread key.
  // be interrupted by signals
  // This method is used for initializing ProfiledThread for known JNI entry points,
  // in case that the Java threads were started before thread creation interceptor
  // is fully initialized, so that ProfiledThreads were not setup for the threads.
  static ProfiledThread* initCurrentThreadSignalSafe();

Comment thread ddprof-lib/src/main/cpp/jvmSupport.cpp Outdated
Copilot AI review requested due to automatic review settings August 3, 2026 22:42

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

Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.

Suppressed comments (5)

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:63

  • ThreadLocalDataPool::unclaim overwrites an existing ProfiledThread with placement-new without running the destructor. ProfiledThread owns heap allocations via UnwindFailures (see unwindStats.h), so this leaks memory each time a slot is released, and also makes the slot eligible for reuse while its internal state is being rewritten.
bool ThreadLocalDataPool::unclaim(ProfiledThread* t) {
    if (contains(t)) {
        new (t)ProfiledThread(0);
        uint16_t used = __atomic_fetch_add(&_used, -1, __ATOMIC_RELEASE);
        assert(used > 0);

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:83

  • ThreadLocalDataPool::acquire reconstructs ProfiledThread with placement-new. This constructor transitively allocates (e.g., UnwindFailures does new[]), so calling ProfiledThread::acquire_current() from signal contexts can reintroduce malloc/new in the signal handler path, defeating the purpose of priming and potentially deadlocking/crashing.
ProfiledThread* ThreadLocalDataPool::acquire(int tid) {
    ThreadLocalDataPool* pool = __atomic_load_n(&_pool, __ATOMIC_ACQUIRE);
    if (pool == nullptr) {
        return nullptr;
    } else {
        ProfiledThread* t = pool->claim(tid);
        if (t != nullptr) {
            new (t)ProfiledThread(tid, true /* claimed */);
        }
        return t;

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:27

  • The pool pre-constructs capacity ProfiledThread objects. Each ProfiledThread constructs UnwindFailures, which allocates two large heap arrays, so initialize() will allocate on the order of tens of MB (e.g., ~64 * ~290KB just for UnwindFailures) and do a lot of zeroing work at startup.

This issue also appears on line 74 of the same file.

    _threads = reinterpret_cast<ProfiledThread*>(p);
    for (uint64_t index = 0; index < capacity; index++) {
        new (&_threads[index]) ProfiledThread(0);
    }
    NativeMem::record(NM_THREAD_LOCAL, malloc_size);

ddprof-lib/src/main/cpp/threadLocalData.h:131

  • The assert message in unclaim() is misleading: it fires when the slot is not claimed, but the message reads as if it already is claimed.
  inline void unclaim() {
    assert(isClaimed() && "Slot has been claimed");
    __atomic_fetch_and(&_misc_flags, ~FLAG_CLAIMED, __ATOMIC_RELEASE);

ddprof-lib/src/main/cpp/jvmSupport.cpp:44

  • Priming/pooling is now enabled from JVMSupport::initialize(), but there are no accompanying C++ unit tests asserting (a) the supportPriming() gate behavior and (b) that ProfiledThread::acquire_current() stays allocation-free/signal-safe when priming is enabled. There are existing gtests in this module (e.g., ddprof-lib/src/test/cpp/threadLocal_ut.cpp, signalSafety_ut.cpp) that could cover this.
    if (ProfiledThread::supportPriming()) {
        ThreadLocalDataPool::initialize();
    }

@dd-octo-sts

dd-octo-sts Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (commit 9a69529)

Pipeline: https://gitlab.ddbuild.io/DataDog/apm-reliability/benchmarking-platform/-/pipelines/128733319 Commit: 9a69529e419112c5a11022e8656979111a43af80

✅ Within expected boundaries

No significant runtime deltas (all within run-to-run noise) and no internal-counter outliers.

Runtime details (per benchmark × JDK)
Benchmark JDK Latest Dev Δ (dev vs latest) Issues L/D
akka-uct 21 ✅ 10363 ms (21 iters) ✅ 10184 ms (21 iters) ≈ -1.7% (±10.5%) — / —
akka-uct 25 ✅ 8913 ms (24 iters) ✅ 8896 ms (24 iters) ≈ -0.2% (±10.1%) — / —
finagle-chirper 21 ✅ 5948 ms (33 iters) ✅ 5903 ms (33 iters) ≈ -0.8% (±25.1%) ⚠️ W:3 / ⚠️ W:3
finagle-chirper 25 ✅ 5422 ms (36 iters) ✅ 5494 ms (36 iters) ≈ +1.3% (±25.1%) ⚠️ W:3 / ⚠️ W:3
fj-kmeans 21 ✅ 2749 ms (68 iters) ✅ 2696 ms (69 iters) ≈ -1.9% (±2.8%) — / —
fj-kmeans 25 ✅ 2839 ms (66 iters) ✅ 2790 ms (67 iters) ≈ -1.7% (±2.6%) — / —
future-genetic 21 ✅ 2121 ms (88 iters) ✅ 2129 ms (87 iters) ≈ +0.4% (±2.7%) — / —
future-genetic 25 ✅ 2059 ms (90 iters) ✅ 2103 ms (88 iters) ≈ +2.1% (±2.7%) — / —
naive-bayes 21 ✅ 1227 ms (139 iters) ✅ 1267 ms (136 iters) ≈ +3.3% (±32.9%) — / —
naive-bayes 25 ✅ 987 ms (173 iters) ✅ 1008 ms (169 iters) ≈ +2.1% (±31.9%) — / —
reactors 21 ✅ 16409 ms (15 iters) ✅ 16151 ms (15 iters) ≈ -1.6% (±8.4%) — / —
reactors 25 ✅ 18481 ms (15 iters) ✅ 18554 ms (15 iters) ≈ +0.4% (±4.9%) — / —
Internal counter details (ddprof)

ddprof internal counters, latest / dev (✅ = 0, · = unavailable):

Benchmark JDK Dropped rec Dropped jvmti Dropped trace Skipped WC AGCT fail Unwind fail
akka-uct 21 ✅ / ✅ ✅ / ✅ 5 / 2 2060 / 1989 ✅ / ✅ ✅ / ✅
akka-uct 25 ✅ / ✅ ✅ / ✅ 3 / 3 2306 / 2153 ✅ / ✅ ✅ / ✅
finagle-chirper 21 ✅ / ✅ ✅ / ✅ 4 / 4 8781 / 8406 ✅ / ✅ ✅ / ✅
finagle-chirper 25 ✅ / ✅ ✅ / ✅ 2 / ✅ 8131 / 8680 ✅ / ✅ ✅ / ✅
fj-kmeans 21 ✅ / ✅ ✅ / ✅ ✅ / ✅ 1257 / 1248 ✅ / ✅ ✅ / ✅
fj-kmeans 25 ✅ / ✅ ✅ / ✅ 2 / 1 1298 / 1263 ✅ / ✅ ✅ / ✅
future-genetic 21 ✅ / ✅ ✅ / ✅ 3 / 1 3042 / 2950 ✅ / ✅ ✅ / ✅
future-genetic 25 ✅ / ✅ ✅ / ✅ 1 / 3 2939 / 2883 ✅ / ✅ ✅ / ✅
naive-bayes 21 ✅ / ✅ ✅ / ✅ 2 / 3 3500 / 3557 ✅ / ✅ ✅ / ✅
naive-bayes 25 ✅ / ✅ ✅ / ✅ 1 / 3 3488 / 3472 ✅ / ✅ ✅ / ✅
reactors 21 ✅ / ✅ ✅ / ✅ 1 / 1 1653 / 1596 ✅ / ✅ ✅ / ✅
reactors 25 ✅ / ✅ ✅ / ✅ ✅ / 2 1868 / 1951 ✅ / ✅ ✅ / ✅

Copilot AI review requested due to automatic review settings August 4, 2026 00:51

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

Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.

Suppressed comments (5)

ddprof-lib/src/main/cpp/threadLocalData.h:192

  • ProfiledThread::current() / acquire_current() are declared inline in threadLocalData.h but their definitions live in a separate header (threadLocalData.inline.h). Many translation units in this repo still include only "threadLocalData.h" and call ProfiledThread::current() (e.g., profiler.cpp, wallClock.cpp, javaApi.cpp). This split makes linkage/inlining brittle and can lead to undefined references depending on compiler/linker settings. Consider either (a) moving the current() definition back into threadLocalData.h (it doesn’t depend on the pool), and keeping only acquire_current() in the inline header, or (b) providing a non-inline out-of-line definition in threadLocalData.cpp and removing the inline-only definition.
  // Signal-handler friendly (no allocation): returns existing TLS or nullptr.
  static inline ProfiledThread *current();
  // signal-handler friendly with priming: return existing TLS or acquire and set
  // ProfiledThread from ThreadLocalDataPool.
  static inline ProfiledThread* acquire_current();

ddprof-lib/src/main/cpp/threadLocalData.cpp:32

  • supportPriming() is called from JVMSupport::initialize() before ProfiledThread::isThreadKeyValid() is checked. The current implementation asserts the key is valid, which can trip in debug builds (and in release builds it can still read/compare an invalid key). Safer to treat an invalid key as “priming unsupported” and return false.
bool ProfiledThread::supportPriming() {
    // Key must be valid
    assert(_current_thread.isKeyValid());
    if (OS::isMusl()) {
      return true;
    } else {
      return _current_thread.key() < PTHREAD_KEY_2NDLEVEL_SIZE;
    }
  }

ddprof-lib/src/main/cpp/threadLocalData.cpp:121

  • resetClaimed() doesn’t fully reinitialize the reused ProfiledThread slot. In particular it leaves ThreadLocalData::_unwinding_Java unchanged and resets _filter_slot_id to 0 even though the constructor uses -1 as the “unregistered” sentinel. This can leak per-thread state across slot reuse (e.g., permanently disable AsyncGetCallTrace for a future thread, or treat it as already registered in the thread filter).
void ProfiledThread::resetClaimed(int tid) {
  _jmp_buf = nullptr;
  _pc = 0;
  _sp = 0;
  _span_id = 0;

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:54

  • ThreadLocalDataPool::claim() mixes signed int indices with _capacity (a uint64_t). int start_pos = tid % _capacity; and the subsequent modulo arithmetic can produce implementation-defined results if tid is negative and also risks truncation if the pool capacity is ever increased. Using an unsigned index type avoids these issues and keeps the modulo math well-defined.
    int start_pos = tid % _capacity;
    int index = start_pos;
    do {
        if (_threads[index].claim_acquire()) {
            return &_threads[index];

ddprof-lib/src/main/cpp/threadLocalDataPool.h:20

  • _capacity is a 64-bit value but _used is a 16-bit counter (volatile uint16_t) that’s manipulated via __atomic_fetch_add. This makes the pool fragile if capacity is ever configured above 65535 (counter overflow) and volatile doesn’t add correctness for atomic operations. Consider using std::atomic<uint32_t>/std::atomic<uint64_t> (or at least a wider non-volatile integer) for _used and aligning the types used in the atomic ops.
    const uint64_t      _capacity;
    volatile uint16_t   _used;
    ProfiledThread*     _threads;

@dd-octo-sts

dd-octo-sts Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (commit 3b1f48c)

Pipeline: https://gitlab.ddbuild.io/DataDog/apm-reliability/benchmarking-platform/-/pipelines/128743773 Commit: 3b1f48cef146d1928737e2a1b026c6a0b55f2afe

⚠️ Significant outliers

  • 🔴 future-genetic (JDK 21): runtime +3.4% (2035→2105 ms)
  • 🔴 future-genetic (JDK 25): runtime +4.2% (2007→2092 ms)
Runtime details (per benchmark × JDK)
Benchmark JDK Latest Dev Δ (dev vs latest) Issues L/D
akka-uct 21 ✅ 10402 ms (21 iters) ✅ 10326 ms (21 iters) ≈ -0.7% (±11%) — / —
akka-uct 25 ✅ 8812 ms (24 iters) ✅ 8893 ms (24 iters) ≈ +0.9% (±10.3%) — / —
finagle-chirper 21 ✅ 6006 ms (33 iters) ✅ 5966 ms (33 iters) ≈ -0.7% (±24.8%) ⚠️ W:4 / ⚠️ W:3
finagle-chirper 25 ✅ 5462 ms (36 iters) ✅ 5449 ms (36 iters) ≈ -0.2% (±24.1%) ⚠️ W:3 / ⚠️ W:4
fj-kmeans 21 ✅ 2734 ms (68 iters) ✅ 2667 ms (69 iters) ≈ -2.5% (±2.6%) — / —
fj-kmeans 25 ✅ 2772 ms (67 iters) ✅ 2815 ms (66 iters) ≈ +1.6% (±2.7%) — / —
future-genetic 21 ✅ 2035 ms (91 iters) ✅ 2105 ms (88 iters) 🔴 +3.4% — / —
future-genetic 25 ✅ 2007 ms (93 iters) ✅ 2092 ms (89 iters) 🔴 +4.2% — / —
naive-bayes 25 ✅ 978 ms (175 iters) ✅ 977 ms (174 iters) ≈ -0.1% (±31.7%) — / —
reactors 21 ✅ 16274 ms (15 iters) ✅ 15699 ms (16 iters) ≈ -3.5% (±8.2%) — / —
Internal counter details (ddprof)

ddprof internal counters, latest / dev (✅ = 0, · = unavailable):

Benchmark JDK Dropped rec Dropped jvmti Dropped trace Skipped WC AGCT fail Unwind fail
akka-uct 21 ✅ / ✅ ✅ / ✅ 4 / 4 2084 / 2002 ✅ / ✅ ✅ / ✅
akka-uct 25 ✅ / ✅ ✅ / ✅ 1 / 2 2191 / 2309 ✅ / ✅ ✅ / ✅
finagle-chirper 21 ✅ / ✅ ✅ / ✅ 5 / 3 8843 / 8350 ✅ / ✅ ✅ / ✅
finagle-chirper 25 ✅ / ✅ ✅ / ✅ ✅ / 1 7960 / 8069 ✅ / ✅ ✅ / ✅
fj-kmeans 21 ✅ / ✅ ✅ / ✅ ✅ / ✅ 1248 / 1246 ✅ / ✅ ✅ / ✅
fj-kmeans 25 ✅ / ✅ ✅ / ✅ 2 / 2 1231 / 1281 ✅ / ✅ ✅ / ✅
future-genetic 21 ✅ / ✅ ✅ / ✅ 2 / 4 2908 / 2939 ✅ / ✅ ✅ / ✅
future-genetic 25 ✅ / ✅ ✅ / ✅ ✅ / 1 2960 / 2887 ✅ / ✅ ✅ / ✅
naive-bayes 25 ✅ / ✅ ✅ / ✅ 3 / 5 3431 / 3469 ✅ / ✅ ✅ / ✅
reactors 21 ✅ / ✅ ✅ / ✅ 1 / 2 1563 / 1544 ✅ / ✅ ✅ / ✅

Copilot AI review requested due to automatic review settings August 4, 2026 01:38

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

Copilot reviewed 24 out of 24 changed files in this pull request and generated no new comments.

Suppressed comments (5)

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:20

  • If the backing malloc fails, the constructor returns without recording the ThreadLocalDataPool object allocation in NativeMem (but the object itself was still allocated via new). This skews NM_THREAD_LOCAL accounting on low-memory paths.
    const size_t malloc_size = capacity * sizeof(ProfiledThread);
    void* p = malloc(malloc_size);
    if (p == nullptr) {
        return;
    }

ddprof-lib/src/main/cpp/threadLocalData.h:133

  • The assert message in unclaim() contradicts the condition: it asserts the slot is claimed, but the message reads as if that's the failure case. This makes debugging assertion failures confusing.
  inline void unclaim() {
    assert(isClaimed() && "Slot has been claimed");
    __atomic_fetch_and(&_misc_flags, ~FLAG_CLAIMED, __ATOMIC_RELEASE);

ddprof-lib/src/main/cpp/threadLocalData.cpp:32

  • supportPriming() relies on assert(_current_thread.isKeyValid()), which is compiled out in release builds. On musl this currently returns true even if the pthread key is invalid, and the glibc-specific key-index check is applied on non-Linux platforms as well. This can enable priming when the prerequisite (a valid key / expected pthread implementation behavior) is not met.
bool ProfiledThread::supportPriming() {
  // Key must be valid
  assert(_current_thread.isKeyValid());
  if (OS::isMusl()) {
    return true;

ddprof-lib/src/main/cpp/threadLocalData.cpp:113

  • resetClaimed() initializes _filter_slot_id to 0, but ProfiledThread’s constructor uses -1 as the default/unset value. Since ThreadFilter::slotForId() treats negative IDs as invalid, resetting to 0 can incorrectly associate a reclaimed slot with thread-filter slot 0.
  _misc_flags = FLAG_CLAIMED;
  _park_block_token = 0;
  _filter_slot_id = 0;
  _init_window = 0;

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:36

  • ThreadLocalDataPool records NM_THREAD_LOCAL in the constructor, but the destructor frees _threads without decrementing the corresponding NativeMem accounting. If the pool is ever destroyed (tests, future shutdown path), NM_THREAD_LOCAL will be permanently inflated.
ThreadLocalDataPool::~ThreadLocalDataPool() {
    if (_threads != nullptr) {
        for (uint64_t index = 0; index < _capacity; index++) {
            _threads[index].~ProfiledThread();
        }

@dd-octo-sts

dd-octo-sts Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (commit 01d4b42)

Pipeline: https://gitlab.ddbuild.io/DataDog/apm-reliability/benchmarking-platform/-/pipelines/128746444 Commit: 01d4b4214706754ad7d458291aea59c4986fd44c

⚠️ Significant outliers

  • 🟢 future-genetic (JDK 25): runtime -4% (2102→2018 ms)
Runtime details (per benchmark × JDK)
Benchmark JDK Latest Dev Δ (dev vs latest) Issues L/D
akka-uct 21 ✅ 10291 ms (21 iters) ✅ 10266 ms (21 iters) ≈ -0.2% (±12%) — / —
akka-uct 25 ✅ 8824 ms (24 iters) ✅ 8867 ms (24 iters) ≈ +0.5% (±10.1%) — / —
finagle-chirper 21 ✅ 5915 ms (33 iters) ✅ 5952 ms (33 iters) ≈ +0.6% (±25.5%) ⚠️ W:3 / ⚠️ W:3
finagle-chirper 25 ✅ 5465 ms (36 iters) ✅ 5448 ms (36 iters) ≈ -0.3% (±24.2%) ⚠️ W:3 / ⚠️ W:3
fj-kmeans 21 ✅ 2704 ms (69 iters) ✅ 2673 ms (69 iters) ≈ -1.1% (±2.6%) — / —
fj-kmeans 25 ✅ 2853 ms (66 iters) ✅ 2785 ms (67 iters) ≈ -2.4% (±2.6%) — / —
future-genetic 21 ✅ 2071 ms (89 iters) ✅ 2037 ms (90 iters) ≈ -1.6% (±2.6%) — / —
future-genetic 25 ✅ 2102 ms (89 iters) ✅ 2018 ms (92 iters) 🟢 -4% — / —
naive-bayes 21 ✅ 1304 ms (131 iters) ✅ 1317 ms (130 iters) ≈ +1% (±32.7%) — / —
naive-bayes 25 ✅ 1011 ms (170 iters) ✅ 1007 ms (170 iters) ≈ -0.4% (±31.4%) — / —
reactors 21 ✅ 15853 ms (15 iters) ✅ 15986 ms (16 iters) ≈ +0.8% (±7.6%) — / —
reactors 25 ✅ 18436 ms (15 iters) ✅ 18803 ms (15 iters) ≈ +2% (±4.1%) — / —
Internal counter details (ddprof)

ddprof internal counters, latest / dev (✅ = 0, · = unavailable):

Benchmark JDK Dropped rec Dropped jvmti Dropped trace Skipped WC AGCT fail Unwind fail
akka-uct 21 ✅ / ✅ ✅ / ✅ 1 / 1 2048 / 1876 ✅ / ✅ ✅ / ✅
akka-uct 25 ✅ / ✅ ✅ / ✅ 1 / 1 2283 / 2247 ✅ / ✅ ✅ / ✅
finagle-chirper 21 ✅ / ✅ ✅ / ✅ 8 / 3 8368 / 8166 ✅ / ✅ ✅ / ✅
finagle-chirper 25 ✅ / ✅ ✅ / ✅ 2 / 3 8164 / 8487 ✅ / ✅ ✅ / ✅
fj-kmeans 21 ✅ / ✅ ✅ / ✅ 3 / 1 1279 / 1254 ✅ / ✅ ✅ / ✅
fj-kmeans 25 ✅ / ✅ ✅ / ✅ 1 / ✅ 1286 / 1300 ✅ / ✅ ✅ / ✅
future-genetic 21 ✅ / ✅ ✅ / ✅ ✅ / ✅ 3010 / 2875 ✅ / ✅ ✅ / ✅
future-genetic 25 ✅ / ✅ ✅ / ✅ 2 / ✅ 2915 / 2930 ✅ / ✅ ✅ / ✅
naive-bayes 21 ✅ / ✅ ✅ / ✅ 4 / 8 3486 / 3466 ✅ / ✅ ✅ / ✅
naive-bayes 25 ✅ / ✅ ✅ / ✅ 4 / 2 3507 / 3492 ✅ / ✅ ✅ / ✅
reactors 21 ✅ / ✅ ✅ / ✅ ✅ / 1 1572 / 1742 ✅ / ✅ ✅ / ✅
reactors 25 ✅ / ✅ ✅ / ✅ ✅ / ✅ 1824 / 1932 ✅ / ✅ ✅ / ✅

@jbachorik jbachorik added the sphinx:spotcheck Sphinx: spot-check recommended label Aug 4, 2026

@jbachorik jbachorik left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🗿 🤖 🔴

Sphinx Review found 3 critical/high severity finding(s) that must be addressed.

return ctx;
}

void ProfiledThread::resetClaimed(int tid) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🗿 🤖 🔴

[Sphinx Review — HIGH] ProfiledThread::resetClaimed() resets most per-thread fields on pool-slot recycle but never resets _in_critical_section or _unwind_failures. If the previous slot occupant was interrupted mid-CriticalSection by a checkFault() siglongjmp (which bypasses the CriticalSection destructor, a pattern already documented elsewhere in this PR via SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP), the flag can be left stuck at true; the next, unrelated thread that claims the recycled slot inherits it and can never successfully enter its own critical section (tryEnterCriticalSection()'s CAS never succeeds). _unwind_failures similarly carries stale counts into the new tenant.

Suggestion: Add _in_critical_section = false; and _unwind_failures.clear(); to resetClaimed() so a reused slot starts from the same defaults as a freshly-constructed ProfiledThread.

_recording_epoch = 0;
_misc_flags = FLAG_CLAIMED;
_park_block_token = 0;
_filter_slot_id = 0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🗿 🤖 🔴

[Sphinx Review — HIGH] ProfiledThread::resetClaimed() sets _filter_slot_id = 0 instead of the -1 "unassigned" sentinel used by the constructor and by Profiler::onThreadEnd(). Since ThreadFilter::slotForId() only rejects slot_id < 0 (slot 0 is a real, valid slot), a freshly pool-claimed ProfiledThread will report ownership of filter slot 0, causing WallClock::prepareWallPrecheck() and other filterSlotId() readers to inspect/mutate another thread's real filter/block-run state instead of treating this thread as unregistered.

Suggestion: Reset _filter_slot_id to -1 in resetClaimed(), matching the constructor default and the sentinel convention used by ThreadFilter::slotForId.

return _current_thread.get();
}

ProfiledThread* ProfiledThread::acquire_current() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🗿 🤖 🔴

[Sphinx Review — HIGH] ProfiledThread::acquire_current() performs a non-atomic check-then-claim-then-publish sequence (current() -> ThreadLocalDataPool::acquire() -> _current_thread.set()) without blocking signals, unlike initCurrentThreadSignalSafe() which wraps the equivalent sequence in a SignalBlocker specifically because it "cannot be interrupted by signals". A different signal (e.g. SIGVTALRM interrupting a SIGPROF handler, since OS::installSignalHandler's sa_mask is empty) can run the same sequence in the gap between acquire() and set(), claim a second pool slot, and publish it first; when the outer handler resumes it unconditionally overwrites TLS with its own slot, permanently orphaning the inner handler's slot in the fixed-capacity (64) pool.

Suggestion: Re-check current() immediately before _current_thread.set() and, if a nested signal already published a slot, release the just-claimed slot back to the pool via ThreadLocalDataPool::release() instead of overwriting the TLS value; or wrap claim+publish in a SignalBlocker.

return _current_thread.get();
}

ProfiledThread* ProfiledThread::acquire_current() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🗿 🤖 🔴

[Sphinx Review — MEDIUM] acquire_current() is defined in threadLocalData.inline.h, which every TU that previously only needed ProfiledThread::current() was switched to include instead of threadLocalData.h -- pulling in ThreadLocalDataPool.h as a transitive dependency for ~13 TUs that never call acquire_current() (only stackWalker.cpp and hotspotSupport.cpp do). This leaks an internal recycling-pool implementation detail into the include graph of every ProfiledThread consumer.

Suggestion: Keep ProfiledThread::current() available via threadLocalData.h alone (it doesn't need ThreadLocalDataPool), and only pull in threadLocalData.inline.h / threadLocalDataPool.h from the 2 TUs that actually call acquire_current().

NativeMem::record(NM_THREAD_LOCAL, malloc_size + sizeof(ThreadLocalDataPool));
}

ThreadLocalDataPool::~ThreadLocalDataPool() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🗿 🤖 🔴

NIT

[Sphinx Review — LOW] ThreadLocalDataPool::~ThreadLocalDataPool() is never invoked anywhere in the codebase (no delete _pool/delete pool call site exists); the destructor is dead code given the singleton is intentionally never torn down.

Suggestion: No action needed if the singleton's process-lifetime intent is documented (see the sibling finding); otherwise consider removing the unused destructor or adding a comment noting it exists only for symmetry/testability.

__atomic_fetch_and(&_misc_flags, ~FLAG_CLAIMED, __ATOMIC_RELEASE);
}

inline bool claim_acquire() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🗿 🤖 🔴

[Sphinx Review — LOW] New methods claim_acquire() and acquire_current() use snake_case, breaking the camelCase naming convention used consistently elsewhere in ProfiledThread/ThreadLocalDataPool (initCurrentThread, currentTid, isThreadKeyValid, tryEnterCriticalSection, etc.).

Suggestion: Rename to claimAcquire() and acquireCurrent() to match the established camelCase convention in this class.


// PTHREAD_KEY_2NDLEVEL_SIZE is an internal macro set to 32 in the GNU C Library (glibc) NPTL
// implementation. Slot indexes less than PTHREAD_KEY_2NDLEVEL_SIZE are pre-allocated.
static constexpr int PTHREAD_KEY_2NDLEVEL_SIZE = 32;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🗿 🤖 🔴

[Sphinx Review — MEDIUM] supportPriming() hardcodes glibc's private NPTL implementation constant PTHREAD_KEY_2NDLEVEL_SIZE=32 as the sole signal-safety proxy deciding whether TLS priming is safe, with no static_assert, runtime probe, or libc-version guard. A future glibc revision or a non-glibc/non-musl libc could silently flip the heuristic either way (unsafely enabling pthread_setspecific allocation from a signal handler, or unnecessarily disabling the safety net).

Suggestion: Add a compile-time or startup sanity check tying the constant to the actual glibc version in use, or gate it behind GLIBC and a version check so unrecognized libcs degrade safely (return false) instead of assuming glibc-compatible behavior.

_wall_epoch = 0;
_call_trace_id = 0;
_recording_epoch = 0;
_misc_flags = FLAG_CLAIMED;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🗿 🤖 🔴

[Sphinx Review — MEDIUM] resetClaimed() writes _misc_flags = FLAG_CLAIMED; as a plain, non-atomic assignment, while every other access to this exact field (isClaimed/unclaim/claim_acquire) goes through _atomic* builtins with explicit ordering. This breaks the field's established atomic-only access contract; no current caller races on this instant, but it defeats TSan-clean reasoning about the field and is a latent hazard for any future concurrent reader added near slot-claim time.

Suggestion: Use __atomic_store_n(&_misc_flags, FLAG_CLAIMED, __ATOMIC_RELEASE); to match the memory-ordering discipline used elsewhere for this field.

if (OS::isMusl()) {
return true;
} else {
return _current_thread.key() < PTHREAD_KEY_2NDLEVEL_SIZE;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🗿 🤖 🔴

[Sphinx Review — LOW] No test in this PR would detect return _current_thread.key() < PTHREAD_KEY_2NDLEVEL_SIZE; mutated to <= in ProfiledThread::supportPriming(). This would incorrectly return true for a pthread key at exactly 32 (the out-of-bounds boundary).

Suggestion: Add a test exercising supportPriming() with a mocked/forced key value at the boundary (31 should return true, 32 should return false on glibc systems).

return false;
}

if (ProfiledThread::supportPriming()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🗿 🤖 🔴

[Sphinx Review — MEDIUM] ThreadLocalDataPool::initialize() only runs when supportPriming() heuristically approves (musl, or a glibc pthread-key index < 32 -- an artifact of key-allocation order unrelated to this profiler, e.g. more likely to be false on late/dynamic attach to an already-running JVM). Before this diff, StackWalker::walkFP/walkDwarf and HotspotSupport::walkVM/walkJavaStack tolerated ProfiledThread::current()==nullptr and still produced a (less-protected) stack sample; now they call acquire_current() and unconditionally drop the sample when it is null. On deployments where supportPriming() is false, the new safety net never engages, so native/signal-only threads that used to produce samples now have them silently dropped, folded into the same SAMPLES_DROPPED_THREAD_LOCAL counter as the pre-existing "no TLS" case.

Suggestion: Either initialize the pool unconditionally (sized/tuned for non-primed environments) or add a distinct counter for "sample dropped because priming unsupported" so this regression is observable in telemetry.

@jbachorik

Copy link
Copy Markdown
Collaborator

🗿 🤖 🔴

The following 3 findings apply to code/behavior outside this diff's hunks and can't be anchored as inline comments:


[Sphinx Review — LOW] ddprof-lib/src/main/cpp/threadLocalData.h:170ProfiledThread::deleteForTest() (and the freeValue() fallback it mirrors) assume the ProfiledThread was heap-allocated individually via forTid()/new. Now that ProfiledThread can also come from ThreadLocalDataPool's single malloc'd array, calling bare delete on a pool-backed instance would be undefined behavior. freeValue() itself is guarded (checks ThreadLocalDataPool::release() first), but the UNIT_TEST-only clearCurrentThreadTLS()/deleteForTest() pair has no equivalent guard. Currently latent: no existing UNIT_TEST call site exercises acquire_current() before this helper.

Suggestion: Document explicitly that these test helpers must only be used with forTid()-obtained threads, or add the same ThreadLocalDataPool::release() check that freeValue() uses before falling back to delete.


[Sphinx Review — MEDIUM] No test in this PR exercises ThreadLocalDataPool's claim/unclaim lifecycle, capacity exhaustion, slot-reuse field reset, or the acquire_current() nested-signal reentrancy window. A stub "always succeed" implementation would pass every existing test, and none of the concrete bugs found in this review (the _filter_slot_id sentinel bug, the missing _in_critical_section reset, the nested-signal slot leak) would be caught by the current test suite.

Suggestion: Add focused unit tests: (1) claim DEFAULT_CAPACITY+1 slots and assert the last claim() returns nullptr; (2) release and reclaim a slot with a different tid, asserting filterSlotId()==-1 and other reset fields match freshly-constructed defaults; (3) a stress/fault-injection test that fires two different signals against the same thread mid-acquire_current() and asserts the pool's used-slot count returns to the expected value afterward.


[Sphinx Review — MEDIUM] The "acquire_current(), then check nullptr, increment SAMPLES_DROPPED_THREAD_LOCAL, and bail out" sequence is duplicated verbatim across four call sites (StackWalker::walkFP, StackWalker::walkDwarf, HotspotSupport::walkVM, HotspotSupport::walkJavaStack) instead of being centralized once alongside acquire_current() itself.

Suggestion: Factor the null-check-and-count-drop sequence into a small helper (e.g. a variant of acquire_current() that also increments the counter) to keep the four call sites in sync as this logic evolves.

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

Labels

sphinx:spotcheck Sphinx: spot-check recommended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants