Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 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
772e5aa
fix: extract wallclock suppresion stuff from ThreadFilter into wallCl…
kaahos Sep 28, 2026
cd2ad5b
Merge branch 'main' into paul.fournillon/wallclock-all-threads
kaahos Sep 28, 2026
c8701f7
fix: address glibc issue with random
kaahos Sep 28, 2026
30f63dc
fix: fix fuzz test
kaahos Sep 29, 2026
37af845
fix: fix mutex performance regressions
kaahos Sep 29, 2026
016e3f9
Merge branch 'main' into paul.fournillon/wallclock-all-threads
kaahos Sep 29, 2026
dc9d91b
fix: guard attributionPC against null pc (UBSan)
jbachorik Sep 30, 2026
d6a04bf
fix: use CallTraceSet in fuzz_callTraceStorage processTraces lambda
jbachorik Sep 30, 2026
fbfa879
test: assert <unloaded> label on the dump that fired jmethodid_skippe…
jbachorik Sep 30, 2026
70a0cad
fix: changes to reduce memory footprint
kaahos Sep 30, 2026
b158ab9
fix: add chaos and fuzzing tests
kaahos Sep 30, 2026
9a18895
Merge branch 'fix/nightlies_sanitized' into paul.fournillon/wallclock…
kaahos Sep 30, 2026
d6f6e9c
fix: address nits
kaahos Oct 1, 2026
73b532d
fix: fix wrong comments
kaahos Oct 1, 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
7 changes: 7 additions & 0 deletions ddprof-lib/src/main/cpp/counters.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,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_HOOK_REREGISTRATION, "thread_registry_hook_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 @@ -84,6 +88,9 @@
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_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
7 changes: 7 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,12 @@ class Engine {
virtual void stop();
virtual long interval() const { return 0L; }

// Whether this engine can keep the ThreadFilter registry populated and
// tracked when context filtering is disabled with an explicit empty
// `filter=` (e.g. to support wall-clock prechecks). Without a filter
// argument the filter defaults to "0", which enables context filtering.
virtual bool supportsUnfilteredThreadRegistryTracking() const { return false; }

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

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) {
Comment thread
jbachorik marked this conversation as resolved.
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
138 changes: 105 additions & 33 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,44 @@ 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);
}

// Threads that existed before the recording started (and so never received
// a ThreadStart callback) bind their slot lazily here. If the tid is already
// indexed, registerThread(tid) returns that existing slot.
//
// This is the only place the filterThreadAdd0 (JavaCritical), parkEnter0
// and blockEnter0 hooks 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.
// A thread that cannot get a slot because the registry is full retries here
// on every call, but registerThread() rejects it without taking the lock.
// THREAD_REGISTRY_HOOK_REREGISTRATION counts every attempt, including those
// lock-free rejections (tracked separately by
// THREAD_REGISTRY_CAPACITY_EXHAUSTED). If it fires per call while
// THREAD_REGISTRY_CAPACITY_EXHAUSTED stays flat, the "provably rare"
// assumption has broken and these hooks should be revisited.
Counters::increment(THREAD_REGISTRY_HOOK_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 +219,23 @@ 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 went stale between ensureCurrentThreadFilterSlot()
// and add(): an unfiltered recording restart reset the registry, so the
// slot no longer carries this thread's tid.
// 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,13 +250,15 @@ 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)) {
// Thread doesn't have a slot ID yet - nothing to remove
if (unlikely(slot_id == -1 ||
thread_filter->activeSlotForId(slot_id, tid) == nullptr)) {
// No slot yet, or a cached slot left over from an earlier recording -
// either way this thread is not in the context window, nothing to remove
return;
}
thread_filter->remove(slot_id);
Expand Down Expand Up @@ -307,6 +346,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,11 +450,12 @@ 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) {
WallClockBlockTracker *tracker = Profiler::instance()->blockTracker();
current->setParkBlockToken(
tf->enterBlockedRun(slot_id, OSThreadState::CONDVAR_WAIT));
tracker->enterBlockedRun(tf, slot_id, OSThreadState::CONDVAR_WAIT));
}
}
}
Expand All @@ -405,10 +473,12 @@ Java_com_datadoghq_profiler_JavaProfiler_parkExit0(
return;
}
ThreadFilter *tf = Profiler::instance()->threadFilter();
if (tf->enabled()) {
ThreadFilter::SlotID slot_id = ThreadFilter::tokenSlotId(park_block_token);
if (current->filterSlotId() == slot_id) {
tf->exitBlockedRun(slot_id, ThreadFilter::tokenGeneration(park_block_token));
if (tf->registryActive()) {
ThreadFilter::SlotID slot_id = WallClockBlockTracker::tokenSlotId(park_block_token);
if (tf->activeSlotForId(current->filterSlotId(), current->tid()) != nullptr &&
current->filterSlotId() == slot_id) {
WallClockBlockTracker *tracker = Profiler::instance()->blockTracker();
tracker->exitBlockedRun(slot_id, WallClockBlockTracker::tokenGeneration(park_block_token));
}
}
}
Expand All @@ -435,14 +505,15 @@ 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;
}
return static_cast<jlong>(tf->enterBlockedRun(slot_id, decoded));
WallClockBlockTracker *tracker = Profiler::instance()->blockTracker();
return static_cast<jlong>(tracker->enterBlockedRun(tf, slot_id, decoded));
}

extern "C" DLLEXPORT void JNICALL
Expand All @@ -457,14 +528,15 @@ Java_com_datadoghq_profiler_JavaProfiler_blockExit0(
if (current == nullptr) {
return;
}

ThreadFilter::SlotID slot_id = ThreadFilter::tokenSlotId(block_token);
if (current->filterSlotId() != slot_id) {
ThreadFilter *tf = Profiler::instance()->threadFilter();
ThreadFilter::SlotID slot_id = WallClockBlockTracker::tokenSlotId(block_token);
if (current->filterSlotId() != slot_id ||
tf->activeSlotForId(slot_id, current->tid()) == nullptr) {
return;
}
ThreadFilter *tf = Profiler::instance()->threadFilter();
if (tf->enabled()) {
tf->exitBlockedRun(slot_id, ThreadFilter::tokenGeneration(block_token));
if (tf->registryActive()) {
WallClockBlockTracker *tracker = Profiler::instance()->blockTracker();
tracker->exitBlockedRun(slot_id, WallClockBlockTracker::tokenGeneration(block_token));
}
}

Expand Down
Loading
Loading