Skip to content

add early thread-state check and TaskBlock emission for wall-clock#502

Open
kaahos wants to merge 43 commits into
mainfrom
paul.fournillon/wallclock_precheck
Open

add early thread-state check and TaskBlock emission for wall-clock#502
kaahos wants to merge 43 commits into
mainfrom
paul.fournillon/wallclock_precheck

Conversation

@kaahos
Copy link
Copy Markdown
Contributor

@kaahos kaahos commented Apr 24, 2026

What does this PR do?:
Improves wall-clock sampling by adding an early thread-state check so we avoid full sampling work when threads are in cheap-to-detect idle states (e.g. sleeping / condition-variable wait). Adds TLS-based park enter/exit handling so parked threads can be treated efficiently during wall-clock sampling. Introduces TaskBlock recording for blocked intervals. Centralizes TaskBlock eligibility, extends wall-clock counters and datadog.WallClockSamplingEpoch JFR fields for observability.

Motivation:
Wall-clock sampling should spend less work on threads that are idle or parked while still surfacing blocking time as TaskBlock events for analysis. This aligns native behavior with tracer instrumentation and improves visibility through counters and epoch metrics.

Additional Notes:

How to test the change?:

./gradlew :ddprof-lib:gtestDebug_park_state_ut
./gradlew :ddprof-test:testDebug -Ptests=PrecheckTest
./gradlew :ddprof-test:testDebug -Ptests=PrecheckEfficiencyTest
./gradlew :ddprof-test:testDebug -Ptests=ParkTaskBlockTest
./gradlew :ddprof-test:testDebug -Ptests=WallclockMitigationsCombinedTest

For Datadog employees:

  • If this PR touches code that signs or publishes builds or packages, or handles credentials of any kind, I've requested a review from @DataDog/security-design-and-guidance.
  • This PR doesn't touch any of that.
  • JIRA: [JIRA-14534]

Unsure? Have a question? Request a review!

@dd-octo-sts
Copy link
Copy Markdown
Contributor

dd-octo-sts Bot commented Apr 24, 2026

CI Test Results

Run: #25802629146 | Commit: 8836c4e | Duration: 31m 44s (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-05-13 14:17:54 UTC

@kaahos kaahos force-pushed the paul.fournillon/wallclock_precheck branch 6 times, most recently from da37425 to 06136e1 Compare April 27, 2026 08:55
@kaahos kaahos force-pushed the paul.fournillon/wallclock_precheck branch 3 times, most recently from 1e61333 to 4523b4a Compare April 28, 2026 15:45
@kaahos kaahos force-pushed the paul.fournillon/wallclock_precheck branch 3 times, most recently from 0ad95a4 to 3e231a0 Compare April 28, 2026 21:51
@kaahos kaahos force-pushed the paul.fournillon/wallclock_precheck branch from 3e231a0 to 28856f4 Compare April 29, 2026 09:19
@kaahos kaahos force-pushed the paul.fournillon/wallclock_precheck branch from d71b9e2 to 24fec34 Compare April 30, 2026 09:10
@kaahos kaahos self-assigned this Apr 30, 2026
@kaahos kaahos force-pushed the paul.fournillon/wallclock_precheck branch from 24fec34 to 81c7712 Compare April 30, 2026 09:54
Comment on lines +1585 to +1586
buf->putVar64(event->_ctx.spanId);
buf->putVar64(event->_ctx.rootSpanId);
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.

We already have the generic way of capturing context. It would be better to use that instead.
It will also take care of the custom attribute, simplifying the code here.

Comment thread ddprof-lib/src/main/cpp/javaApi.cpp Outdated
Comment on lines +331 to +333
Context context = ContextApi::snapshot();
context.spanId = (u64)spanId;
context.rootSpanId = (u64)rootSpanId;
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.

The same - let's just use the generic way of getting and storing context in the events.

Comment on lines +220 to +226
// Tracker settings are sticky across recordings. Preserve the historical
// table/config behavior, but allow HeapUsage recording to be enabled later
// (e.g. if an earlier test initialized liveness without ':L'). Once heap usage
// recording has been requested for this JVM lifetime, keep emitting HeapUsage
// even if a later recording omits the flag — avoids dropping JVM heap telemetry
// when liveness was turned on first without ':L'.
_record_heap_usage = _record_heap_usage || args._record_heap_usage;
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 seems to be not really related (although valid) code change

Comment thread doc/reference/EventTypeSystem.md Outdated
- **`datadog.WallClockSamplingEpoch`** (`T_WALLCLOCK_SAMPLE_EPOCH`) — timer aggregation snapshot;
appended numeric fields must remain backward-compatible for parsers that tolerate trailing varints.

**JVMTI**: `MonitorWait` / `MonitorWaited` are registered only for `java_version() < 21`
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.

For Java 21+ there are no such callbacks?

Comment thread doc/architecture/TLSContext.md Outdated
Comment on lines +503 to +526
## Wall-clock park TLS, TaskBlock, and OS-thread precheck

### Park TLS (`ProfiledThread`)

`parkEnter` / `parkExit` track Java-level `LockSupport` park intervals with span context
snapshots for wall-clock collapsing and TaskBlock emission. The parked flag suppresses
wall-clock signals on filtered threads when enabled.

### `wallprecheck` (default `false`, opt-in)

