Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1118f62
feat(wall): track all threads for unfiltered wall-clock precheck
kaahos Jul 19, 2026
40dbe8e
Merge branch 'main' into paul.fournillon/wallclock-all-threads
kaahos Jul 20, 2026
d82291f
Merge branch 'main' into paul.fournillon/wallclock-all-threads
kaahos Jul 21, 2026
ec76f58
fix: apply review suggestions
kaahos Jul 21, 2026
52def55
Merge branch 'main' into paul.fournillon/wallclock-all-threads
kaahos Jul 21, 2026
9723a5a
Merge branch 'main' into paul.fournillon/wallclock-all-threads
kaahos Jul 22, 2026
e6c6793
fix: apply review
kaahos Jul 23, 2026
51a9178
Merge branch 'main' into paul.fournillon/wallclock-all-threads
kaahos Jul 24, 2026
9e3701e
Merge branch 'main' into paul.fournillon/wallclock-all-threads
kaahos Jul 24, 2026
4954d49
test: scope precheck workloads to all threads
kaahos Jul 16, 2026
80f8049
fix: preserve JVMTI frames in overlapping buffers
kaahos Jul 16, 2026
31c5cfc
test: compare precheck counter delta
kaahos Jul 17, 2026
efb6648
fix: address sphinx review
kaahos Jul 19, 2026
22b8e00
fix(wall): register unfiltered threads from lifecycle callbacks
kaahos Jul 24, 2026
d124905
Merge branch 'main' into paul.fournillon/wallclock-all-threads
kaahos Jul 31, 2026
82e05b6
Merge branch 'main' into paul.fournillon/wallclock-all-threads
kaahos Aug 3, 2026
f5b81de
test: remove obsolete thread context reset
kaahos Aug 3, 2026
b397ac9
Merge remote-tracking branch 'origin/main' into paul.fournillon/wallc…
kaahos Aug 14, 2026
599c21e
fix(test): migrate UnfilteredWallPrecheckTest to jafar JFR API
kaahos Aug 16, 2026
a68d3f2
Merge branch 'main' into paul.fournillon/wallclock-all-threads
kaahos Aug 17, 2026
391fdd2
fix: address some review comments
kaahos Aug 18, 2026
d6f5b1a
fix: address review comments
kaahos Aug 19, 2026
4d2c505
fix: avoid CAS on context-window enter/exit hot path
kaahos Aug 19, 2026
8ef675e
Merge branch 'main' into paul.fournillon/wallclock-all-threads
kaahos Aug 19, 2026
172e453
fix: fix musl bug
kaahos Aug 19, 2026
1b94815
Merge branch 'main' into paul.fournillon/wallclock-all-threads
kaahos Aug 20, 2026
26ae49b
fix: address some review comments
kaahos Aug 21, 2026
930f734
fix: restore <unloaded> label and self-skip force-failure test in non…
jbachorik Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ddprof-lib/src/main/cpp/arguments.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ class Error {

const char *message() { return _message; }

operator bool() { return _message != NULL; }
operator bool() const { return _message != NULL; }
};

