From 830893457ca4b23fa3ac16ac21fcb64f41fc172f Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Fri, 24 Apr 2026 10:50:31 +0000 Subject: [PATCH 1/3] Implement option to use JVMTI/JFR stack-walker for CPU- and wall-clock-profiler --- ddprof-lib/src/main/cpp/arguments.cpp | 18 ++++ ddprof-lib/src/main/cpp/arguments.h | 4 +- ddprof-lib/src/main/cpp/counters.h | 7 +- ddprof-lib/src/main/cpp/ctimer.h | 21 +++- ddprof-lib/src/main/cpp/ctimer_linux.cpp | 81 +++++++++++++++ ddprof-lib/src/main/cpp/flightRecorder.cpp | 35 ++++++- ddprof-lib/src/main/cpp/flightRecorder.h | 11 +- ddprof-lib/src/main/cpp/jfrMetadata.cpp | 2 + ddprof-lib/src/main/cpp/profiler.cpp | 85 +++++++++++++++ ddprof-lib/src/main/cpp/profiler.h | 12 ++- ddprof-lib/src/main/cpp/vmEntry.cpp | 45 ++++++++ ddprof-lib/src/main/cpp/vmEntry.h | 13 +++ ddprof-lib/src/main/cpp/wallClock.cpp | 115 +++++++++++++++++++++ ddprof-lib/src/main/cpp/wallClock.h | 21 ++++ 14 files changed, 462 insertions(+), 8 deletions(-) diff --git a/ddprof-lib/src/main/cpp/arguments.cpp b/ddprof-lib/src/main/cpp/arguments.cpp index 708263e54..8571c94fe 100644 --- a/ddprof-lib/src/main/cpp/arguments.cpp +++ b/ddprof-lib/src/main/cpp/arguments.cpp @@ -364,6 +364,24 @@ Error Arguments::parse(const char *args) { _remote_symbolication = true; } + CASE("jvmtistacks") + if (value != NULL) { + switch (value[0]) { + case 'n': // no + case 'f': // false + case '0': // 0 + _jvmtistacks = false; + break; + case 'y': // yes + case 't': // true + case '1': // 1 + default: + _jvmtistacks = true; + } + } else { + _jvmtistacks = true; + } + CASE("wallsampler") if (value != NULL) { switch (value[0]) { diff --git a/ddprof-lib/src/main/cpp/arguments.h b/ddprof-lib/src/main/cpp/arguments.h index 3698cf73d..787653bc7 100644 --- a/ddprof-lib/src/main/cpp/arguments.h +++ b/ddprof-lib/src/main/cpp/arguments.h @@ -190,6 +190,7 @@ class Arguments { bool _lightweight; bool _enable_method_cleanup; bool _remote_symbolication; // Enable remote symbolication for native frames + bool _jvmtistacks; // Delegate CPU/wall stack walks to HotSpot JFR RequestStackTrace extension Arguments(bool persistent = false) : _buf(NULL), @@ -224,7 +225,8 @@ class Arguments { _context_attributes({}), _lightweight(false), _enable_method_cleanup(true), - _remote_symbolication(false) {} + _remote_symbolication(false), + _jvmtistacks(false) {} ~Arguments(); diff --git a/ddprof-lib/src/main/cpp/counters.h b/ddprof-lib/src/main/cpp/counters.h index 8e0636fe4..5905d7e29 100644 --- a/ddprof-lib/src/main/cpp/counters.h +++ b/ddprof-lib/src/main/cpp/counters.h @@ -98,7 +98,12 @@ X(WALKVM_CONT_ENTRY_NULL, "walkvm_cont_entry_null") \ X(NATIVE_LIBS_DROPPED, "native_libs_dropped") \ X(SIGACTION_PATCHED_LIBS, "sigaction_patched_libs") \ - X(SIGACTION_INTERCEPTED, "sigaction_intercepted") + X(SIGACTION_INTERCEPTED, "sigaction_intercepted") \ + X(JVMTI_STACKS_INIT_OK, "jvmti_stacks_init_ok") \ + X(JVMTI_STACKS_INIT_FAILED, "jvmti_stacks_init_failed") \ + X(JVMTI_STACKS_REQUESTED, "jvmti_stacks_requested") \ + X(JVMTI_STACKS_FAILED_WRONG_PHASE, "jvmti_stacks_failed_wrong_phase") \ + X(JVMTI_STACKS_FAILED_OTHER, "jvmti_stacks_failed_other") #define X_ENUM(a, b) a, typedef enum CounterId : int { DD_COUNTER_TABLE(X_ENUM) DD_NUM_COUNTERS diff --git a/ddprof-lib/src/main/cpp/ctimer.h b/ddprof-lib/src/main/cpp/ctimer.h index 0f5340b2d..b0c6584e0 100644 --- a/ddprof-lib/src/main/cpp/ctimer.h +++ b/ddprof-lib/src/main/cpp/ctimer.h @@ -25,7 +25,7 @@ #include class CTimer : public Engine { -private: +protected: // This is accessed from signal handlers, so must be async-signal-safe static bool _enabled; static long _interval; @@ -38,6 +38,7 @@ class CTimer : public Engine { int registerThread(int tid); void unregisterThread(int tid); +private: // cppcheck-suppress unusedPrivateFunction static void signalHandler(int signo, siginfo_t *siginfo, void *ucontext); @@ -60,6 +61,24 @@ class CTimer : public Engine { static int getSignal() { return _signal; } }; +// A CPU-time engine that reuses CTimer's per-thread timer_create / SIGPROF +// dispatch, but instead of walking the stack in the signal handler delegates +// the walk to HotSpot's JFR RequestStackTrace JVMTI extension. The sampled +// event is emitted on our side with only a correlation ID; the JVM writes +// the stack trace (and its own JFR stack-trace id) into the concurrent JFR +// recording as jdk.AsyncStackTrace. See VM::canRequestStackTrace(). +class CTimerJvmti : public CTimer { +private: + // cppcheck-suppress unusedPrivateFunction + static void signalHandler(int signo, siginfo_t *siginfo, void *ucontext); + +public: + const char *name() { return "CTimerJvmti"; } + + Error check(Arguments &args); + Error start(Arguments &args); +}; + #else class CTimer : public Engine { diff --git a/ddprof-lib/src/main/cpp/ctimer_linux.cpp b/ddprof-lib/src/main/cpp/ctimer_linux.cpp index c951a4388..061cafaaf 100644 --- a/ddprof-lib/src/main/cpp/ctimer_linux.cpp +++ b/ddprof-lib/src/main/cpp/ctimer_linux.cpp @@ -142,6 +142,87 @@ void CTimer::stop() { } } +Error CTimerJvmti::check(Arguments &args) { + if (!VM::canRequestStackTrace()) { + return Error("HotSpot RequestStackTrace JVMTI extension not available"); + } + return CTimer::check(args); +} + +Error CTimerJvmti::start(Arguments &args) { + if (!VM::canRequestStackTrace()) { + return Error("HotSpot RequestStackTrace JVMTI extension not available"); + } + if (args._interval < 0) { + return Error("interval must be positive"); + } + + _interval = args.cpuSamplerInterval(); + _cstack = args._cstack; + _signal = SIGPROF; + + int max_timers = OS::getMaxThreadId(); + if (max_timers != _max_timers) { + free(_timers); + _timers = (int *)calloc(max_timers, sizeof(int)); + _max_timers = max_timers; + } + + OS::installSignalHandler(_signal, CTimerJvmti::signalHandler); + + Error result = Error::OK; + ThreadList *thread_list = OS::listThreads(); + while (thread_list->hasNext()) { + int tid = thread_list->next(); + int err = registerThread(tid); + if (err != 0) { + result = Error("Failed to register thread"); + } + } + delete thread_list; + + return Error::OK; +} + +void CTimerJvmti::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) { + CriticalSection cs; + if (!cs.entered()) { + return; + } + int saved_errno = errno; + if (!__atomic_load_n(&_enabled, __ATOMIC_ACQUIRE)) { + errno = saved_errno; + return; + } + int tid = 0; + ProfiledThread *current = ProfiledThread::currentSignalSafe(); + assert(current == nullptr || !current->isDeepCrashHandler()); + if (current != nullptr && JVMThread::isInitialized() && JVMThread::current() == nullptr + && current->inInitWindow()) { + current->tickInitWindow(); + errno = saved_errno; + return; + } + if (current != NULL) { + current->noteCPUSample(Profiler::instance()->recordingEpoch()); + tid = current->tid(); + } else { + tid = OS::threadId(); + } + Shims::instance().setSighandlerTid(tid); + + ExecutionEvent event; + event._execution_mode = getThreadExecutionMode(); + // Opted into JVMTI delegation; drop the sample if the JVM rejects the + // request (WRONG_PHASE if JFR is not recording, NOT_AVAILABLE if + // jdk.AsyncStackTrace is disabled). recordSampleDelegated() bumps the + // failure counters; there is no fallback to ASGCT in this engine. + (void)Profiler::instance()->recordSampleDelegated(ucontext, _interval, tid, + BCI_CPU, &event); + Shims::instance().setSighandlerTid(-1); + errno = saved_errno; +} + void CTimer::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) { // Atomically try to enter critical section - prevents all reentrancy races CriticalSection cs; diff --git a/ddprof-lib/src/main/cpp/flightRecorder.cpp b/ddprof-lib/src/main/cpp/flightRecorder.cpp index eeaa3f32d..804eabe82 100644 --- a/ddprof-lib/src/main/cpp/flightRecorder.cpp +++ b/ddprof-lib/src/main/cpp/flightRecorder.cpp @@ -1486,6 +1486,7 @@ void Recording::writeEventSizePrefix(Buffer *buf, int start) { } void Recording::recordExecutionSample(Buffer *buf, int tid, u64 call_trace_id, + u64 correlation_id, ExecutionEvent *event) { int start = buf->skip(1); buf->putVar64(T_EXECUTION_SAMPLE); @@ -1495,12 +1496,14 @@ void Recording::recordExecutionSample(Buffer *buf, int tid, u64 call_trace_id, buf->put8(static_cast(event->_thread_state)); buf->put8(static_cast(event->_execution_mode)); buf->putVar64(event->_weight); + buf->putVar64(correlation_id); writeCurrentContext(buf); writeEventSizePrefix(buf, start); flushIfNeeded(buf); } void Recording::recordMethodSample(Buffer *buf, int tid, u64 call_trace_id, + u64 correlation_id, ExecutionEvent *event) { int start = buf->skip(1); buf->putVar64(T_METHOD_SAMPLE); @@ -1510,6 +1513,7 @@ void Recording::recordMethodSample(Buffer *buf, int tid, u64 call_trace_id, buf->put8(static_cast(event->_thread_state)); buf->put8(static_cast(event->_execution_mode)); buf->putVar64(event->_weight); + buf->putVar64(correlation_id); writeCurrentContext(buf); writeEventSizePrefix(buf, start); flushIfNeeded(buf); @@ -1797,11 +1801,11 @@ void FlightRecorder::recordEvent(int lock_index, int tid, u64 call_trace_id, RecordingBuffer *buf = rec->buffer(lock_index); switch (event_type) { case BCI_CPU: - rec->recordExecutionSample(buf, tid, call_trace_id, + rec->recordExecutionSample(buf, tid, call_trace_id, 0, (ExecutionEvent *)event); break; case BCI_WALL: - rec->recordMethodSample(buf, tid, call_trace_id, + rec->recordMethodSample(buf, tid, call_trace_id, 0, (ExecutionEvent *)event); break; case BCI_ALLOC: @@ -1824,6 +1828,33 @@ void FlightRecorder::recordEvent(int lock_index, int tid, u64 call_trace_id, } } +void FlightRecorder::recordEventDelegated(int lock_index, int tid, + u64 correlation_id, int event_type, + Event *event) { + OptionalSharedLockGuard locker(&_rec_lock); + if (locker.ownsLock()) { + Recording* rec = _rec; + if (rec != nullptr) { + RecordingBuffer *buf = rec->buffer(lock_index); + switch (event_type) { + case BCI_CPU: + rec->recordExecutionSample(buf, tid, 0, correlation_id, + (ExecutionEvent *)event); + break; + case BCI_WALL: + rec->recordMethodSample(buf, tid, 0, correlation_id, + (ExecutionEvent *)event); + break; + default: + // Delegation is only wired for CPU/wall samples in v1. + return; + } + rec->flushIfNeeded(buf); + rec->addThread(lock_index, tid); + } + } +} + void FlightRecorder::recordLog(LogLevel level, const char *message, size_t len) { OptionalSharedLockGuard locker(&_rec_lock); diff --git a/ddprof-lib/src/main/cpp/flightRecorder.h b/ddprof-lib/src/main/cpp/flightRecorder.h index 02efdebc0..e62ab62d9 100644 --- a/ddprof-lib/src/main/cpp/flightRecorder.h +++ b/ddprof-lib/src/main/cpp/flightRecorder.h @@ -273,9 +273,9 @@ class Recording { void writeCurrentContext(Buffer *buf); void recordExecutionSample(Buffer *buf, int tid, u64 call_trace_id, - ExecutionEvent *event); + u64 correlation_id, ExecutionEvent *event); void recordMethodSample(Buffer *buf, int tid, u64 call_trace_id, - ExecutionEvent *event); + u64 correlation_id, ExecutionEvent *event); void recordWallClockEpoch(Buffer *buf, WallClockEpochEvent *event); void recordTraceRoot(Buffer *buf, int tid, TraceRootEvent *event); void recordQueueTime(Buffer *buf, int tid, QueueTimeEvent *event); @@ -351,6 +351,13 @@ class FlightRecorder { void recordEvent(int lock_index, int tid, u64 call_trace_id, int event_type, Event *event); + // Emit a BCI_CPU / BCI_WALL sample with no stack-trace attached to our + // recording. `correlation_id` is the same jlong passed to the HotSpot + // RequestStackTrace extension so downstream tooling can join our event with + // the JVM-emitted jdk.AsyncStackTrace. + void recordEventDelegated(int lock_index, int tid, u64 correlation_id, + int event_type, Event *event); + void recordLog(LogLevel level, const char *message, size_t len); void recordDatadogSetting(int lock_index, int length, const char *name, diff --git a/ddprof-lib/src/main/cpp/jfrMetadata.cpp b/ddprof-lib/src/main/cpp/jfrMetadata.cpp index 127f99c87..2de808db7 100644 --- a/ddprof-lib/src/main/cpp/jfrMetadata.cpp +++ b/ddprof-lib/src/main/cpp/jfrMetadata.cpp @@ -123,6 +123,7 @@ void JfrMetadata::initialize( << field("state", T_THREAD_STATE, "Thread State", F_CPOOL) << field("mode", T_EXECUTION_MODE, "Execution Mode", F_CPOOL) << field("weight", T_LONG, "Sample weight") + << field("correlationId", T_LONG, "Async Stack Trace Correlation ID") << field("spanId", T_LONG, "Span ID") << field("localRootSpanId", T_LONG, "Local Root Span ID") || contextAttributes) @@ -136,6 +137,7 @@ void JfrMetadata::initialize( << field("state", T_THREAD_STATE, "Thread State", F_CPOOL) << field("mode", T_EXECUTION_MODE, "Execution Mode", F_CPOOL) << field("weight", T_LONG, "Sample weight") + << field("correlationId", T_LONG, "Async Stack Trace Correlation ID") << field("spanId", T_LONG, "Span ID") << field("localRootSpanId", T_LONG, "Local Root Span ID") || contextAttributes) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index c552a8101..93e19d169 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -61,9 +61,11 @@ static void (*orig_busHandler)(int signo, siginfo_t *siginfo, void *ucontext); static Engine noop_engine; static PerfEvents perf_events; static WallClockASGCT wall_asgct_engine; +static WallClockJvmti wall_jvmti_engine; static J9WallClock j9_engine; static ITimer itimer; static CTimer ctimer; +static CTimerJvmti ctimer_jvmti; void Profiler::onThreadStart(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread) { ProfiledThread::initCurrentThread(); @@ -594,6 +596,45 @@ void Profiler::recordSample(void *ucontext, u64 counter, int tid, _locks[lock_index].unlock(); } +bool Profiler::recordSampleDelegated(void *ucontext, u64 weight, int tid, + jint event_type, Event *event) { + if (!VM::canRequestStackTrace()) { + return false; + } + + // Reserve the correlation ID up-front so we can pass the same value to the + // JVM (as user_data) and to our own event. Zero is reserved as "no + // correlation" on the wire, so we always start from 1. + u64 correlation_id = atomicInc(_sample_seq) + 1; + + Counters::increment(JVMTI_STACKS_REQUESTED); + jvmtiError rc = VM::requestStackTrace(ucontext, (jlong)correlation_id); + if (rc != JVMTI_ERROR_NONE) { + if (rc == JVMTI_ERROR_WRONG_PHASE) { + Counters::increment(JVMTI_STACKS_FAILED_WRONG_PHASE); + } else { + Counters::increment(JVMTI_STACKS_FAILED_OTHER); + } + return false; + } + + atomicIncRelaxed(_total_samples); + u32 lock_index = getLockIndex(tid); + if (!_locks[lock_index].tryLock() && + !_locks[lock_index = (lock_index + 1) % CONCURRENCY_LEVEL].tryLock() && + !_locks[lock_index = (lock_index + 2) % CONCURRENCY_LEVEL].tryLock()) { + atomicIncRelaxed(_failures[-ticks_skipped]); + // The JVM-side stack trace request is already in flight; we just drop our + // sample event. The dangling AsyncStackTrace entry in the JVM recording + // will simply have no matching datadog event, which is harmless. + return true; + } + + _jfr.recordEventDelegated(lock_index, tid, correlation_id, event_type, event); + _locks[lock_index].unlock(); + return true; +} + void Profiler::recordWallClockEpoch(int tid, WallClockEpochEvent *event) { u32 lock_index = getLockIndex(tid); if (!_locks[lock_index].tryLock() && @@ -941,6 +982,12 @@ Engine *Profiler::selectCpuEngine(Arguments &args) { } TEST_LOG("J9[cpu]=asgct"); } + // Prefer the JVMTI JFR-delegated engine when the HotSpot extension is + // available and the user opted into jvmtistacks. + if (args._jvmtistacks && !ctimer_jvmti.check(args)) { + TEST_LOG("HS[cpu]=ctimer_jvmti"); + return &ctimer_jvmti; + } return !ctimer.check(args) ? (Engine *)&ctimer : (!perf_events.check(args) ? (Engine *)&perf_events @@ -976,6 +1023,14 @@ Engine *Profiler::selectWallEngine(Arguments &args) { return (Engine *)&wall_asgct_engine; } } + // Prefer the JVMTI JFR-delegated engine when the HotSpot extension is + // available and the user opted into jvmtistacks. The engine-level + // _wallclock_sampler knob still takes precedence for users who explicitly + // request JVMTI/ASGCT. + if (args._jvmtistacks && VM::canRequestStackTrace()) { + TEST_LOG("HS[wall]=jvmti"); + return (Engine *)&wall_jvmti_engine; + } switch (args._wallclock_sampler) { case JVMTI: fprintf(stderr, "[ddprof] [WARN] JVMTI wallclock is not available on this JVM, fallback to ASGCT wallclock\n"); @@ -1067,6 +1122,7 @@ Error Profiler::start(Arguments &args, bool reset) { if (reset || _start_time == 0) { // Reset counters _total_samples = 0; + _sample_seq = 0; memset(_failures, 0, sizeof(_failures)); // Reset dictionaries and bitmaps @@ -1263,6 +1319,35 @@ Error Profiler::stop() { updateJavaThreadNames(); updateNativeThreadNames(); + // If jvmtistacks delegation was used this recording, surface likely + // misconfigurations. The JVM returns WRONG_PHASE when JFR is not recording + // and NOT_AVAILABLE when JFR is recording but the AsyncStackTrace event is + // disabled. If the request was accepted the JVM will have written the + // stack trace, so no warning is needed. + if (VM::canRequestStackTrace()) { + long long requested = + Counters::getCounter(JVMTI_STACKS_REQUESTED); + long long wrong_phase = + Counters::getCounter(JVMTI_STACKS_FAILED_WRONG_PHASE); + long long other = + Counters::getCounter(JVMTI_STACKS_FAILED_OTHER); + if (requested > 0 && wrong_phase * 2 >= requested) { + fprintf(stderr, + "[java-profiler] jvmtistacks: %lld of %lld stack-trace requests " + "were rejected with WRONG_PHASE, so no async stack traces were " + "emitted by the JVM. Start JFR (e.g. " + "-XX:StartFlightRecording=...) before or as the profiler starts.\n", + wrong_phase, requested); + } else if (requested > 0 && other * 2 >= requested) { + fprintf(stderr, + "[java-profiler] jvmtistacks: %lld of %lld stack-trace requests " + "were rejected with NOT_AVAILABLE. The jdk.AsyncStackTrace event " + "is likely disabled; enable it in the JFR configuration, e.g. " + "-XX:StartFlightRecording=...,+jdk.AsyncStackTrace#enabled=true.\n", + other, requested); + } + } + // writing these out before stopping the JFR recording allows to report the // correct counts in the recording _thread_info.reportCounters(); diff --git a/ddprof-lib/src/main/cpp/profiler.h b/ddprof-lib/src/main/cpp/profiler.h index d9dc28ac7..2fc5acecd 100644 --- a/ddprof-lib/src/main/cpp/profiler.h +++ b/ddprof-lib/src/main/cpp/profiler.h @@ -97,6 +97,7 @@ class alignas(alignof(SpinLock)) Profiler { void *_timer_id; volatile u64 _total_samples; + volatile u64 _sample_seq; u64 _failures[ASGCT_FAILURE_TYPES]; SpinLock _class_map_lock; @@ -155,7 +156,7 @@ class alignas(alignof(SpinLock)) Profiler { _call_trace_storage(), _jfr(), _cpu_engine(NULL), _wall_engine(NULL), _alloc_engine(NULL), _event_mask(0), _start_time(0), _stop_time(0), _epoch(0), _timer_id(NULL), - _total_samples(0), _failures(), _class_map_lock(), + _total_samples(0), _sample_seq(0), _failures(), _class_map_lock(), _max_stack_depth(0), _features(), _safe_mode(0), _cstack(CSTACK_NO), _thread_events_state(JVMTI_DISABLE), _libs(Libraries::instance()), _num_context_attributes(0), _omit_stacktraces(false), @@ -329,6 +330,15 @@ class alignas(alignof(SpinLock)) Profiler { ASGCT_CallFrame *frames, int lock_index); void recordSample(void *ucontext, u64 weight, int tid, jint event_type, u64 call_trace_id, Event *event); + // Delegated sample path: stack-walking is performed by the HotSpot JFR + // RequestStackTrace extension (the JVM emits the stack trace into its own + // JFR recording). We only emit the CPU/wall sample event with no + // stack-trace reference, tagged by the correlation ID we passed to + // RequestStackTrace as user_data. Returns true if the request was accepted + // by the JVM (JVMTI_ERROR_NONE) and the sample event was recorded, false + // otherwise; on false the caller may fall back to a direct walk. + bool recordSampleDelegated(void *ucontext, u64 weight, int tid, + jint event_type, Event *event); u64 recordJVMTISample(u64 weight, int tid, jthread thread, jint event_type, Event *event, bool deferred); void recordDeferredSample(int tid, u64 call_trace_id, jint event_type, Event *event); void recordExternalSample(u64 weight, int tid, int num_frames, diff --git a/ddprof-lib/src/main/cpp/vmEntry.cpp b/ddprof-lib/src/main/cpp/vmEntry.cpp index 76fd49c68..88c0bf880 100644 --- a/ddprof-lib/src/main/cpp/vmEntry.cpp +++ b/ddprof-lib/src/main/cpp/vmEntry.cpp @@ -8,6 +8,7 @@ #include "vmEntry.h" #include "arguments.h" #include "context.h" +#include "counters.h" #include "j9/j9Support.h" #include "jniHelper.h" #include "jvmThread.h" @@ -42,6 +43,10 @@ bool VM::_can_sample_objects = false; bool VM::_can_intercept_binding = false; bool VM::_is_adaptive_gc_boundary_flag_set = false; +jvmtiExtensionFunction VM::_request_stack_trace = nullptr; +jvmtiExtensionFunction VM::_init_request_stack_trace = nullptr; +bool VM::_request_stack_trace_initialized = false; + jvmtiError(JNICALL *VM::_orig_RedefineClasses)(jvmtiEnv *, jint, const jvmtiClassDefinition *); jvmtiError(JNICALL *VM::_orig_RetransformClasses)(jvmtiEnv *, jint, @@ -426,6 +431,46 @@ bool VM::initProfilerBridge(JavaVM *vm, bool attach) { _jvmti->AddCapabilities(&capabilities); + // Detect the HotSpot JFR async stack-trace JVMTI extension. Present on JDK 27+ + // builds that expose the functions "com.sun.hotspot.functions.RequestStackTrace" + // and "com.sun.hotspot.functions.InitializeRequestStackTrace". If the user asked + // for jvmtistacks and both are present, initialise now (the JDK requires this + // to happen during Agent_OnLoad). After initialisation, signal handlers may + // delegate stack walks via VM::requestStackTrace(). + if (_hotspot) { + jint ext_count = 0; + jvmtiExtensionFunctionInfo *ext_functions = nullptr; + if (_jvmti->GetExtensionFunctions(&ext_count, &ext_functions) == 0) { + for (jint i = 0; i < ext_count; i++) { + if (strcmp(ext_functions[i].id, + "com.sun.hotspot.functions.RequestStackTrace") == 0) { + _request_stack_trace = ext_functions[i].func; + } else if (strcmp(ext_functions[i].id, + "com.sun.hotspot.functions.InitializeRequestStackTrace") == 0) { + _init_request_stack_trace = ext_functions[i].func; + } + } + _jvmti->Deallocate((unsigned char *)ext_functions); + } + + if (_agent_args._jvmtistacks) { + if (_request_stack_trace != nullptr && _init_request_stack_trace != nullptr) { + jvmtiError rc = _init_request_stack_trace(_jvmti); + if (rc == JVMTI_ERROR_NONE) { + _request_stack_trace_initialized = true; + Counters::increment(JVMTI_STACKS_INIT_OK); + } else { + Log::warn("InitializeRequestStackTrace failed: %d", rc); + Counters::increment(JVMTI_STACKS_INIT_FAILED); + } + } else { + Log::warn("jvmtistacks requested but HotSpot RequestStackTrace extension " + "is not available on this JVM"); + Counters::increment(JVMTI_STACKS_INIT_FAILED); + } + } + } + jvmtiEventCallbacks callbacks = {0}; callbacks.VMInit = VMInit; callbacks.VMDeath = VMDeath; diff --git a/ddprof-lib/src/main/cpp/vmEntry.h b/ddprof-lib/src/main/cpp/vmEntry.h index 322436558..6e707f4a3 100644 --- a/ddprof-lib/src/main/cpp/vmEntry.h +++ b/ddprof-lib/src/main/cpp/vmEntry.h @@ -127,6 +127,11 @@ class VM { static bool _can_intercept_binding; static bool _is_adaptive_gc_boundary_flag_set; + // HotSpot JFR async stack-trace extension (optional, JDK 27+). + static jvmtiExtensionFunction _request_stack_trace; + static jvmtiExtensionFunction _init_request_stack_trace; + static bool _request_stack_trace_initialized; + static jvmtiError(JNICALL *_orig_RedefineClasses)( jvmtiEnv *, jint, const jvmtiClassDefinition *); static jvmtiError(JNICALL *_orig_RetransformClasses)(jvmtiEnv *, jint, @@ -189,6 +194,14 @@ class VM { return _is_adaptive_gc_boundary_flag_set; } + static bool canRequestStackTrace() { + return _request_stack_trace != nullptr && _request_stack_trace_initialized; + } + + static jvmtiError requestStackTrace(void* ucontext, jlong user_data) { + return _request_stack_trace(_jvmti, (jthread)nullptr, ucontext, user_data); + } + static void JNICALL VMInit(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread); static void JNICALL VMDeath(jvmtiEnv *jvmti, JNIEnv *jni); diff --git a/ddprof-lib/src/main/cpp/wallClock.cpp b/ddprof-lib/src/main/cpp/wallClock.cpp index 1f8982eda..f95529836 100644 --- a/ddprof-lib/src/main/cpp/wallClock.cpp +++ b/ddprof-lib/src/main/cpp/wallClock.cpp @@ -197,3 +197,118 @@ void WallClockASGCT::timerLoop() { timerLoopCommon(collectThreads, sampleThreads, doNothing, _reservoir_size, _interval); } + +// WallClockJvmti: mirrors WallClockASGCT's dispatch, but the signal handler +// delegates the stack walk to HotSpot's JFR RequestStackTrace JVMTI extension +// instead of invoking ASGCT. Used only when VM::canRequestStackTrace() is true +// and the profiler has opted into jvmtistacks. + +bool WallClockJvmti::inSyscall(void *ucontext) { + StackFrame frame(ucontext); + uintptr_t pc = frame.pc(); + + if (StackFrame::isSyscall((instruction_t *)pc)) { + return true; + } + + uintptr_t prev_pc = pc - SYSCALL_SIZE; + if ((pc & 0xfff) >= SYSCALL_SIZE || + Libraries::instance()->findLibraryByAddress((instruction_t *)prev_pc) != + NULL) { + if (StackFrame::isSyscall((instruction_t *)prev_pc) && + frame.checkInterruptedSyscall()) { + return true; + } + } + + return false; +} + +void WallClockJvmti::sharedSignalHandler(int signo, siginfo_t *siginfo, + void *ucontext) { + WallClockJvmti *engine = + reinterpret_cast(Profiler::instance()->wallEngine()); + if (signo == SIGVTALRM) { + engine->signalHandler(signo, siginfo, ucontext, engine->_interval); + } +} + +void WallClockJvmti::signalHandler(int signo, siginfo_t *siginfo, + void *ucontext, u64 last_sample) { + CriticalSection cs; + if (!cs.entered()) { + return; + } + ProfiledThread *current = ProfiledThread::currentSignalSafe(); + if (current != nullptr && JVMThread::isInitialized() && JVMThread::current() == nullptr + && current->inInitWindow()) { + current->tickInitWindow(); + return; + } + int tid = current != NULL ? current->tid() : OS::threadId(); + Shims::instance().setSighandlerTid(tid); + + ExecutionEvent event; + OSThreadState state = getOSThreadState(); + ExecutionMode mode = getThreadExecutionMode(); + if (state == OSThreadState::UNKNOWN) { + if (inSyscall(ucontext)) { + state = OSThreadState::SYSCALL; + mode = ExecutionMode::SYSCALL; + } else { + state = OSThreadState::RUNNABLE; + } + } + event._thread_state = state; + event._execution_mode = mode; + event._weight = 1; + // Delegate the stack walk to the JVM. On rejection we drop the sample; + // recordSampleDelegated() increments the failure counters. + (void)Profiler::instance()->recordSampleDelegated(ucontext, last_sample, tid, + BCI_WALL, &event); + Shims::instance().setSighandlerTid(-1); +} + +void WallClockJvmti::initialize(Arguments &args) { + OS::installSignalHandler(SIGVTALRM, sharedSignalHandler); +} + +void WallClockJvmti::timerLoop() { + auto collectThreads = [&](std::vector &tids) { + if (Profiler::instance()->threadFilter()->enabled()) { + Profiler::instance()->threadFilter()->collect(tids); + } else { + ThreadList *thread_list = OS::listThreads(); + while (thread_list->hasNext()) { + int tid = thread_list->next(); + if (tid != OS::threadId()) { + tids.push_back(tid); + } + } + delete thread_list; + } + }; + + auto sampleThreads = [&](int tid, int &num_failures, + int &threads_already_exited, int &permission_denied) { + if (!OS::sendSignalToThread(tid, SIGVTALRM)) { + num_failures++; + if (errno != 0) { + if (errno == ESRCH) { + threads_already_exited++; + } else if (errno == EPERM) { + permission_denied++; + } else { + Log::debug("unexpected error %s", strerror(errno)); + } + } + return false; + } + return true; + }; + + auto doNothing = []() {}; + + timerLoopCommon(collectThreads, sampleThreads, doNothing, + _reservoir_size, _interval); +} diff --git a/ddprof-lib/src/main/cpp/wallClock.h b/ddprof-lib/src/main/cpp/wallClock.h index 89338fbaf..38a46d7fb 100644 --- a/ddprof-lib/src/main/cpp/wallClock.h +++ b/ddprof-lib/src/main/cpp/wallClock.h @@ -157,4 +157,25 @@ class WallClockASGCT : public BaseWallClock { } }; +// Wall-clock engine that uses BaseWallClock's pthread reservoir sampling loop +// to signal target threads, but in its signal handler delegates the stack walk +// to HotSpot's JFR RequestStackTrace JVMTI extension. Requires +// VM::canRequestStackTrace(). +class WallClockJvmti : public BaseWallClock { + private: + static bool inSyscall(void* ucontext); + + static void sharedSignalHandler(int signo, siginfo_t* siginfo, void* ucontext); + void signalHandler(int signo, siginfo_t* siginfo, void* ucontext, u64 last_sample); + + void initialize(Arguments& args) override; + void timerLoop() override; + + public: + WallClockJvmti() : BaseWallClock() {} + const char* name() override { + return "WallClock (JVMTI)"; + } +}; + #endif // _WALLCLOCK_H From 045bf6316ce4f9ce43a9fa88c7ae1bdc06dc5579 Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Mon, 27 Apr 2026 13:34:36 +0200 Subject: [PATCH 2/3] MacOS parts --- ddprof-lib/src/main/cpp/ctimer.h | 13 ++++ ddprof-lib/src/main/cpp/hotspot/vmStructs.h | 2 +- ddprof-lib/src/main/cpp/itimer.cpp | 68 +++++++++++++++++++++ ddprof-lib/src/main/cpp/itimer.h | 28 +++++++++ ddprof-lib/src/main/cpp/profiler.cpp | 17 ++++-- ddprof-lib/src/main/cpp/wallClock.cpp | 7 ++- 6 files changed, 127 insertions(+), 8 deletions(-) diff --git a/ddprof-lib/src/main/cpp/ctimer.h b/ddprof-lib/src/main/cpp/ctimer.h index b0c6584e0..ac39e860c 100644 --- a/ddprof-lib/src/main/cpp/ctimer.h +++ b/ddprof-lib/src/main/cpp/ctimer.h @@ -94,6 +94,19 @@ class CTimer : public Engine { static bool supported() { return false; } }; +class CTimerJvmti : public Engine { +public: + const char *name() { return "CTimerJvmti"; } + + Error check(Arguments &args) { + return Error("CTimerJvmti is not supported on this platform"); + } + + Error start(Arguments &args) { + return Error("CTimerJvmti is not supported on this platform"); + } +}; + #endif // __linux__ #endif // _CTIMER_H \ No newline at end of file diff --git a/ddprof-lib/src/main/cpp/hotspot/vmStructs.h b/ddprof-lib/src/main/cpp/hotspot/vmStructs.h index bd4d66efd..13aea46af 100644 --- a/ddprof-lib/src/main/cpp/hotspot/vmStructs.h +++ b/ddprof-lib/src/main/cpp/hotspot/vmStructs.h @@ -300,7 +300,7 @@ typedef void* address; field(_flag_type_offset, offset, MATCH_SYMBOLS("_type", "type")) \ type_end() \ type_begin(VMOop, MATCH_SYMBOLS("oopDesc")) \ - field(_oop_klass_offset, offset, MATCH_SYMBOLS("_metadata._klass")) \ + field(_oop_klass_offset, offset, MATCH_SYMBOLS("_metadata._klass", "_compressed_klass")) \ type_end() \ type_begin(VMUniverse, MATCH_SYMBOLS("Universe", "CompressedKlassPointers")) \ field(_narrow_klass_base_addr, address, MATCH_SYMBOLS("_narrow_klass._base", "_base")) \ diff --git a/ddprof-lib/src/main/cpp/itimer.cpp b/ddprof-lib/src/main/cpp/itimer.cpp index f4e052206..38d13893b 100644 --- a/ddprof-lib/src/main/cpp/itimer.cpp +++ b/ddprof-lib/src/main/cpp/itimer.cpp @@ -17,6 +17,7 @@ #include "itimer.h" #include "debugSupport.h" +#include "jvmThread.h" #include "os.h" #include "profiler.h" #include "stackWalker.h" @@ -90,3 +91,70 @@ void ITimer::stop() { struct itimerval tv = {{0, 0}, {0, 0}}; setitimer(ITIMER_PROF, &tv, NULL); } + +volatile bool ITimerJvmti::_enabled = false; +long ITimerJvmti::_interval = 0; + +void ITimerJvmti::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) { + CriticalSection cs; + if (!cs.entered()) { + return; + } + int saved_errno = errno; + if (!__atomic_load_n(&_enabled, __ATOMIC_ACQUIRE)) { + errno = saved_errno; + return; + } + ProfiledThread *current = ProfiledThread::currentSignalSafe(); + if (current != nullptr && JVMThread::isInitialized() && JVMThread::current() == nullptr + && current->inInitWindow()) { + current->tickInitWindow(); + errno = saved_errno; + return; + } + int tid = current ? current->tid() : OS::threadId(); + if (current) { + current->noteCPUSample(Profiler::instance()->recordingEpoch()); + } + Shims::instance().setSighandlerTid(tid); + + ExecutionEvent event; + event._execution_mode = getThreadExecutionMode(); + // setitimer(ITIMER_PROF) delivers SIGPROF to an arbitrary thread chosen by + // the OS, so ucontext may be from a JVM-internal thread. Pass nullptr to + // force the JVM into safepoint-based stack walking instead. + (void)Profiler::instance()->recordSampleDelegated(nullptr, _interval, tid, + BCI_CPU, &event); + Shims::instance().setSighandlerTid(-1); + errno = saved_errno; +} + +Error ITimerJvmti::check(Arguments &args) { + if (!VM::canRequestStackTrace()) { + return Error("HotSpot RequestStackTrace JVMTI extension not available"); + } + return Error::OK; +} + +Error ITimerJvmti::start(Arguments &args) { + if (args._interval < 0) { + return Error("interval must be positive"); + } + _interval = args.cpuSamplerInterval(); + + OS::installSignalHandler(SIGPROF, signalHandler); + + time_t sec = _interval / 1000000000; + suseconds_t usec = (_interval % 1000000000) / 1000; + struct itimerval tv = {{sec, usec}, {sec, usec}}; + if (setitimer(ITIMER_PROF, &tv, nullptr) != 0) { + return Error("ITIMER_PROF is not supported on this system"); + } + return Error::OK; +} + +void ITimerJvmti::stop() { + struct itimerval tv = {}; + setitimer(ITIMER_PROF, &tv, nullptr); + OS::installSignalHandler(SIGPROF, nullptr, SIG_IGN); +} diff --git a/ddprof-lib/src/main/cpp/itimer.h b/ddprof-lib/src/main/cpp/itimer.h index 51f47280b..2a216e28f 100644 --- a/ddprof-lib/src/main/cpp/itimer.h +++ b/ddprof-lib/src/main/cpp/itimer.h @@ -42,4 +42,32 @@ class ITimer : public Engine { inline void enableEvents(bool enabled) { _enabled = enabled; } }; +// CPU-time engine identical to ITimer in its timer mechanism (process-wide +// setitimer(ITIMER_PROF) / SIGPROF) but delegates stack collection to the +// HotSpot JFR RequestStackTrace JVMTI extension instead of ASGCT. Used on +// platforms where per-thread CPU timers are unavailable (e.g. macOS), as a +// macOS-compatible alternative to CTimerJvmti. Because SIGPROF may land on +// any thread, nullptr is passed as ucontext so the JVM uses safepoint-based +// stack walking rather than relying on the signal-frame PC. +class ITimerJvmti : public Engine { +private: + static volatile bool _enabled; + static long _interval; + + static void signalHandler(int signo, siginfo_t *siginfo, void *ucontext); + +public: + const char *units() { return "ns"; } + const char *name() { return "ITimerJvmti"; } + long interval() const { return _interval; } + + Error check(Arguments &args); + Error start(Arguments &args); + void stop(); + + inline void enableEvents(bool enabled) { + __atomic_store_n(&_enabled, enabled, __ATOMIC_RELEASE); + } +}; + #endif // _ITIMER_H diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 93e19d169..25feeffd6 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -64,6 +64,7 @@ static WallClockASGCT wall_asgct_engine; static WallClockJvmti wall_jvmti_engine; static J9WallClock j9_engine; static ITimer itimer; +static ITimerJvmti itimer_jvmti; static CTimer ctimer; static CTimerJvmti ctimer_jvmti; @@ -983,10 +984,18 @@ Engine *Profiler::selectCpuEngine(Arguments &args) { TEST_LOG("J9[cpu]=asgct"); } // Prefer the JVMTI JFR-delegated engine when the HotSpot extension is - // available and the user opted into jvmtistacks. - if (args._jvmtistacks && !ctimer_jvmti.check(args)) { - TEST_LOG("HS[cpu]=ctimer_jvmti"); - return &ctimer_jvmti; + // available and the user opted into jvmtistacks. On Linux, CTimerJvmti + // uses per-thread CPU timers. On other platforms (e.g. macOS) it is not + // supported, so fall back to ITimerJvmti which uses setitimer(ITIMER_PROF). + if (args._jvmtistacks) { + if (!ctimer_jvmti.check(args)) { + TEST_LOG("HS[cpu]=ctimer_jvmti"); + return &ctimer_jvmti; + } + if (!itimer_jvmti.check(args)) { + TEST_LOG("HS[cpu]=itimer_jvmti"); + return &itimer_jvmti; + } } return !ctimer.check(args) ? (Engine *)&ctimer diff --git a/ddprof-lib/src/main/cpp/wallClock.cpp b/ddprof-lib/src/main/cpp/wallClock.cpp index f95529836..bc6fea7ca 100644 --- a/ddprof-lib/src/main/cpp/wallClock.cpp +++ b/ddprof-lib/src/main/cpp/wallClock.cpp @@ -262,9 +262,10 @@ void WallClockJvmti::signalHandler(int signo, siginfo_t *siginfo, event._thread_state = state; event._execution_mode = mode; event._weight = 1; - // Delegate the stack walk to the JVM. On rejection we drop the sample; - // recordSampleDelegated() increments the failure counters. - (void)Profiler::instance()->recordSampleDelegated(ucontext, last_sample, tid, + // Pass nullptr ucontext so the JVM uses safepoint-based stack walking. + // Passing the signal-frame PC causes the extension to reject samples where + // the thread is currently inside JVM-internal (non-Java) code. + (void)Profiler::instance()->recordSampleDelegated(nullptr, last_sample, tid, BCI_WALL, &event); Shims::instance().setSighandlerTid(-1); } From 57ab35fed9c42cff7bb79ec7ea19469e3681b837 Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Tue, 28 Apr 2026 13:15:02 +0200 Subject: [PATCH 3/3] Extract JVMTI stack-walker feature detection into separate method --- ddprof-lib/src/main/cpp/vmEntry.cpp | 72 ++++++++++++++--------------- ddprof-lib/src/main/cpp/vmEntry.h | 1 + 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/ddprof-lib/src/main/cpp/vmEntry.cpp b/ddprof-lib/src/main/cpp/vmEntry.cpp index 88c0bf880..08cbe5d0d 100644 --- a/ddprof-lib/src/main/cpp/vmEntry.cpp +++ b/ddprof-lib/src/main/cpp/vmEntry.cpp @@ -380,6 +380,40 @@ bool VM::initLibrary(JavaVM *vm) { return true; } +void VM::probeJFRRequestStackTrace() { + jint ext_count = 0; + jvmtiExtensionFunctionInfo *ext_functions = nullptr; + if (_jvmti->GetExtensionFunctions(&ext_count, &ext_functions) == 0) { + for (jint i = 0; i < ext_count; i++) { + if (strcmp(ext_functions[i].id, + "com.sun.hotspot.functions.RequestStackTrace") == 0) { + _request_stack_trace = ext_functions[i].func; + } else if (strcmp(ext_functions[i].id, + "com.sun.hotspot.functions.InitializeRequestStackTrace") == 0) { + _init_request_stack_trace = ext_functions[i].func; + } + } + _jvmti->Deallocate((unsigned char *)ext_functions); + } + + if (_agent_args._jvmtistacks) { + if (_request_stack_trace != nullptr && _init_request_stack_trace != nullptr) { + jvmtiError rc = _init_request_stack_trace(_jvmti); + if (rc == JVMTI_ERROR_NONE) { + _request_stack_trace_initialized = true; + Counters::increment(JVMTI_STACKS_INIT_OK); + } else { + Log::warn("InitializeRequestStackTrace failed: %d", rc); + Counters::increment(JVMTI_STACKS_INIT_FAILED); + } + } else { + Log::warn("jvmtistacks requested but HotSpot RequestStackTrace extension " + "is not available on this JVM"); + Counters::increment(JVMTI_STACKS_INIT_FAILED); + } + } +} + bool VM::initProfilerBridge(JavaVM *vm, bool attach) { TEST_LOG("VM::initProfilerBridge"); if (!initShared(vm)) { @@ -431,44 +465,8 @@ bool VM::initProfilerBridge(JavaVM *vm, bool attach) { _jvmti->AddCapabilities(&capabilities); - // Detect the HotSpot JFR async stack-trace JVMTI extension. Present on JDK 27+ - // builds that expose the functions "com.sun.hotspot.functions.RequestStackTrace" - // and "com.sun.hotspot.functions.InitializeRequestStackTrace". If the user asked - // for jvmtistacks and both are present, initialise now (the JDK requires this - // to happen during Agent_OnLoad). After initialisation, signal handlers may - // delegate stack walks via VM::requestStackTrace(). if (_hotspot) { - jint ext_count = 0; - jvmtiExtensionFunctionInfo *ext_functions = nullptr; - if (_jvmti->GetExtensionFunctions(&ext_count, &ext_functions) == 0) { - for (jint i = 0; i < ext_count; i++) { - if (strcmp(ext_functions[i].id, - "com.sun.hotspot.functions.RequestStackTrace") == 0) { - _request_stack_trace = ext_functions[i].func; - } else if (strcmp(ext_functions[i].id, - "com.sun.hotspot.functions.InitializeRequestStackTrace") == 0) { - _init_request_stack_trace = ext_functions[i].func; - } - } - _jvmti->Deallocate((unsigned char *)ext_functions); - } - - if (_agent_args._jvmtistacks) { - if (_request_stack_trace != nullptr && _init_request_stack_trace != nullptr) { - jvmtiError rc = _init_request_stack_trace(_jvmti); - if (rc == JVMTI_ERROR_NONE) { - _request_stack_trace_initialized = true; - Counters::increment(JVMTI_STACKS_INIT_OK); - } else { - Log::warn("InitializeRequestStackTrace failed: %d", rc); - Counters::increment(JVMTI_STACKS_INIT_FAILED); - } - } else { - Log::warn("jvmtistacks requested but HotSpot RequestStackTrace extension " - "is not available on this JVM"); - Counters::increment(JVMTI_STACKS_INIT_FAILED); - } - } + probeJFRRequestStackTrace(); } jvmtiEventCallbacks callbacks = {0}; diff --git a/ddprof-lib/src/main/cpp/vmEntry.h b/ddprof-lib/src/main/cpp/vmEntry.h index 6e707f4a3..254f39529 100644 --- a/ddprof-lib/src/main/cpp/vmEntry.h +++ b/ddprof-lib/src/main/cpp/vmEntry.h @@ -144,6 +144,7 @@ class VM { static void loadAllMethodIDs(jvmtiEnv *jvmti, JNIEnv *jni); static bool initShared(JavaVM *vm); + static void probeJFRRequestStackTrace(); static CodeCache* openJvmLibrary();