Disabled by default. When explicitly enabled with `wallprecheck=true` *and* thread filtering
is on (`Arguments::_wall_precheck == true`), the wall-clock timer samples **HotSpot**
`VMThread::osThreadState()` **before** sending `SIGVTALRM`. Slots must hold a non-null
`VMThread*` from registration (`ThreadFilter::setVMThread`) for the precheck to run;
otherwise only Java park-flag suppression applies.

On JDK **21+**, `Thread.sleep` often maps to **CONDVAR_WAIT** (not only legacy **SLEEPING**),
same broad category as condvar-based park — see `PrecheckTest` and `wallClock.cpp`.

### JVMTI `MonitorWait` / TaskBlock (JDK before 21)

JVMTI `MonitorWait` / `MonitorWaited` callbacks are registered only when
`java_version() < 21` (`vmEntry.cpp`). They populate TaskBlock with a **blocker** value
matching **`System.identityHashCode(waitedObject)`** (same convention as the JNI park path).
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 does not seem to be the right place. This is about the TLS context implementation.

Comment on lines +41 to +44
// wallprecheck=false: this test exercises thread filter + JFR thread identity, not sleep
// suppression (see PrecheckTest). With wallprecheck=true and VMThread set on the filter
// slot, a thread sleeping the whole window receives no wall signals.
return "wall=~1ms,filter=0,wallprecheck=false";
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 should not be necessary - wallprecheck is false by default, right?

Comment thread ddprof-lib/src/main/cpp/wallClock.h Outdated

public:
WallClockASGCT() : BaseWallClock(), _collapsing(false) {}
WallClockASGCT() : BaseWallClock(), _collapsing(false), _precheck(true) {}
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.

Constructor default for _precheck is inverted

WallClockASGCT() initialises _precheck to true, but Arguments._wall_precheck defaults to false. If initialize(args) is ever skipped (or a caller constructs the engine without calling it), precheck is silently active, suppressing SIGVTALRM for sleeping/parked threads without user opt-in — contrary to the documented opt-in semantics.

Consider matching the Arguments default:

Suggested change
WallClockASGCT() : BaseWallClock(), _collapsing(false), _precheck(true) {}
WallClockASGCT() : BaseWallClock(), _collapsing(false), _precheck(false) {}

int slot_tid = slot.value.load(std::memory_order_acquire);
if (slot_tid != -1) {
VMThread* vm = slot.vm_thread.load(std::memory_order_acquire);
ProfiledThread* pt = slot.profiled_thread.load(std::memory_order_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.

profiled_thread is collected but never consumed

collectWithState performs an atomic acquire load of ProfiledThread* into every ThreadEntry, but no caller in the timer path (sampleThreads, wallClock.cpp) dereferences it. Carrying a raw pointer to a non-lifetime-guaranteed object through the sampling loop is a footgun — a future change that consumes entry.profiled_thread will race with ProfiledThread::release()/delete unless every site adds a lifetime guard.

Either remove the field from ThreadEntry (and the corresponding load) until there is a concrete consumer, or document the lifetime contract on the field declaration so future users add a null check before dereferencing.

Comment thread ddprof-lib/src/main/cpp/thread.h Outdated
: ParkedWallClockState::PARKED_SPANLESS;
}

inline void monitorWaitEnter(u64 start_ticks, const Context& context, u64 blocker) {
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.

Misleading comment: _monitor_wait_active is not read from the wall-clock path

The comment claims the wall-clock path reads _monitor_wait_* fields, but the signal handler in wallClock.cpp never reads any of them. Both MonitorWait and MonitorWaited JVMTI callbacks fire on the affected (waited) thread itself, so there is no cross-thread access pattern here. Leaving the comment in place implies a memory-ordering constraint that doesn't exist and could mislead future contributors into adding unnecessary synchronisation.

Consider updating it to reflect the actual access pattern, e.g.:

Written and read only on the waited thread via JVMTI MonitorWait/MonitorWaited callbacks; no cross-thread access.


void WallClockASGCT::initialize(Arguments& args) {
_collapsing = args._wall_collapsing;
_precheck = args._wall_precheck;
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.

Initialization-ordering reliance is implicit

_precheck = args._wall_precheck correctly mirrors the requested setting, but the constructor's default of true (see wallClock.h) means there is a window between construction and initialize(args) where the field is enabled. Today this is benign because start() calls pthread_create() only after initialize() returns, but the contract is undocumented — any future code that runs the timer loop or invokes sampleThreads before initialize() will silently observe the wrong default.

No code change required if the existing initialize() → start() order is guaranteed, but a brief comment on initialize() ("must be called before start()/timer-loop entry; otherwise _precheck is unset") would lock the invariant in.

@jbachorik
Copy link
Copy Markdown
Collaborator

Nit: Either change _num_skipped_precheck_os to u64 to match putVar64, or use putVar32 consistent with the T_INT JFR metadata declaration.

@Override
protected String getProfilerCommand() {
return "wall=1ms";
return "wall=1ms,wallprecheck=false";
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.

Not really necessary, I assume

Comment on lines +15 to +19
// 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;
}
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?

@jbachorik
Copy link
Copy Markdown
Collaborator

I did the first pass. I have some concerns about the basics - let's take it off-line and decide how to proceed. Until then you should not spend time on addressing my inline comments.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants