Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
323eed7
feat(thread-filter): add VMThread* slot to ThreadFilter for OS-state …
kaahos Apr 24, 2026
f80802b
feat(wall): register VMThread* in ThreadFilter at thread start and end
kaahos Apr 24, 2026
10129d2
test(wall): add PrecheckTest and PrecheckEfficiencyTest; disable prec…
kaahos Apr 24, 2026
28856f4
fix(liveness): update the _record_heap_usage flag if profiler args ch…
kaahos Apr 29, 2026
ff11514
feat(jfr): add TaskBlockEvent and datadog.TaskBlock metadata
kaahos Apr 29, 2026
0fd00b5
feat(jfr): record datadog.TaskBlock in FlightRecorder
kaahos Apr 29, 2026
51cd4e3
feat(thread): add park enter/exit state for wall-clock gating
kaahos Apr 29, 2026
13fbd20
feat(wall): store ProfiledThread in thread filter for wall sampling
kaahos Apr 29, 2026
05d9ac4
feat(wall): skip wall SIGVTALRM while thread is parked
kaahos Apr 29, 2026
dcc0810
feat(api): add recordTaskBlock and park JNI entry points
kaahos Apr 29, 2026
cb97b77
fix(hotspot): guard VMThread access for non-HotSpot JVMs
kaahos Apr 29, 2026
430e979
test(thread): add gtests for ProfiledThread park state
kaahos Apr 29, 2026
81c7712
test(wall): add TaskBlock and combined precheck/park coverage
kaahos Apr 29, 2026
c45d4d0
fix
kaahos Apr 30, 2026
7843f86
Merge branch 'main' into paul.fournillon/wallclock_precheck
kaahos May 5, 2026
9db91f8
chore: renaming things
kaahos May 5, 2026
199e533
fix: add wall-clock suppression accounting
kaahos May 7, 2026
54fc911
fix: centralize TaskBlock recording eligibility
kaahos May 7, 2026
26eb685
fix: record Object.wait TaskBlock via JVMTI
kaahos May 7, 2026
1ad80d6
fix: fix recordTaskBlock
kaahos May 11, 2026
76036eb
fix: clear park span bits and monitor-wait on buffer recycle
kaahos May 11, 2026
70d7bbb
test: cover releaseFromBuffer park and monitor-wait reset
kaahos May 11, 2026
c3715c4
chore: clarify JVMTI monitor-wait registration for JDK <21
kaahos May 11, 2026
ab57de5
fix: apply review
kaahos May 11, 2026
8a4f530
Merge branch 'main' into paul.fournillon/wallclock_precheck
kaahos May 12, 2026
5e4b08a
fix
kaahos May 12, 2026
62941d5
fix: apply copilot review
kaahos May 12, 2026
a0dfd45
fix: set wallprecheck default flag to false
kaahos May 13, 2026
4529440
fix: wallprecheck default value to false
kaahos May 19, 2026
76d78dc
fix: fix WallClockSamplingEpoch metadata
kaahos May 20, 2026
14228b4
refactor(taskblock): capture context natively via ContextApi; drop sp…
kaahos May 21, 2026
5189cf0
feat(jfr): reorder datadog.TaskBlock fields to precede the context block
kaahos May 21, 2026
fe4df73
test(park): regression case for OTEP-snapshotted span and custom attr…
kaahos May 21, 2026
cc7bef4
refactor(jvmti): rename MonitorWait dispatch to ObjectWait*
kaahos May 22, 2026
fea34f1
docs: clarify JVMTI MonitorWait observes Object.wait, not OSThreadSta…
kaahos May 22, 2026
b6c82ee
feat(wallclock): move wallprecheck filter from timer thread to signal…
kaahos May 22, 2026
f157b86
fix: fix robustness and correctness of the tests
kaahos May 22, 2026
2b2ca17
fix: move once-per-run filter state to ThreadFilter::Slot
kaahos May 26, 2026
a17586a
fix: skip sending wall-clock signals to threads already sampled while…
kaahos May 26, 2026
9cbf303
feat: native synchronized-contention capture via MonitorContendedEnte…
kaahos May 26, 2026
a45836e
feat: delegate-monitor-events handshake for JDK 21+ agent integration
kaahos May 26, 2026
3316291
test: fix wall-clock/TaskBlock test set
kaahos May 26, 2026
8587c5e
fix
kaahos May 26, 2026
73664f7
chore: prune dead skipped_parked counters
kaahos May 27, 2026
93bdeed
fix: use accumulator semantics for WallClockSamplingEpoch counters
kaahos May 27, 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
8 changes: 8 additions & 0 deletions ddprof-lib/src/main/cpp/arguments.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,14 @@ Error Arguments::parse(const char *args) {
_remote_symbolication = true;
}