class Arguments {
Expand Down
8 changes: 8 additions & 0 deletions ddprof-lib/src/main/cpp/counters.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@
X(THREAD_NAMES_COUNT, "thread_names_count") \
X(THREAD_FILTER_PAGES, "thread_filter_pages") \
X(THREAD_FILTER_BYTES, "thread_filter_bytes") \
X(THREAD_REGISTRY_CAPACITY_EXHAUSTED, "thread_registry_capacity_exhausted") \
X(THREAD_REGISTRY_INDEX_FAILURES, "thread_registry_index_failures") \
X(THREAD_REGISTRY_CONTEXT_RESET_RACE_DETECTED, "thread_registry_context_reset_race_detected") \
X(THREAD_REGISTRY_JAVACRITICAL_REREGISTRATION, "thread_registry_javacritical_reregistration") \
X(JMETHODID_SKIPPED, "jmethodid_skipped_count") \
X(CODECACHE_NATIVE_SIZE_BYTES, "codecache_native_size_bytes") \
X(CODECACHE_NATIVE_COUNT, "native_codecache_count") \
Expand All @@ -70,6 +74,10 @@
X(AGCT_BLOCKED_IN_VM, "agct_blocked_in_vm") \
X(SKIPPED_WALLCLOCK_UNWINDS, "skipped_wallclock_unwinds") \
X(WC_SIGNAL_SUPPRESSED_SAMPLED_RUN, "wc_signals_suppressed_sampled_run") \
X(WC_PRECHECK_REGISTRY_LOOKUPS, "wc_precheck_registry_lookups") \
X(WC_PRECHECK_SLOT_ID_RECOVERED, "wc_precheck_slot_id_recovered") \
X(WC_PRECHECK_CANDIDATES_REJECTED, "wc_precheck_candidates_rejected") \
X(WC_PRECHECK_LOOKUP_BUDGET_EXHAUSTED, "wc_precheck_lookup_budget_exhausted") \
X(WC_UNOWNED_BLOCKED_SUPPRESSED, "wc_unowned_blocked_suppressed") \
X(WC_UNOWNED_BLOCKED_RECORDED, "wc_unowned_blocked_recorded") \
X(WC_SIGNAL_QUEUE_FULL, "wc_signals_queue_full") \
Expand Down
6 changes: 6 additions & 0 deletions ddprof-lib/src/main/cpp/engine.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
* Copyright 2017 Andrei Pangin
* Copyright 2026, Datadog, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -51,6 +52,11 @@ class Engine {
virtual void stop();
virtual long interval() const { return 0L; }

// Whether this engine can keep the ThreadFilter registry populated and
// tracked even with no explicit filter (e.g. to support wall-clock
// prechecks).
virtual bool supportsUnfilteredThreadRegistryTracking() const { return false; }

virtual int registerThread(int tid) { return -1; }
virtual void unregisterThread(int tid) {}

Expand Down
2 changes: 1 addition & 1 deletion ddprof-lib/src/main/cpp/flightRecorder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ void Lookup::fillJavaMethodInfo(MethodInfo *mi, jmethodID method,
} else {
Counters::increment(JMETHODID_SKIPPED);
class_name_id = _classes->lookupDuringDump("", 0, Profiler::maxClassMapSize());
method_name_id = _symbols.lookup("jvmtiError");
method_name_id = _symbols.lookup("<unloaded>");
method_sig_id = _symbols.lookup("()L;");
}

Expand Down
19 changes: 19 additions & 0 deletions ddprof-lib/src/main/cpp/frames.h
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
/*
* Copyright 2026, Datadog, Inc.
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef _FRAMES_H
Comment thread
kaahos marked this conversation as resolved.
#define _FRAMES_H

#include <jni.h>
#include <jvmti.h>
#include "vmEntry.h"

inline void copyJvmtiFrames(ASGCT_CallFrame *frames,
const jvmtiFrameInfo *jvmti_frames,
jint num_frames) {
// The source and destination commonly refer to the two views of the same
// CallTraceBuffer union. Read both source fields before either write.
for (jint i = 0; i < num_frames; ++i) {

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.

This might be a premature optimization, but did you consider making the code friendlier to potential vectorization?
Probably not necessary to act upon this immediately, unless the benchmarks would show this function becoming a hotspot.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Honestly I didn't for now, but it is definitely something to consider.

jmethodID method = jvmti_frames[i].method;
jlocation location = jvmti_frames[i].location;
frames[i].method_id = method;
frames[i].bci = static_cast<jint>(location);
LP64_ONLY(frames[i].padding = 0;)
}
}

inline int makeFrame(ASGCT_CallFrame *frames, jint type, jmethodID id) {
frames[0].bci = type;
frames[0].method_id = id;
Expand Down
114 changes: 88 additions & 26 deletions ddprof-lib/src/main/cpp/javaApi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include "threadLocalData.inline.h"
#include "tsc.h"
#include "vmEntry.h"
#include "wallClock.h"
#include <errno.h>
#include <fstream>
#include <sstream>
Expand Down Expand Up @@ -163,6 +164,40 @@ Java_com_datadoghq_profiler_JavaProfiler_getSamples(JNIEnv *env,

// some duplication between add and remove, though we want to avoid having an extra branch in the hot path

static ThreadFilter::SlotID ensureCurrentThreadFilterSlot(
Comment thread
kaahos marked this conversation as resolved.
ThreadFilter *thread_filter, ProfiledThread *current) {
int tid = current->tid();
if (unlikely(tid < 0)) {
return -1;
}

ThreadFilter::SlotID slot_id = current->filterSlotId();
if (likely(slot_id >= 0)) {
if (likely(thread_filter->activeSlotForId(slot_id, tid) != nullptr)) {
return slot_id;
}
current->setFilterSlotId(-1);
}

// Startup can register this TID centrally, but it cannot update another
// pthread's TLS. registerThread(tid) reuses that existing slot.
//
// This is the only place a JavaCritical fast path (filterThreadAdd0,
// parkEnter0, blockEnter0) can block on _registry_lock. It's bounded to at
// most once per thread lifetime (cold TLS) plus once per recording-epoch
// transition this thread observes (stale cached slot) - not a per-call cost.
// THREAD_REGISTRY_JAVACRITICAL_REREGISTRATION makes that bound observable;
// if it starts firing per-sample rather than per-thread/per-recording, the
// "provably rare" assumption has broken and the JavaCritical dispatch should
// be revisited.
Counters::increment(THREAD_REGISTRY_JAVACRITICAL_REREGISTRATION);
slot_id = thread_filter->registerThread(tid);
if (slot_id >= 0) {
current->setFilterSlotId(slot_id);
}
return slot_id;
}

// JavaCritical is faster JNI, but more restrictive - parameters and return value have to be
// primitives or arrays of primitive types.
// We direct corresponding JNI calls to JavaCritical to make sure the parameters/return value
Expand All @@ -180,25 +215,22 @@ JavaCritical_com_datadoghq_profiler_JavaProfiler_filterThreadAdd0() {
return;
}
ThreadFilter *thread_filter = Profiler::instance()->threadFilter();
if (unlikely(!thread_filter->enabled())) {
if (unlikely(!thread_filter->registryActive())) {
return;
}

int slot_id = current->filterSlotId();
if (unlikely(slot_id == -1)) {
// Thread doesn't have a slot ID yet (e.g., main thread), so register it
// Happens when we are not enabled before thread start
slot_id = thread_filter->registerThread();
current->setFilterSlotId(slot_id);
}

if (unlikely(slot_id == -1)) {
int slot_id = ensureCurrentThreadFilterSlot(thread_filter, current);
if (unlikely(slot_id < 0)) {
return; // Failed to register thread
}
// Reset suppression state so a new thread occupying this slot does not inherit
// stale state from its predecessor. Must happen before add().
thread_filter->resetSlotRunState(slot_id);
thread_filter->add(tid, slot_id);
if (unlikely(!thread_filter->add(tid, slot_id))) {
// The cached slot_id was rejected (lazy tid-index fallback failed under
// this thread's own registry reset race, or the tid index is exhausted).
// Clear the cache so the next filterThreadAdd0()/parkEnter0()/blockEnter0()
// call re-runs ensureCurrentThreadFilterSlot()'s registerThread() path
// instead of leaving this thread permanently outside the context window.
current->setFilterSlotId(-1);
}
}

extern "C" DLLEXPORT void JNICALL
Expand All @@ -213,12 +245,13 @@ JavaCritical_com_datadoghq_profiler_JavaProfiler_filterThreadRemove0() {
return;
}
ThreadFilter *thread_filter = Profiler::instance()->threadFilter();
if (unlikely(!thread_filter->enabled())) {
if (unlikely(!thread_filter->registryActive())) {
return;
}

int slot_id = current->filterSlotId();
if (unlikely(slot_id == -1)) {
if (unlikely(slot_id == -1 ||
thread_filter->activeSlotForId(slot_id, tid) == nullptr)) {
// Thread doesn't have a slot ID yet - nothing to remove
return;
}
Expand Down Expand Up @@ -307,6 +340,34 @@ Java_com_datadoghq_profiler_JavaProfiler_describeDebugCounters0(
#endif // COUNTERS
}

extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfilerTestSupport_setForceWallStartFailureForTest0(
JNIEnv *env, jclass unused, jboolean force) {
#ifdef DEBUG
BaseWallClock::setForceStartFailureForTest(force);
#endif // DEBUG
}

extern "C" DLLEXPORT jboolean JNICALL
Java_com_datadoghq_profiler_JavaProfilerTestSupport_isForceWallStartFailureArmedForTest0(
JNIEnv *env, jclass unused) {
#ifdef DEBUG
return BaseWallClock::isForceStartFailureForTest() ? JNI_TRUE : JNI_FALSE;
#else
// The setForceWallStartFailureForTest0 hook above is a no-op outside DEBUG,
// so the forced-failure toggle can never be armed here. Returning false lets
// callers (e.g. UnfilteredWallPrecheckFallbackTest) self-skip via
// Assumptions.assumeTrue rather than fail spuriously in release builds.
return JNI_FALSE;
#endif // DEBUG
}

extern "C" DLLEXPORT jboolean JNICALL
Java_com_datadoghq_profiler_JavaProfilerTestSupport_isThreadRegistryActiveForTest0(
JNIEnv *env, jclass unused) {
return Profiler::instance()->threadFilter()->registryActive();
}

extern "C" DLLEXPORT void JNICALL
Java_com_datadoghq_profiler_JavaProfiler_recordSettingEvent0(
JNIEnv *env, jclass unused, jstring name, jstring value, jstring unit) {
Expand Down Expand Up @@ -383,8 +444,8 @@ Java_com_datadoghq_profiler_JavaProfiler_parkEnter0(JNIEnv *env, jclass unused)

bool first_park = current->parkEnter();
ThreadFilter *tf = Profiler::instance()->threadFilter();
if (first_park && tf->enabled()) {
ThreadFilter::SlotID slot_id = current->filterSlotId();
if (first_park && tf->registryActive()) {
ThreadFilter::SlotID slot_id = ensureCurrentThreadFilterSlot(tf, current);
if (slot_id >= 0) {
current->setParkBlockToken(
tf->enterBlockedRun(slot_id, OSThreadState::CONDVAR_WAIT));
Expand All @@ -405,9 +466,10 @@ Java_com_datadoghq_profiler_JavaProfiler_parkExit0(
return;
}
ThreadFilter *tf = Profiler::instance()->threadFilter();
if (tf->enabled()) {
if (tf->registryActive()) {
ThreadFilter::SlotID slot_id = ThreadFilter::tokenSlotId(park_block_token);
if (current->filterSlotId() == slot_id) {
if (tf->activeSlotForId(current->filterSlotId(), current->tid()) != nullptr &&
current->filterSlotId() == slot_id) {
tf->exitBlockedRun(slot_id, ThreadFilter::tokenGeneration(park_block_token));
}
}
Expand Down Expand Up @@ -435,10 +497,10 @@ Java_com_datadoghq_profiler_JavaProfiler_blockEnter0(
return 0;
}
ThreadFilter *tf = Profiler::instance()->threadFilter();
if (!tf->enabled()) {
if (!tf->registryActive()) {
return 0;
}
ThreadFilter::SlotID slot_id = current->filterSlotId();
ThreadFilter::SlotID slot_id = ensureCurrentThreadFilterSlot(tf, current);
if (slot_id < 0) {
return 0;
}
Expand All @@ -457,13 +519,13 @@ Java_com_datadoghq_profiler_JavaProfiler_blockExit0(
if (current == nullptr) {
return;
}

ThreadFilter *tf = Profiler::instance()->threadFilter();
ThreadFilter::SlotID slot_id = ThreadFilter::tokenSlotId(block_token);
if (current->filterSlotId() != slot_id) {
if (current->filterSlotId() != slot_id ||
tf->activeSlotForId(slot_id, current->tid()) == nullptr) {
return;
}
ThreadFilter *tf = Profiler::instance()->threadFilter();
if (tf->enabled()) {
if (tf->registryActive()) {
tf->exitBlockedRun(slot_id, ThreadFilter::tokenGeneration(block_token));
}
}
Expand Down
2 changes: 1 addition & 1 deletion ddprof-lib/src/main/cpp/jvmThread.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class JVMThread {
/*
* The initialization happens in early startup, in single-threaded mode,
* no synchronization is needed
*/
*/
static bool initialize();

static inline bool isInitialized() {
Expand Down
Loading
Loading