CASE("wallprecheck")
if (value != NULL) {
_wall_precheck = strcmp(value, "false") != 0 && strcmp(value, "0") != 0;
} else {
// No value means disable
_wall_precheck = false;
}

CASE("wallsampler")
if (value != NULL) {
switch (value[0]) {
Expand Down
2 changes: 2 additions & 0 deletions ddprof-lib/src/main/cpp/arguments.h
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ class Arguments {
long _cpu;
long _wall;
bool _wall_collapsing;
bool _wall_precheck;
int _wall_threads_per_tick;
WallclockSampler _wallclock_sampler;
long _memory;
Expand Down Expand Up @@ -204,6 +205,7 @@ class Arguments {
_cpu(-1),
_wall(-1),
_wall_collapsing(false),
_wall_precheck(false),
_wall_threads_per_tick(DEFAULT_WALL_THREADS_PER_TICK),
_wallclock_sampler(ASGCT),
_memory(-1),
Expand Down
14 changes: 13 additions & 1 deletion ddprof-lib/src/main/cpp/counters.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@
X(AGCT_NATIVE_NO_JAVA_CONTEXT, "agct_native_no_java_context") \
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_SIGNAL_QUEUE_FULL, "wc_signals_queue_full") \
X(TASK_BLOCK_EMITTED, "task_block_emitted") \
X(TASK_BLOCK_SKIPPED_SPAN_ZERO, "task_block_skipped_span_zero") \
X(TASK_BLOCK_SKIPPED_TOO_SHORT, "task_block_skipped_too_short") \
X(UNWINDING_TIME_ASYNC, "unwinding_ticks_async") \
X(UNWINDING_TIME_JVMTI, "unwinding_ticks_jvmti") \
X(CALLTRACE_STORAGE_DROPPED, "calltrace_storage_dropped_traces") \
Expand Down Expand Up @@ -102,7 +107,14 @@
X(CTIMER_SIGNAL_OWN, "ctimer_signal_own") \
X(CTIMER_SIGNAL_FOREIGN, "ctimer_signal_foreign") \
X(WALLCLOCK_SIGNAL_OWN, "wallclock_signal_own") \
X(WALLCLOCK_SIGNAL_FOREIGN, "wallclock_signal_foreign")
X(WALLCLOCK_SIGNAL_FOREIGN, "wallclock_signal_foreign") \
/* JVMTI monitor-event callbacks (synchronized contention + Object.wait). \
* Driven by the can_generate_monitor_events capability. Zero when the agent \
* has signalled delegate_monitor_events on JDK 21+ (Java-side modules own \
* the populations); non-zero on JDK < 21 or when no delegate is registered. \
* Used by MonitorEventDelegationTest to verify the handshake. */ \
X(JVMTI_MONITOR_CONTENDED_ENTER, "jvmti_monitor_contended_enter") \
X(JVMTI_OBJECT_WAIT_ENTER, "jvmti_object_wait_enter")
#define X_ENUM(a, b) a,
typedef enum CounterId : int {
DD_COUNTER_TABLE(X_ENUM) DD_NUM_COUNTERS
Expand Down
96 changes: 83 additions & 13 deletions ddprof-lib/src/main/cpp/event.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,57 +109,118 @@ class WallClockEpochEvent {
u32 _num_failed_samples;
u32 _num_exited_threads;
u32 _num_permission_denied;
u64 _num_suppressed_sampled_run;
u64 _num_task_block_emitted;
u64 _num_task_block_skipped_span_zero;
u64 _num_task_block_skipped_too_short;

WallClockEpochEvent(u64 start_time)
: _dirty(false), _start_time(start_time), _duration_millis(0),
_num_samplable_threads(0), _num_successful_samples(0),
_num_failed_samples(0), _num_exited_threads(0),
_num_permission_denied(0) {}
_num_permission_denied(0), _num_suppressed_sampled_run(0),
_num_task_block_emitted(0), _num_task_block_skipped_span_zero(0),
_num_task_block_skipped_too_short(0) {}

bool hasChanged() { return _dirty; }

// Snapshot semantics: _num_samplable_threads tracks the current count of
// profilable threads at the time of the iteration. Dirties only on change,
// so steady-state thread counts collapse adjacent iterations into a single
// emitted epoch.
void updateNumSamplableThreads(u32 num_samplable_threads) {
if (_num_samplable_threads != num_samplable_threads) {
_dirty = true;
_num_samplable_threads = num_samplable_threads;
}
}

void updateNumSuccessfulSamples(u32 num_successful_samples) {
if (_num_successful_samples != num_successful_samples) {
// Accumulator semantics: the addNum*() methods accumulate per-iteration
// event counts into a since-last-emit total. This guarantees that when
// multiple iterations collapse into a single emitted epoch (because
// hasChanged() stays false across them — e.g. steady-state workloads where
// adjacent drained counter values happen to match), the per-iteration
// contributions are summed into the next emit rather than discarded.
// newEpoch() resets these fields to 0 at emit boundaries so each emitted
// epoch reports drained-counter activity for its own _duration_millis
// interval. JFR consumers can sum the field across all epochs to recover
// the exact cumulative count over the recording.
void addNumSuccessfulSamples(u32 n) {
if (n > 0) {
_dirty = true;
_num_successful_samples = num_successful_samples;
_num_successful_samples += n;
}
}

void updateNumFailedSamples(u32 num_failed_samples) {
if (_num_failed_samples != num_failed_samples) {
void addNumFailedSamples(u32 n) {
if (n > 0) {
_dirty = true;
_num_failed_samples = num_failed_samples;
_num_failed_samples += n;
}
}

void updateNumExitedThreads(u32 num_exited_threads) {
if (_num_exited_threads != num_exited_threads) {
void addNumExitedThreads(u32 n) {
if (n > 0) {
_dirty = true;
_num_exited_threads = num_exited_threads;
_num_exited_threads += n;
}
}

void updateNumPermissionDenied(u32 num_permission_denied) {
if (_num_permission_denied != num_permission_denied) {
void addNumPermissionDenied(u32 n) {
if (n > 0) {
_dirty = true;
_num_permission_denied = num_permission_denied;
_num_permission_denied += n;
}
}

void addNumSuppressedSampledRun(u64 n) {
if (n > 0) {
_dirty = true;
_num_suppressed_sampled_run += n;
}
}

void addNumTaskBlockEmitted(u64 n) {
if (n > 0) {
_dirty = true;
_num_task_block_emitted += n;
}
}

void addNumTaskBlockSkippedSpanZero(u64 n) {
if (n > 0) {
_dirty = true;
_num_task_block_skipped_span_zero += n;
}
}

void addNumTaskBlockSkippedTooShort(u64 n) {
if (n > 0) {
_dirty = true;
_num_task_block_skipped_too_short += n;
}
}

void endEpoch(u64 millis) { _duration_millis = millis; }

void clean() { _dirty = false; }

// Reset the accumulator fields at emit boundaries so each emitted epoch
// reports only the activity that occurred since the previous emit.
// _num_samplable_threads is a snapshot (current count), not an accumulator,
// and is intentionally preserved across newEpoch() so steady-state thread
// counts continue to collapse via the != check in updateNumSamplableThreads.
void newEpoch(u64 start_time) {
_dirty = false;
_start_time = start_time;
_num_successful_samples = 0;
_num_failed_samples = 0;
_num_exited_threads = 0;
_num_permission_denied = 0;
_num_suppressed_sampled_run = 0;
_num_task_block_emitted = 0;
_num_task_block_skipped_span_zero = 0;
_num_task_block_skipped_too_short = 0;
}
};

Expand All @@ -184,4 +245,13 @@ typedef struct QueueTimeEvent {
u32 _queueLength;
} QueueTimeEvent;

typedef struct TaskBlockEvent {
u64 _start;
u64 _end;
u64 _blocker;
u64 _unblockingSpanId;
/** Span IDs and tag encodings for JFR (park exit uses snapshot from park enter). */
Context _ctx;
} TaskBlockEvent;

#endif // _EVENT_H
66 changes: 66 additions & 0 deletions ddprof-lib/src/main/cpp/flightRecorder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,10 @@ void Recording::recordWallClockEpoch(Buffer *buf, WallClockEpochEvent *event) {
buf->putVar64(event->_num_failed_samples);
buf->putVar64(event->_num_exited_threads);
buf->putVar64(event->_num_permission_denied);
buf->putVar64(event->_num_suppressed_sampled_run);
buf->putVar64(event->_num_task_block_emitted);
buf->putVar64(event->_num_task_block_skipped_span_zero);
buf->putVar64(event->_num_task_block_skipped_too_short);
writeEventSizePrefix(buf, start);
flushIfNeeded(buf);
}
Expand Down Expand Up @@ -1570,6 +1574,39 @@ void Recording::recordQueueTime(Buffer *buf, int tid, QueueTimeEvent *event) {
flushIfNeeded(buf);
}

void Recording::recordTaskBlockLive(Buffer *buf, int tid, TaskBlockEvent *event) {
// Live capture: TaskBlock-specific fields first, then the standard context block via
// writeCurrentContext — same shape as recordQueueTime.
int start = buf->skip(1);
buf->putVar64(T_TASK_BLOCK);
buf->putVar64(event->_start);
buf->putVar64(event->_end - event->_start);
buf->putVar64(tid);
buf->putVar64(event->_blocker);
buf->putVar64(event->_unblockingSpanId);
writeCurrentContext(buf);
writeEventSizePrefix(buf, start);
flushIfNeeded(buf);
}

void Recording::recordTaskBlockDeferred(Buffer *buf, int tid,
TaskBlockEvent *event) {
// Deferred capture: context was snapshotted earlier (parkEnter / ObjectWaitEnter from
// JVMTI MonitorWait) and is emitted from event->_ctx via writeContextSnapshot — same
// on-disk layout as the live path, distinct only in the source of the spanId/rootSpanId/tag
// triple.
int start = buf->skip(1);
buf->putVar64(T_TASK_BLOCK);
buf->putVar64(event->_start);
buf->putVar64(event->_end - event->_start);
buf->putVar64(tid);
buf->putVar64(event->_blocker);
buf->putVar64(event->_unblockingSpanId);
writeContextSnapshot(buf, event->_ctx);
writeEventSizePrefix(buf, start);
flushIfNeeded(buf);
}

void Recording::recordAllocation(RecordingBuffer *buf, int tid,
u64 call_trace_id, AllocEvent *event) {
int start = buf->skip(1);
Expand Down Expand Up @@ -1772,6 +1809,7 @@ void FlightRecorder::recordTraceRoot(int lock_index, int tid,
Recording* rec = _rec;
if (rec != nullptr) {
Buffer *buf = rec->buffer(lock_index);
rec->addThread(lock_index, tid);
rec->recordTraceRoot(buf, tid, event);
}
}
Expand All @@ -1784,11 +1822,39 @@ void FlightRecorder::recordQueueTime(int lock_index, int tid,
Recording* rec = _rec;
if (rec != nullptr) {
Buffer *buf = rec->buffer(lock_index);
rec->addThread(lock_index, tid);
rec->addThread(lock_index, event->_origin);
rec->recordQueueTime(buf, tid, event);
}
}
}

void FlightRecorder::recordTaskBlockLive(int lock_index, int tid,
TaskBlockEvent *event) {
OptionalSharedLockGuard locker(&_rec_lock);
if (locker.ownsLock()) {
Recording* rec = _rec;
if (rec != nullptr) {
Buffer *buf = rec->buffer(lock_index);
rec->addThread(lock_index, tid);
rec->recordTaskBlockLive(buf, tid, event);
}
}
}

void FlightRecorder::recordTaskBlockDeferred(int lock_index, int tid,
TaskBlockEvent *event) {
OptionalSharedLockGuard locker(&_rec_lock);
if (locker.ownsLock()) {
Recording* rec = _rec;
if (rec != nullptr) {
Buffer *buf = rec->buffer(lock_index);
rec->addThread(lock_index, tid);
rec->recordTaskBlockDeferred(buf, tid, event);
}
}
}

void FlightRecorder::recordDatadogSetting(int lock_index, int length,
const char *name, const char *value,
const char *unit) {
Expand Down
4 changes: 4 additions & 0 deletions ddprof-lib/src/main/cpp/flightRecorder.h
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ class Recording {
void recordWallClockEpoch(Buffer *buf, WallClockEpochEvent *event);
void recordTraceRoot(Buffer *buf, int tid, TraceRootEvent *event);
void recordQueueTime(Buffer *buf, int tid, QueueTimeEvent *event);
void recordTaskBlockLive(Buffer *buf, int tid, TaskBlockEvent *event);
void recordTaskBlockDeferred(Buffer *buf, int tid, TaskBlockEvent *event);
void recordAllocation(RecordingBuffer *buf, int tid, u64 call_trace_id,
AllocEvent *event);
void recordMallocSample(Buffer *buf, int tid, u64 call_trace_id,
Expand Down Expand Up @@ -347,6 +349,8 @@ class FlightRecorder {
void wallClockEpoch(int lock_index, WallClockEpochEvent *event);
void recordTraceRoot(int lock_index, int tid, TraceRootEvent *event);
void recordQueueTime(int lock_index, int tid, QueueTimeEvent *event);
void recordTaskBlockLive(int lock_index, int tid, TaskBlockEvent *event);
void recordTaskBlockDeferred(int lock_index, int tid, TaskBlockEvent *event);

bool active() const { return _rec != NULL; }

Expand Down
9 changes: 9 additions & 0 deletions ddprof-lib/src/main/cpp/hotspot/vmStructs.inline.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,21 @@

#include "hotspot/vmStructs.h"
#include "jvmThread.h"
#include "vmEntry.h"

VMThread* VMThread::current() {
// JVMThread::current() is the native thread self pointer. On OpenJ9/Zing it
// is not a HotSpot JavaThread*; only HotSpot may reinterpret it as VMThread*.
if (!VM::isHotspot() || JVMThread::current() == nullptr) {
return nullptr;
}
Comment on lines +15 to +19
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.

These changes are coincidental, right?

return VMThread::cast(JVMThread::current());
}

VMThread* VMThread::fromJavaThread(JNIEnv* env, jthread thread) {
if (!VM::isHotspot()) {
return nullptr;
}
assert(_eetop != nullptr);
if (_eetop != nullptr) {
jlong eetop = env->GetLongField(thread, _eetop);
Expand Down
Loading