From 5b27e9c2df8b386b38b0553d941469cd8aa65c28 Mon Sep 17 00:00:00 2001 From: Johannes Bechberger Date: Wed, 4 Jun 2025 22:08:58 +0000 Subject: [PATCH] 8342818: Implement JEP 509: JFR CPU-Time Profiling Reviewed-by: mgronlun, mdoerr, pchilanomate, apangin, shade --- src/hotspot/os/posix/signals_posix.cpp | 8 + src/hotspot/os/posix/signals_posix.hpp | 2 + src/hotspot/share/jfr/jfr.inline.hpp | 3 +- src/hotspot/share/jfr/jni/jfrJniMethod.cpp | 6 + src/hotspot/share/jfr/jni/jfrJniMethod.hpp | 2 + .../jfr/jni/jfrJniMethodRegistration.cpp | 1 + src/hotspot/share/jfr/metadata/metadata.xml | 16 + .../sampling/jfrCPUTimeThreadSampler.cpp | 769 ++++++++++++++++++ .../sampling/jfrCPUTimeThreadSampler.hpp | 152 ++++ .../periodic/sampling/jfrSampleRequest.cpp | 33 +- .../periodic/sampling/jfrSampleRequest.hpp | 5 + .../periodic/sampling/jfrThreadSampling.cpp | 92 ++- .../periodic/sampling/jfrThreadSampling.hpp | 2 + .../share/jfr/recorder/jfrRecorder.cpp | 15 + .../share/jfr/recorder/jfrRecorder.hpp | 1 + .../recorder/service/jfrEventThrottler.cpp | 12 +- .../share/jfr/support/jfrThreadLocal.cpp | 98 ++- .../share/jfr/support/jfrThreadLocal.hpp | 53 ++ src/hotspot/share/runtime/thread.hpp | 1 + src/hotspot/share/runtime/vmOperation.hpp | 2 + src/hotspot/share/utilities/ticks.hpp | 1 + .../jdk/jfr/internal/EventControl.java | 4 + .../share/classes/jdk/jfr/internal/JVM.java | 10 + .../jdk/jfr/internal/PlatformEventType.java | 19 + .../classes/jdk/jfr/internal/query/view.ini | 25 +- .../internal/settings/CPUThrottleSetting.java | 89 ++ .../classes/jdk/jfr/internal/util/Rate.java | 4 + .../jdk/jfr/internal/util/TimespanRate.java | 68 ++ src/jdk.jfr/share/conf/jfr/default.jfc | 10 + src/jdk.jfr/share/conf/jfr/profile.jfc | 10 + .../metadata/TestLookForUntestedEvents.java | 9 +- .../profiling/BaseTestFullStackTrace.java | 176 ++++ .../TestCPUTimeAndExecutionSample.java | 65 ++ .../TestCPUTimeSampleFullStackTrace.java | 41 + .../TestCPUTimeSampleMultipleRecordings.java | 70 ++ .../profiling/TestCPUTimeSampleNative.java | 66 ++ .../TestCPUTimeSampleThrottling.java | 111 +++ .../TestCPUTimeSamplingLongPeriod.java | 58 ++ .../event/profiling/TestFullStackTrace.java | 131 +-- .../classes/test/RecursiveMethods.java | 76 ++ test/lib/jdk/test/lib/jfr/EventNames.java | 2 + 41 files changed, 2178 insertions(+), 140 deletions(-) create mode 100644 src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.cpp create mode 100644 src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.hpp create mode 100644 src/jdk.jfr/share/classes/jdk/jfr/internal/settings/CPUThrottleSetting.java create mode 100644 src/jdk.jfr/share/classes/jdk/jfr/internal/util/TimespanRate.java create mode 100644 test/jdk/jdk/jfr/event/profiling/BaseTestFullStackTrace.java create mode 100644 test/jdk/jdk/jfr/event/profiling/TestCPUTimeAndExecutionSample.java create mode 100644 test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleFullStackTrace.java create mode 100644 test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleMultipleRecordings.java create mode 100644 test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleNative.java create mode 100644 test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleThrottling.java create mode 100644 test/jdk/jdk/jfr/event/profiling/TestCPUTimeSamplingLongPeriod.java create mode 100644 test/jdk/jdk/jfr/event/profiling/classes/test/RecursiveMethods.java diff --git a/src/hotspot/os/posix/signals_posix.cpp b/src/hotspot/os/posix/signals_posix.cpp index e900d5695ae..0157d354f40 100644 --- a/src/hotspot/os/posix/signals_posix.cpp +++ b/src/hotspot/os/posix/signals_posix.cpp @@ -1505,6 +1505,14 @@ bool PosixSignals::is_sig_ignored(int sig) { } } +void* PosixSignals::get_signal_handler_for_signal(int sig) { + struct sigaction oact; + if (sigaction(sig, (struct sigaction*)nullptr, &oact) == -1) { + return nullptr; + } + return get_signal_handler(&oact); +} + static void signal_sets_init() { sigemptyset(&preinstalled_sigs); diff --git a/src/hotspot/os/posix/signals_posix.hpp b/src/hotspot/os/posix/signals_posix.hpp index 9deade4db18..c1cb70df153 100644 --- a/src/hotspot/os/posix/signals_posix.hpp +++ b/src/hotspot/os/posix/signals_posix.hpp @@ -52,6 +52,8 @@ public: static bool is_sig_ignored(int sig); + static void* get_signal_handler_for_signal(int sig); + static void hotspot_sigmask(Thread* thread); static void print_signal_handler(outputStream* st, int sig, char* buf, size_t buflen); diff --git a/src/hotspot/share/jfr/jfr.inline.hpp b/src/hotspot/share/jfr/jfr.inline.hpp index bdb47f600e6..5b6fc62d0ea 100644 --- a/src/hotspot/share/jfr/jfr.inline.hpp +++ b/src/hotspot/share/jfr/jfr.inline.hpp @@ -32,7 +32,8 @@ inline bool Jfr::has_sample_request(JavaThread* jt) { assert(jt != nullptr, "invariant"); - return jt->jfr_thread_local()->has_sample_request(); + JfrThreadLocal* tl = jt->jfr_thread_local(); + return tl->has_sample_request() || tl->has_cpu_time_jfr_requests(); } inline void Jfr::check_and_process_sample_request(JavaThread* jt) { diff --git a/src/hotspot/share/jfr/jni/jfrJniMethod.cpp b/src/hotspot/share/jfr/jni/jfrJniMethod.cpp index 6f1c1936574..bc2412a90c1 100644 --- a/src/hotspot/share/jfr/jni/jfrJniMethod.cpp +++ b/src/hotspot/share/jfr/jni/jfrJniMethod.cpp @@ -24,6 +24,7 @@ #include "jfr/jfr.hpp" #include "jfr/jfrEvents.hpp" +#include "jfr/periodic/sampling/jfrCPUTimeThreadSampler.hpp" #include "jfr/periodic/sampling/jfrThreadSampler.hpp" #include "jfr/recorder/jfrEventSetting.hpp" #include "jfr/recorder/jfrRecorder.hpp" @@ -169,6 +170,11 @@ NO_TRANSITION(jboolean, jfr_set_throttle(JNIEnv* env, jclass jvm, jlong event_ty return JNI_TRUE; NO_TRANSITION_END +JVM_ENTRY_NO_ENV(void, jfr_set_cpu_throttle(JNIEnv* env, jclass jvm, jdouble rate, jboolean auto_adapt)) + JfrEventSetting::set_enabled(JfrCPUTimeSampleEvent, rate > 0); + JfrCPUTimeThreadSampling::set_rate(rate, auto_adapt == JNI_TRUE); +JVM_END + NO_TRANSITION(void, jfr_set_miscellaneous(JNIEnv* env, jclass jvm, jlong event_type_id, jlong value)) JfrEventSetting::set_miscellaneous(event_type_id, value); const JfrEventId typed_event_id = (JfrEventId)event_type_id; diff --git a/src/hotspot/share/jfr/jni/jfrJniMethod.hpp b/src/hotspot/share/jfr/jni/jfrJniMethod.hpp index 9c78c6239d4..dbea7f0180d 100644 --- a/src/hotspot/share/jfr/jni/jfrJniMethod.hpp +++ b/src/hotspot/share/jfr/jni/jfrJniMethod.hpp @@ -129,6 +129,8 @@ jlong JNICALL jfr_get_unloaded_event_classes_count(JNIEnv* env, jclass jvm); jboolean JNICALL jfr_set_throttle(JNIEnv* env, jclass jvm, jlong event_type_id, jlong event_sample_size, jlong period_ms); +void JNICALL jfr_set_cpu_throttle(JNIEnv* env, jclass jvm, jdouble rate, jboolean auto_adapt); + void JNICALL jfr_set_miscellaneous(JNIEnv* env, jclass jvm, jlong id, jlong value); void JNICALL jfr_emit_old_object_samples(JNIEnv* env, jclass jvm, jlong cutoff_ticks, jboolean, jboolean); diff --git a/src/hotspot/share/jfr/jni/jfrJniMethodRegistration.cpp b/src/hotspot/share/jfr/jni/jfrJniMethodRegistration.cpp index 33a564dee2f..82ef93d95b2 100644 --- a/src/hotspot/share/jfr/jni/jfrJniMethodRegistration.cpp +++ b/src/hotspot/share/jfr/jni/jfrJniMethodRegistration.cpp @@ -83,6 +83,7 @@ JfrJniMethodRegistration::JfrJniMethodRegistration(JNIEnv* env) { (char*)"getUnloadedEventClassCount", (char*)"()J", (void*)jfr_get_unloaded_event_classes_count, (char*)"setMiscellaneous", (char*)"(JJ)V", (void*)jfr_set_miscellaneous, (char*)"setThrottle", (char*)"(JJJ)Z", (void*)jfr_set_throttle, + (char*)"setCPUThrottle", (char*)"(DZ)V", (void*)jfr_set_cpu_throttle, (char*)"emitOldObjectSamples", (char*)"(JZZ)V", (void*)jfr_emit_old_object_samples, (char*)"shouldRotateDisk", (char*)"()Z", (void*)jfr_should_rotate_disk, (char*)"exclude", (char*)"(Ljava/lang/Thread;)V", (void*)jfr_exclude_thread, diff --git a/src/hotspot/share/jfr/metadata/metadata.xml b/src/hotspot/share/jfr/metadata/metadata.xml index 9c04ec3dca1..03daca946f6 100644 --- a/src/hotspot/share/jfr/metadata/metadata.xml +++ b/src/hotspot/share/jfr/metadata/metadata.xml @@ -962,6 +962,22 @@ + + + + + + + + + + + + + diff --git a/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.cpp b/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.cpp new file mode 100644 index 00000000000..ae8877ee3f2 --- /dev/null +++ b/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.cpp @@ -0,0 +1,769 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +#include "jfr/periodic/sampling/jfrCPUTimeThreadSampler.hpp" +#include "logging/log.hpp" + + +#if defined(LINUX) +#include "jfr/periodic/sampling/jfrThreadSampling.hpp" +#include "jfr/support/jfrThreadLocal.hpp" +#include "jfr/utilities/jfrTime.hpp" +#include "jfr/utilities/jfrThreadIterator.hpp" +#include "jfr/utilities/jfrTypes.hpp" +#include "jfrfiles/jfrEventClasses.hpp" +#include "memory/resourceArea.hpp" +#include "runtime/atomic.hpp" +#include "runtime/javaThread.hpp" +#include "runtime/osThread.hpp" +#include "runtime/safepointMechanism.inline.hpp" +#include "runtime/threadSMR.hpp" +#include "runtime/vmOperation.hpp" +#include "runtime/vmThread.hpp" +#include "utilities/ticks.hpp" + +#include "signals_posix.hpp" + +static const int64_t AUTOADAPT_INTERVAL_MS = 100; + +static bool is_excluded(JavaThread* jt) { + return jt->is_hidden_from_external_view() || + jt->jfr_thread_local()->is_excluded() || + jt->is_JfrRecorder_thread(); +} + +static JavaThread* get_java_thread_if_valid() { + Thread* raw_thread = Thread::current_or_null_safe(); + if (raw_thread == nullptr) { + // probably while shutting down + return nullptr; + } + assert(raw_thread->is_Java_thread(), "invariant"); + JavaThread* jt = JavaThread::cast(raw_thread); + if (is_excluded(jt) || jt->is_exiting()) { + return nullptr; + } + return jt; +} + +JfrCPUTimeTraceQueue::JfrCPUTimeTraceQueue(u4 capacity) : + _capacity(capacity), _head(0), _lost_samples(0) { + _data = JfrCHeapObj::new_array(capacity); +} + +JfrCPUTimeTraceQueue::~JfrCPUTimeTraceQueue() { + JfrCHeapObj::free(_data, _capacity * sizeof(JfrCPUTimeSampleRequest)); +} + +bool JfrCPUTimeTraceQueue::enqueue(JfrCPUTimeSampleRequest& request) { + assert(JavaThread::current()->jfr_thread_local()->is_cpu_time_jfr_enqueue_locked(), "invariant"); + assert(&JavaThread::current()->jfr_thread_local()->cpu_time_jfr_queue() == this, "invariant"); + u4 elementIndex; + do { + elementIndex = Atomic::load_acquire(&_head); + if (elementIndex >= _capacity) { + return false; + } + } while (Atomic::cmpxchg(&_head, elementIndex, elementIndex + 1) != elementIndex); + _data[elementIndex] = request; + return true; +} + +JfrCPUTimeSampleRequest& JfrCPUTimeTraceQueue::at(u4 index) { + assert(index < _head, "invariant"); + return _data[index]; +} + +static volatile u4 _lost_samples_sum = 0; + +u4 JfrCPUTimeTraceQueue::size() const { + return Atomic::load_acquire(&_head); +} + +void JfrCPUTimeTraceQueue::set_size(u4 size) { + Atomic::release_store(&_head, size); +} + +u4 JfrCPUTimeTraceQueue::capacity() const { + return _capacity; +} + +void JfrCPUTimeTraceQueue::set_capacity(u4 capacity) { + _head = 0; + JfrCHeapObj::free(_data, _capacity * sizeof(JfrCPUTimeSampleRequest)); + _data = JfrCHeapObj::new_array(capacity); + _capacity = capacity; +} + +bool JfrCPUTimeTraceQueue::is_empty() const { + return Atomic::load_acquire(&_head) == 0; +} + +u4 JfrCPUTimeTraceQueue::lost_samples() const { + return Atomic::load(&_lost_samples); +} + +void JfrCPUTimeTraceQueue::increment_lost_samples() { + Atomic::inc(&_lost_samples_sum); + Atomic::inc(&_lost_samples); +} + +u4 JfrCPUTimeTraceQueue::get_and_reset_lost_samples() { + return Atomic::xchg(&_lost_samples, (u4)0); +} + +void JfrCPUTimeTraceQueue::resize(u4 capacity) { + if (capacity != _capacity) { + set_capacity(capacity); + } +} + +void JfrCPUTimeTraceQueue::resize_for_period(u4 period_millis) { + u4 capacity = CPU_TIME_QUEUE_CAPACITY; + if (period_millis > 0 && period_millis < 10) { + capacity = (u4) ((double) capacity * 10 / period_millis); + } + resize(capacity); +} + +void JfrCPUTimeTraceQueue::clear() { + Atomic::release_store(&_head, (u4)0); +} + +static int64_t compute_sampling_period(double rate) { + if (rate == 0) { + return 0; + } + return os::active_processor_count() * 1000000000.0 / rate; +} + +class JfrCPUSamplerThread : public NonJavaThread { + friend class JfrCPUTimeThreadSampling; + private: + Semaphore _sample; + NonJavaThread* _sampler_thread; + double _rate; + bool _auto_adapt; + volatile int64_t _current_sampling_period_ns; + volatile bool _disenrolled; + // top bit is used to indicate that no signal handler should proceed + volatile u4 _active_signal_handlers; + volatile bool _is_async_processing_of_cpu_time_jfr_requests_triggered; + volatile bool _warned_about_timer_creation_failure; + volatile bool _signal_handler_installed; + + static const u4 STOP_SIGNAL_BIT = 0x80000000; + + JfrCPUSamplerThread(double rate, bool auto_adapt); + + void start_thread(); + + void enroll(); + void disenroll(); + void update_all_thread_timers(); + + void auto_adapt_period_if_needed(); + + void set_rate(double rate, bool auto_adapt); + int64_t get_sampling_period() const { return Atomic::load(&_current_sampling_period_ns); }; + + void sample_thread(JfrSampleRequest& request, void* ucontext, JavaThread* jt, JfrThreadLocal* tl, JfrTicks& now); + + // process the queues for all threads that are in native state (and requested to be processed) + void stackwalk_threads_in_native(); + bool create_timer_for_thread(JavaThread* thread, timer_t &timerid); + + void stop_signal_handlers(); + + // returns false if the stop signal bit was set, true otherwise + bool increment_signal_handler_count(); + + void decrement_signal_handler_count(); + + void initialize_active_signal_handler_counter(); + +protected: + virtual void post_run(); +public: + virtual const char* name() const { return "JFR CPU Sampler Thread"; } + virtual const char* type_name() const { return "JfrCPUTimeSampler"; } + void run(); + void on_javathread_create(JavaThread* thread); + void on_javathread_terminate(JavaThread* thread); + + void handle_timer_signal(siginfo_t* info, void* context); + bool init_timers(); + void stop_timer(); + + void trigger_async_processing_of_cpu_time_jfr_requests(); +}; + +JfrCPUSamplerThread::JfrCPUSamplerThread(double rate, bool auto_adapt) : + _sample(), + _sampler_thread(nullptr), + _rate(rate), + _auto_adapt(auto_adapt), + _current_sampling_period_ns(compute_sampling_period(rate)), + _disenrolled(true), + _active_signal_handlers(STOP_SIGNAL_BIT), + _is_async_processing_of_cpu_time_jfr_requests_triggered(false), + _warned_about_timer_creation_failure(false), + _signal_handler_installed(false) { + assert(rate >= 0, "invariant"); +} + +void JfrCPUSamplerThread::trigger_async_processing_of_cpu_time_jfr_requests() { + Atomic::release_store(&_is_async_processing_of_cpu_time_jfr_requests_triggered, true); +} + +void JfrCPUSamplerThread::on_javathread_create(JavaThread* thread) { + if (thread->is_hidden_from_external_view() || thread->is_JfrRecorder_thread() || + !Atomic::load_acquire(&_signal_handler_installed)) { + return; + } + JfrThreadLocal* tl = thread->jfr_thread_local(); + assert(tl != nullptr, "invariant"); + tl->cpu_time_jfr_queue().resize_for_period(_current_sampling_period_ns / 1000000); + timer_t timerid; + if (create_timer_for_thread(thread, timerid)) { + tl->set_cpu_timer(&timerid); + } else { + if (!Atomic::or_then_fetch(&_warned_about_timer_creation_failure, true)) { + log_warning(jfr)("Failed to create timer for a thread"); + } + tl->deallocate_cpu_time_jfr_queue(); + } +} + +void JfrCPUSamplerThread::on_javathread_terminate(JavaThread* thread) { + JfrThreadLocal* tl = thread->jfr_thread_local(); + assert(tl != nullptr, "invariant"); + timer_t* timer = tl->cpu_timer(); + if (timer == nullptr) { + return; // no timer was created for this thread + } + tl->unset_cpu_timer(); + tl->deallocate_cpu_time_jfr_queue(); + s4 lost_samples = tl->cpu_time_jfr_queue().lost_samples(); + if (lost_samples > 0) { + JfrCPUTimeThreadSampling::send_lost_event(JfrTicks::now(), JfrThreadLocal::thread_id(thread), lost_samples); + } +} + +void JfrCPUSamplerThread::start_thread() { + if (os::create_thread(this, os::os_thread)) { + os::start_thread(this); + } else { + log_error(jfr)("Failed to create thread for thread sampling"); + } +} + +void JfrCPUSamplerThread::enroll() { + if (Atomic::cmpxchg(&_disenrolled, true, false)) { + Atomic::store(&_warned_about_timer_creation_failure, false); + initialize_active_signal_handler_counter(); + log_trace(jfr)("Enrolling CPU thread sampler"); + _sample.signal(); + if (!init_timers()) { + log_error(jfr)("Failed to initialize timers for CPU thread sampler"); + disenroll(); + return; + } + log_trace(jfr)("Enrolled CPU thread sampler"); + } +} + +void JfrCPUSamplerThread::disenroll() { + if (!Atomic::cmpxchg(&_disenrolled, false, true)) { + log_trace(jfr)("Disenrolling CPU thread sampler"); + if (Atomic::load_acquire(&_signal_handler_installed)) { + stop_timer(); + stop_signal_handlers(); + } + _sample.wait(); + log_trace(jfr)("Disenrolled CPU thread sampler"); + } +} + +void JfrCPUSamplerThread::run() { + assert(_sampler_thread == nullptr, "invariant"); + _sampler_thread = this; + int64_t last_auto_adapt_check = os::javaTimeNanos(); + while (true) { + if (!_sample.trywait()) { + // disenrolled + _sample.wait(); + } + _sample.signal(); + + if (os::javaTimeNanos() - last_auto_adapt_check > AUTOADAPT_INTERVAL_MS * 1000000) { + auto_adapt_period_if_needed(); + last_auto_adapt_check = os::javaTimeNanos(); + } + + if (Atomic::cmpxchg(&_is_async_processing_of_cpu_time_jfr_requests_triggered, true, false)) { + stackwalk_threads_in_native(); + } + os::naked_sleep(100); + } +} + +void JfrCPUSamplerThread::stackwalk_threads_in_native() { + ResourceMark rm; + // Required to prevent JFR from sampling through an ongoing safepoint + MutexLocker tlock(Threads_lock); + ThreadsListHandle tlh; + Thread* current = Thread::current(); + for (size_t i = 0; i < tlh.list()->length(); i++) { + JavaThread* jt = tlh.list()->thread_at(i); + JfrThreadLocal* tl = jt->jfr_thread_local(); + if (tl->wants_async_processing_of_cpu_time_jfr_requests()) { + if (jt->thread_state() != _thread_in_native || !tl->try_acquire_cpu_time_jfr_dequeue_lock()) { + tl->set_do_async_processing_of_cpu_time_jfr_requests(false); + continue; + } + if (jt->has_last_Java_frame()) { + JfrThreadSampling::process_cpu_time_request(jt, tl, current, false); + } else { + tl->set_do_async_processing_of_cpu_time_jfr_requests(false); + } + tl->release_cpu_time_jfr_queue_lock(); + } + } +} + +static volatile size_t count = 0; + +void JfrCPUTimeThreadSampling::send_empty_event(const JfrTicks &start_time, traceid tid, Tickspan cpu_time_period) { + EventCPUTimeSample event(UNTIMED); + event.set_failed(true); + event.set_starttime(start_time); + event.set_eventThread(tid); + event.set_stackTrace(0); + event.set_samplingPeriod(cpu_time_period); + event.set_biased(false); + event.commit(); +} + + +static volatile size_t biased_count = 0; + +void JfrCPUTimeThreadSampling::send_event(const JfrTicks &start_time, traceid sid, traceid tid, Tickspan cpu_time_period, bool biased) { + EventCPUTimeSample event(UNTIMED); + event.set_failed(false); + event.set_starttime(start_time); + event.set_eventThread(tid); + event.set_stackTrace(sid); + event.set_samplingPeriod(cpu_time_period); + event.set_biased(biased); + event.commit(); + Atomic::inc(&count); + if (biased) { + Atomic::inc(&biased_count); + } + if (Atomic::load(&count) % 1000 == 0) { + log_debug(jfr)("CPU thread sampler sent %zu events, lost %d, biased %zu\n", Atomic::load(&count), Atomic::load(&_lost_samples_sum), Atomic::load(&biased_count)); + } +} + +void JfrCPUTimeThreadSampling::send_lost_event(const JfrTicks &time, traceid tid, s4 lost_samples) { + if (!EventCPUTimeSamplesLost::is_enabled()) { + return; + } + EventCPUTimeSamplesLost event(UNTIMED); + event.set_starttime(time); + event.set_lostSamples(lost_samples); + event.set_eventThread(tid); + event.commit(); +} + +void JfrCPUSamplerThread::post_run() { + this->NonJavaThread::post_run(); + delete this; +} + +static JfrCPUTimeThreadSampling* _instance = nullptr; + +JfrCPUTimeThreadSampling& JfrCPUTimeThreadSampling::instance() { + return *_instance; +} + +JfrCPUTimeThreadSampling* JfrCPUTimeThreadSampling::create() { + assert(_instance == nullptr, "invariant"); + _instance = new JfrCPUTimeThreadSampling(); + return _instance; +} + +void JfrCPUTimeThreadSampling::destroy() { + if (_instance != nullptr) { + delete _instance; + _instance = nullptr; + } +} + +JfrCPUTimeThreadSampling::JfrCPUTimeThreadSampling() : _sampler(nullptr) {} + +JfrCPUTimeThreadSampling::~JfrCPUTimeThreadSampling() { + if (_sampler != nullptr) { + _sampler->disenroll(); + } +} + +void JfrCPUTimeThreadSampling::create_sampler(double rate, bool auto_adapt) { + assert(_sampler == nullptr, "invariant"); + _sampler = new JfrCPUSamplerThread(rate, auto_adapt); + _sampler->start_thread(); + _sampler->enroll(); +} + +void JfrCPUTimeThreadSampling::update_run_state(double rate, bool auto_adapt) { + if (rate != 0) { + if (_sampler == nullptr) { + create_sampler(rate, auto_adapt); + } else { + _sampler->set_rate(rate, auto_adapt); + _sampler->enroll(); + } + return; + } + if (_sampler != nullptr) { + _sampler->set_rate(rate /* 0 */, auto_adapt); + _sampler->disenroll(); + } +} + +void JfrCPUTimeThreadSampling::set_rate(double rate, bool auto_adapt) { + assert(rate >= 0, "invariant"); + if (_instance == nullptr) { + return; + } + instance().set_rate_value(rate, auto_adapt); +} + +void JfrCPUTimeThreadSampling::set_rate_value(double rate, bool auto_adapt) { + if (_sampler != nullptr) { + _sampler->set_rate(rate, auto_adapt); + } + update_run_state(rate, auto_adapt); +} + +void JfrCPUTimeThreadSampling::on_javathread_create(JavaThread *thread) { + if (_instance != nullptr && _instance->_sampler != nullptr) { + _instance->_sampler->on_javathread_create(thread); + } +} + +void JfrCPUTimeThreadSampling::on_javathread_terminate(JavaThread *thread) { + if (_instance != nullptr && _instance->_sampler != nullptr) { + _instance->_sampler->on_javathread_terminate(thread); + } +} + +void JfrCPUTimeThreadSampling::trigger_async_processing_of_cpu_time_jfr_requests() { + if (_instance != nullptr && _instance->_sampler != nullptr) { + _instance->_sampler->trigger_async_processing_of_cpu_time_jfr_requests(); + } +} + +void handle_timer_signal(int signo, siginfo_t* info, void* context) { + assert(_instance != nullptr, "invariant"); + _instance->handle_timer_signal(info, context); +} + + +void JfrCPUTimeThreadSampling::handle_timer_signal(siginfo_t* info, void* context) { + if (info->si_code != SI_TIMER) { + // not the signal we are interested in + return; + } + assert(_sampler != nullptr, "invariant"); + + if (!_sampler->increment_signal_handler_count()) { + return; + } + _sampler->handle_timer_signal(info, context); + _sampler->decrement_signal_handler_count(); +} + +void JfrCPUSamplerThread::sample_thread(JfrSampleRequest& request, void* ucontext, JavaThread* jt, JfrThreadLocal* tl, JfrTicks& now) { + JfrSampleRequestBuilder::build_cpu_time_sample_request(request, ucontext, jt, jt->jfr_thread_local(), now); +} + +static bool check_state(JavaThread* thread) { + switch (thread->thread_state()) { + case _thread_in_Java: + case _thread_in_native: + return true; + default: + return false; + } +} + +void JfrCPUSamplerThread::handle_timer_signal(siginfo_t* info, void* context) { + JfrTicks now = JfrTicks::now(); + JavaThread* jt = get_java_thread_if_valid(); + if (jt == nullptr) { + return; + } + JfrThreadLocal* tl = jt->jfr_thread_local(); + JfrCPUTimeTraceQueue& queue = tl->cpu_time_jfr_queue(); + if (!check_state(jt)) { + queue.increment_lost_samples(); + return; + } + if (!tl->try_acquire_cpu_time_jfr_enqueue_lock()) { + queue.increment_lost_samples(); + return; + } + + JfrCPUTimeSampleRequest request; + // the sampling period might be too low for the current Linux configuration + // so samples might be skipped and we have to compute the actual period + int64_t period = get_sampling_period() * (info->si_overrun + 1); + request._cpu_time_period = Ticks(period / 1000000000.0 * JfrTime::frequency()) - Ticks(0); + sample_thread(request._request, context, jt, tl, now); + + if (queue.enqueue(request)) { + if (queue.size() == 1) { + tl->set_has_cpu_time_jfr_requests(true); + SafepointMechanism::arm_local_poll_release(jt); + } + } else { + queue.increment_lost_samples(); + } + + if (jt->thread_state() == _thread_in_native) { + if (!tl->wants_async_processing_of_cpu_time_jfr_requests()) { + tl->set_do_async_processing_of_cpu_time_jfr_requests(true); + JfrCPUTimeThreadSampling::trigger_async_processing_of_cpu_time_jfr_requests(); + } + } else { + tl->set_do_async_processing_of_cpu_time_jfr_requests(false); + } + + tl->release_cpu_time_jfr_queue_lock(); +} + +static const int SIG = SIGPROF; + +static void set_timer_time(timer_t timerid, int64_t period_nanos) { + struct itimerspec its; + if (period_nanos == 0) { + its.it_interval.tv_sec = 0; + its.it_interval.tv_nsec = 0; + } else { + its.it_interval.tv_sec = period_nanos / NANOSECS_PER_SEC; + its.it_interval.tv_nsec = period_nanos % NANOSECS_PER_SEC; + } + its.it_value = its.it_interval; + if (timer_settime(timerid, 0, &its, nullptr) == -1) { + warning("Failed to set timer for thread sampling: %s", os::strerror(os::get_last_error())); + } +} + +bool JfrCPUSamplerThread::create_timer_for_thread(JavaThread* thread, timer_t& timerid) { + struct sigevent sev; + sev.sigev_notify = SIGEV_THREAD_ID; + sev.sigev_signo = SIG; + sev.sigev_value.sival_ptr = nullptr; + ((int*)&sev.sigev_notify)[1] = thread->osthread()->thread_id(); + clockid_t clock; + int err = pthread_getcpuclockid(thread->osthread()->pthread_id(), &clock); + if (err != 0) { + log_error(jfr)("Failed to get clock for thread sampling: %s", os::strerror(err)); + return false; + } + if (timer_create(clock, &sev, &timerid) < 0) { + return false; + } + int64_t period = get_sampling_period(); + if (period != 0) { + set_timer_time(timerid, period); + } + return true; +} + + +void JfrCPUSamplerThread::stop_signal_handlers() { + // set the stop signal bit + Atomic::or_then_fetch(&_active_signal_handlers, STOP_SIGNAL_BIT, memory_order_acq_rel); + while (Atomic::load_acquire(&_active_signal_handlers) > STOP_SIGNAL_BIT) { + // wait for all signal handlers to finish + os::naked_short_nanosleep(1000); + } +} + +// returns false if the stop signal bit was set, true otherwise +bool JfrCPUSamplerThread::increment_signal_handler_count() { + // increment the count of active signal handlers + u4 old_value = Atomic::fetch_then_add(&_active_signal_handlers, (u4)1, memory_order_acq_rel); + if ((old_value & STOP_SIGNAL_BIT) != 0) { + // if the stop signal bit was set, we are not allowed to increment + Atomic::dec(&_active_signal_handlers, memory_order_acq_rel); + return false; + } + return true; +} + +void JfrCPUSamplerThread::decrement_signal_handler_count() { + Atomic::dec(&_active_signal_handlers, memory_order_acq_rel); +} + +void JfrCPUSamplerThread::initialize_active_signal_handler_counter() { + Atomic::release_store(&_active_signal_handlers, (u4)0); +} + +class VM_JFRInitializeCPUTimeSampler : public VM_Operation { + private: + JfrCPUSamplerThread* const _sampler; + + public: + VM_JFRInitializeCPUTimeSampler(JfrCPUSamplerThread* sampler) : _sampler(sampler) {} + + VMOp_Type type() const { return VMOp_JFRInitializeCPUTimeSampler; } + void doit() { + JfrJavaThreadIterator iter; + while (iter.has_next()) { + _sampler->on_javathread_create(iter.next()); + } + }; +}; + +bool JfrCPUSamplerThread::init_timers() { + // install sig handler for sig + void* prev_handler = PosixSignals::get_signal_handler_for_signal(SIG); + if ((prev_handler != SIG_DFL && prev_handler != SIG_IGN && prev_handler != (void*)::handle_timer_signal) || + PosixSignals::install_generic_signal_handler(SIG, (void*)::handle_timer_signal) == (void*)-1) { + log_error(jfr)("Conflicting SIGPROF handler found: %p. CPUTimeSample events will not be recorded", prev_handler); + return false; + } + Atomic::release_store(&_signal_handler_installed, true); + VM_JFRInitializeCPUTimeSampler op(this); + VMThread::execute(&op); + return true; +} + +class VM_JFRTerminateCPUTimeSampler : public VM_Operation { + private: + JfrCPUSamplerThread* const _sampler; + + public: + VM_JFRTerminateCPUTimeSampler(JfrCPUSamplerThread* sampler) : _sampler(sampler) {} + + VMOp_Type type() const { return VMOp_JFRTerminateCPUTimeSampler; } + void doit() { + JfrJavaThreadIterator iter; + while (iter.has_next()) { + JavaThread *thread = iter.next(); + JfrThreadLocal* tl = thread->jfr_thread_local(); + timer_t* timer = tl->cpu_timer(); + if (timer == nullptr) { + continue; + } + timer_delete(*timer); + tl->deallocate_cpu_time_jfr_queue(); + tl->unset_cpu_timer(); + } + }; +}; + +void JfrCPUSamplerThread::stop_timer() { + VM_JFRTerminateCPUTimeSampler op(this); + VMThread::execute(&op); +} + +void JfrCPUSamplerThread::auto_adapt_period_if_needed() { + int64_t current_period = get_sampling_period(); + if (_auto_adapt || current_period == -1) { + int64_t period = compute_sampling_period(_rate); + if (period != current_period) { + Atomic::store(&_current_sampling_period_ns, period); + update_all_thread_timers(); + } + } +} + +void JfrCPUSamplerThread::set_rate(double rate, bool auto_adapt) { + _rate = rate; + _auto_adapt = auto_adapt; + if (_rate > 0 && Atomic::load_acquire(&_disenrolled) == false) { + auto_adapt_period_if_needed(); + } else { + Atomic::store(&_current_sampling_period_ns, compute_sampling_period(rate)); + } +} + +void JfrCPUSamplerThread::update_all_thread_timers() { + int64_t period_millis = get_sampling_period(); + ThreadsListHandle tlh; + for (size_t i = 0; i < tlh.length(); i++) { + JavaThread* thread = tlh.thread_at(i); + JfrThreadLocal* tl = thread->jfr_thread_local(); + assert(tl != nullptr, "invariant"); + timer_t* timer = tl->cpu_timer(); + if (timer != nullptr) { + set_timer_time(*timer, period_millis); + } + } +} + +#else + +static void warn() { + static bool displayed_warning = false; + if (!displayed_warning) { + warning("CPU time method sampling not supported in JFR on your platform"); + displayed_warning = true; + } +} + +static JfrCPUTimeThreadSampling* _instance = nullptr; + +JfrCPUTimeThreadSampling& JfrCPUTimeThreadSampling::instance() { + return *_instance; +} + +JfrCPUTimeThreadSampling* JfrCPUTimeThreadSampling::create() { + _instance = new JfrCPUTimeThreadSampling(); + return _instance; +} + +void JfrCPUTimeThreadSampling::destroy() { + delete _instance; + _instance = nullptr; +} + +void JfrCPUTimeThreadSampling::set_rate(double rate, bool auto_adapt) { + if (rate != 0) { + warn(); + } +} + +void JfrCPUTimeThreadSampling::on_javathread_create(JavaThread* thread) { +} + +void JfrCPUTimeThreadSampling::on_javathread_terminate(JavaThread* thread) { +} + +#endif // defined(LINUX) && defined(INCLUDE_JFR) diff --git a/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.hpp b/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.hpp new file mode 100644 index 00000000000..7c0545f4772 --- /dev/null +++ b/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.hpp @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +#ifndef SHARE_JFR_PERIODIC_SAMPLING_JFRCPUTIMETHREADSAMPLER_HPP +#define SHARE_JFR_PERIODIC_SAMPLING_JFRCPUTIMETHREADSAMPLER_HPP + +#include "jfr/utilities/jfrAllocation.hpp" + +class JavaThread; + +#if defined(LINUX) + +#include "jfr/periodic/sampling/jfrSampleRequest.hpp" +#include "jfr/utilities/jfrTypes.hpp" + +struct JfrCPUTimeSampleRequest { + JfrSampleRequest _request; + Tickspan _cpu_time_period; + + JfrCPUTimeSampleRequest() {} +}; + +// Fixed size async-signal-safe SPSC linear queue backed by an array. +// Designed to be only used under lock and read linearly +class JfrCPUTimeTraceQueue { + + // the default queue capacity, scaled if the sampling period is smaller than 10ms + // when the thread is started + static const u4 CPU_TIME_QUEUE_CAPACITY = 500; + + JfrCPUTimeSampleRequest* _data; + u4 _capacity; + // next unfilled index + volatile u4 _head; + + volatile u4 _lost_samples; + +public: + JfrCPUTimeTraceQueue(u4 capacity); + + ~JfrCPUTimeTraceQueue(); + + // signal safe, but can't be interleaved with dequeue + bool enqueue(JfrCPUTimeSampleRequest& trace); + + JfrCPUTimeSampleRequest& at(u4 index); + + u4 size() const; + + void set_size(u4 size); + + u4 capacity() const; + + // deletes all samples in the queue + void set_capacity(u4 capacity); + + bool is_empty() const; + + u4 lost_samples() const; + + void increment_lost_samples(); + + // returns the previous lost samples count + u4 get_and_reset_lost_samples(); + + void resize(u4 capacity); + + void resize_for_period(u4 period_millis); + + void clear(); + +}; + + +class JfrCPUSamplerThread; + +class JfrCPUTimeThreadSampling : public JfrCHeapObj { + friend class JfrRecorder; + private: + + JfrCPUSamplerThread* _sampler; + + void create_sampler(double rate, bool auto_adapt); + void set_rate_value(double rate, bool auto_adapt); + + JfrCPUTimeThreadSampling(); + ~JfrCPUTimeThreadSampling(); + + static JfrCPUTimeThreadSampling& instance(); + static JfrCPUTimeThreadSampling* create(); + static void destroy(); + + void update_run_state(double rate, bool auto_adapt); + + public: + static void set_rate(double rate, bool auto_adapt); + + static void on_javathread_create(JavaThread* thread); + static void on_javathread_terminate(JavaThread* thread); + void handle_timer_signal(siginfo_t* info, void* context); + + static void send_empty_event(const JfrTicks& start_time, traceid tid, Tickspan cpu_time_period); + static void send_event(const JfrTicks& start_time, traceid sid, traceid tid, Tickspan cpu_time_period, bool biased); + static void send_lost_event(const JfrTicks& time, traceid tid, s4 lost_samples); + + static void trigger_async_processing_of_cpu_time_jfr_requests(); +}; + +#else + +// a basic implementation on other platforms that +// emits warnings + +class JfrCPUTimeThreadSampling : public JfrCHeapObj { + friend class JfrRecorder; +private: + static JfrCPUTimeThreadSampling& instance(); + static JfrCPUTimeThreadSampling* create(); + static void destroy(); + + public: + static void set_rate(double rate, bool auto_adapt); + + static void on_javathread_create(JavaThread* thread); + static void on_javathread_terminate(JavaThread* thread); +}; + +#endif // defined(LINUX) + + +#endif // SHARE_JFR_PERIODIC_SAMPLING_JFRCPUTIMETHREADSAMPLER_HPP diff --git a/src/hotspot/share/jfr/periodic/sampling/jfrSampleRequest.cpp b/src/hotspot/share/jfr/periodic/sampling/jfrSampleRequest.cpp index f8e63e2e344..7049df0198b 100644 --- a/src/hotspot/share/jfr/periodic/sampling/jfrSampleRequest.cpp +++ b/src/hotspot/share/jfr/periodic/sampling/jfrSampleRequest.cpp @@ -24,6 +24,7 @@ #include "asm/codeBuffer.hpp" #include "interpreter/interpreter.hpp" #include "jfr/periodic/sampling/jfrSampleRequest.hpp" +#include "jfr/utilities/jfrTime.hpp" #include "runtime/continuationEntry.hpp" #include "runtime/frame.inline.hpp" #include "runtime/javaThread.inline.hpp" @@ -171,7 +172,7 @@ static bool build(JfrSampleRequest& request, intptr_t* fp, JavaThread* jt) { assert(request._sample_sp != nullptr, "invariant"); assert(request._sample_pc != nullptr, "invariant"); assert(jt != nullptr, "invariant"); - assert(jt->thread_state() == _thread_in_Java, "invariant"); + assert(jt->thread_state() == _thread_in_Java || jt->thread_state() == _thread_in_native, "invariant"); // 1. Interpreter frame? if (is_interpreter(request)) { @@ -303,3 +304,33 @@ JfrSampleResult JfrSampleRequestBuilder::build_java_sample_request(const void* u } return set_biased_java_sample(request, tl, jt); } + + +// A biased sample request is denoted by an empty bcp and an empty pc. +static inline void set_cpu_time_biased_sample(JfrSampleRequest& request, JavaThread* jt) { + if (request._sample_bcp != nullptr) { + request._sample_bcp = nullptr; + } + assert(request._sample_bcp == nullptr, "invariant"); + request._sample_pc = nullptr; +} + +void JfrSampleRequestBuilder::build_cpu_time_sample_request(JfrSampleRequest& request, + void* ucontext, + JavaThread* jt, + JfrThreadLocal* tl, + JfrTicks& now) { + assert(jt != nullptr, "invariant"); + request._sample_ticks = now; + + // Prioritize the ljf, if one exists. + request._sample_sp = jt->last_Java_sp(); + if (request._sample_sp == nullptr || !build_from_ljf(request, tl, jt)) { + intptr_t* fp; + request._sample_pc = os::fetch_frame_from_context(ucontext, reinterpret_cast(&request._sample_sp), &fp); + assert(sp_in_stack(request, jt), "invariant"); + if (!build(request, fp, jt)) { + set_cpu_time_biased_sample(request, jt); + } + } +} diff --git a/src/hotspot/share/jfr/periodic/sampling/jfrSampleRequest.hpp b/src/hotspot/share/jfr/periodic/sampling/jfrSampleRequest.hpp index 6567e7f8bff..20e737e0cbf 100644 --- a/src/hotspot/share/jfr/periodic/sampling/jfrSampleRequest.hpp +++ b/src/hotspot/share/jfr/periodic/sampling/jfrSampleRequest.hpp @@ -81,6 +81,11 @@ class JfrSampleRequestBuilder : AllStatic { static JfrSampleResult build_java_sample_request(const void* ucontext, JfrThreadLocal* tl, JavaThread* jt); + static void build_cpu_time_sample_request(JfrSampleRequest &request, + void* ucontext, + JavaThread* jt, + JfrThreadLocal* tl, + JfrTicks& now); }; #endif // SHARE_JFR_PERIODIC_SAMPLING_JFRSAMPLEREQUEST_HPP diff --git a/src/hotspot/share/jfr/periodic/sampling/jfrThreadSampling.cpp b/src/hotspot/share/jfr/periodic/sampling/jfrThreadSampling.cpp index aa72c29cf50..ddc9d59b295 100644 --- a/src/hotspot/share/jfr/periodic/sampling/jfrThreadSampling.cpp +++ b/src/hotspot/share/jfr/periodic/sampling/jfrThreadSampling.cpp @@ -28,6 +28,7 @@ #include "code/nmethod.hpp" #include "interpreter/interpreter.hpp" #include "jfr/jfrEvents.hpp" +#include "jfr/periodic/sampling/jfrCPUTimeThreadSampler.hpp" #include "jfr/periodic/sampling/jfrSampleMonitor.hpp" #include "jfr/periodic/sampling/jfrSampleRequest.hpp" #include "jfr/periodic/sampling/jfrThreadSampling.hpp" @@ -161,7 +162,7 @@ static inline bool is_valid(const PcDesc* pc_desc) { return pc_desc != nullptr && pc_desc->scope_decode_offset() != DebugInformationRecorder::serialized_null; } -static bool compute_top_frame(const JfrSampleRequest& request, frame& top_frame, bool& in_continuation, JavaThread* jt) { +static bool compute_top_frame(const JfrSampleRequest& request, frame& top_frame, bool& in_continuation, JavaThread* jt, bool& biased) { assert(jt != nullptr, "invariant"); if (!jt->has_last_Java_frame()) { @@ -178,6 +179,7 @@ static bool compute_top_frame(const JfrSampleRequest& request, frame& top_frame, // A biased sample is requested or no code blob. top_frame = jt->last_frame(); in_continuation = is_in_continuation(top_frame, jt); + biased = true; return true; } @@ -227,6 +229,8 @@ static bool compute_top_frame(const JfrSampleRequest& request, frame& top_frame, assert(!stream.current()->is_safepoint_blob_frame(), "invariant"); + biased = true; + // Search the first frame that is above the sampled sp. for (; !stream.is_done(); stream.next()) { frame* const current = stream.current(); @@ -250,6 +254,7 @@ static bool compute_top_frame(const JfrSampleRequest& request, frame& top_frame, const PcDesc* const pc_desc = get_pc_desc(sampled_nm, sampled_pc); if (is_valid(pc_desc)) { current->adjust_pc(pc_desc->real_pc(sampled_nm)); + biased = false; } } } @@ -270,8 +275,9 @@ static void record_thread_in_java(const JfrSampleRequest& request, const JfrTick assert(current != nullptr, "invariant"); frame top_frame; + bool biased = false; bool in_continuation; - if (!compute_top_frame(request, top_frame, in_continuation, jt)) { + if (!compute_top_frame(request, top_frame, in_continuation, jt, biased)) { return; } @@ -293,6 +299,42 @@ static void record_thread_in_java(const JfrSampleRequest& request, const JfrTick } } +#ifdef LINUX +static void record_cpu_time_thread(const JfrCPUTimeSampleRequest& request, const JfrTicks& now, const JfrThreadLocal* tl, JavaThread* jt, Thread* current) { + assert(jt != nullptr, "invariant"); + assert(tl != nullptr, "invariant"); + assert(current != nullptr, "invariant"); + frame top_frame; + bool biased = false; + bool in_continuation = false; + bool could_compute_top_frame = compute_top_frame(request._request, top_frame, in_continuation, jt, biased); + const traceid tid = in_continuation ? tl->vthread_id_with_epoch_update(jt) : JfrThreadLocal::jvm_thread_id(jt); + + if (!could_compute_top_frame) { + JfrCPUTimeThreadSampling::send_empty_event(request._request._sample_ticks, tid, request._cpu_time_period); + return; + } + traceid sid; + { + ResourceMark rm(current); + JfrStackTrace stacktrace; + if (!stacktrace.record(jt, top_frame, in_continuation, request._request)) { + // Unable to record stacktrace. Fail. + JfrCPUTimeThreadSampling::send_empty_event(request._request._sample_ticks, tid, request._cpu_time_period); + return; + } + sid = JfrStackTraceRepository::add(stacktrace); + } + assert(sid != 0, "invariant"); + + + JfrCPUTimeThreadSampling::send_event(request._request._sample_ticks, sid, tid, request._cpu_time_period, biased); + if (current == jt) { + send_safepoint_latency_event(request._request, now, sid, jt); + } +} +#endif + static void drain_enqueued_requests(const JfrTicks& now, JfrThreadLocal* tl, JavaThread* jt, Thread* current) { assert(tl != nullptr, "invariant"); assert(jt != nullptr, "invariant"); @@ -308,6 +350,49 @@ static void drain_enqueued_requests(const JfrTicks& now, JfrThreadLocal* tl, Jav assert(!tl->has_enqueued_requests(), "invariant"); } +static void drain_enqueued_cpu_time_requests(const JfrTicks& now, JfrThreadLocal* tl, JavaThread* jt, Thread* current, bool lock) { + assert(tl != nullptr, "invariant"); + assert(jt != nullptr, "invariant"); + assert(current != nullptr, "invariant"); +#ifdef LINUX + tl->set_do_async_processing_of_cpu_time_jfr_requests(false); + if (lock) { + tl->acquire_cpu_time_jfr_dequeue_lock(); + } + JfrCPUTimeTraceQueue& queue = tl->cpu_time_jfr_queue(); + for (u4 i = 0; i < queue.size(); i++) { + record_cpu_time_thread(queue.at(i), now, tl, jt, current); + } + queue.clear(); + assert(queue.is_empty(), "invariant"); + tl->set_has_cpu_time_jfr_requests(false); + if (queue.lost_samples() > 0) { + JfrCPUTimeThreadSampling::send_lost_event( now, JfrThreadLocal::thread_id(jt), queue.get_and_reset_lost_samples()); + } + if (lock) { + tl->release_cpu_time_jfr_queue_lock(); + } +#endif +} + +// Entry point for a thread that has been sampled in native code and has a pending JFR CPU time request. +void JfrThreadSampling::process_cpu_time_request(JavaThread* jt, JfrThreadLocal* tl, Thread* current, bool lock) { + assert(jt != nullptr, "invariant"); + + const JfrTicks now = JfrTicks::now(); + drain_enqueued_cpu_time_requests(now, tl, jt, current, lock); +} + +static void drain_all_enqueued_requests(const JfrTicks& now, JfrThreadLocal* tl, JavaThread* jt, Thread* current) { + assert(tl != nullptr, "invariant"); + assert(jt != nullptr, "invariant"); + assert(current != nullptr, "invariant"); + drain_enqueued_requests(now, tl, jt, current); + if (tl->has_cpu_time_jfr_requests()) { + drain_enqueued_cpu_time_requests(now, tl, jt, current, true); + } +} + // Only entered by the JfrSampler thread. bool JfrThreadSampling::process_native_sample_request(JfrThreadLocal* tl, JavaThread* jt, Thread* sampler_thread) { assert(tl != nullptr, "invairant"); @@ -382,5 +467,6 @@ void JfrThreadSampling::process_sample_request(JavaThread* jt) { break; } } - drain_enqueued_requests(now, tl, jt, jt); + drain_all_enqueued_requests(now, tl, jt, jt); } + diff --git a/src/hotspot/share/jfr/periodic/sampling/jfrThreadSampling.hpp b/src/hotspot/share/jfr/periodic/sampling/jfrThreadSampling.hpp index d4c4bc0d873..3c4d1a126b0 100644 --- a/src/hotspot/share/jfr/periodic/sampling/jfrThreadSampling.hpp +++ b/src/hotspot/share/jfr/periodic/sampling/jfrThreadSampling.hpp @@ -33,8 +33,10 @@ class Thread; class JfrThreadSampling : AllStatic { friend class JfrSamplerThread; + friend class JfrCPUSamplerThread; private: static bool process_native_sample_request(JfrThreadLocal* tl, JavaThread* jt, Thread* sampler_thread); + static void process_cpu_time_request(JavaThread* jt, JfrThreadLocal* tl, Thread* current, bool lock); public: static void process_sample_request(JavaThread* jt); }; diff --git a/src/hotspot/share/jfr/recorder/jfrRecorder.cpp b/src/hotspot/share/jfr/recorder/jfrRecorder.cpp index 384305862ca..dd75cb2929f 100644 --- a/src/hotspot/share/jfr/recorder/jfrRecorder.cpp +++ b/src/hotspot/share/jfr/recorder/jfrRecorder.cpp @@ -29,6 +29,7 @@ #include "jfr/jni/jfrJavaSupport.hpp" #include "jfr/leakprofiler/sampling/objectSampler.hpp" #include "jfr/periodic/jfrOSInterface.hpp" +#include "jfr/periodic/sampling/jfrCPUTimeThreadSampler.hpp" #include "jfr/periodic/sampling/jfrThreadSampler.hpp" #include "jfr/recorder/jfrRecorder.hpp" #include "jfr/recorder/checkpoint/jfrCheckpointManager.hpp" @@ -304,6 +305,9 @@ bool JfrRecorder::create_components() { if (!create_thread_sampler()) { return false; } + if (!create_cpu_time_thread_sampling()) { + return false; + } if (!create_event_throttler()) { return false; } @@ -318,6 +322,7 @@ static JfrStackTraceRepository* _stack_trace_repository; static JfrStringPool* _stringpool = nullptr; static JfrOSInterface* _os_interface = nullptr; static JfrThreadSampler* _thread_sampler = nullptr; +static JfrCPUTimeThreadSampling* _cpu_time_thread_sampling = nullptr; static JfrCheckpointManager* _checkpoint_manager = nullptr; bool JfrRecorder::create_java_event_writer() { @@ -390,6 +395,12 @@ bool JfrRecorder::create_thread_sampler() { return _thread_sampler != nullptr; } +bool JfrRecorder::create_cpu_time_thread_sampling() { + assert(_cpu_time_thread_sampling == nullptr, "invariant"); + _cpu_time_thread_sampling = JfrCPUTimeThreadSampling::create(); + return _cpu_time_thread_sampling != nullptr; +} + bool JfrRecorder::create_event_throttler() { return JfrEventThrottler::create(); } @@ -428,6 +439,10 @@ void JfrRecorder::destroy_components() { JfrThreadSampler::destroy(); _thread_sampler = nullptr; } + if (_cpu_time_thread_sampling != nullptr) { + JfrCPUTimeThreadSampling::destroy(); + _cpu_time_thread_sampling = nullptr; + } JfrEventThrottler::destroy(); } diff --git a/src/hotspot/share/jfr/recorder/jfrRecorder.hpp b/src/hotspot/share/jfr/recorder/jfrRecorder.hpp index b917904c5fb..3099c8ad344 100644 --- a/src/hotspot/share/jfr/recorder/jfrRecorder.hpp +++ b/src/hotspot/share/jfr/recorder/jfrRecorder.hpp @@ -54,6 +54,7 @@ class JfrRecorder : public JfrCHeapObj { static bool create_storage(); static bool create_stringpool(); static bool create_thread_sampler(); + static bool create_cpu_time_thread_sampling(); static bool create_event_throttler(); static bool create_components(); static void destroy_components(); diff --git a/src/hotspot/share/jfr/recorder/service/jfrEventThrottler.cpp b/src/hotspot/share/jfr/recorder/service/jfrEventThrottler.cpp index 0befaae7751..f660a01c04c 100644 --- a/src/hotspot/share/jfr/recorder/service/jfrEventThrottler.cpp +++ b/src/hotspot/share/jfr/recorder/service/jfrEventThrottler.cpp @@ -34,6 +34,7 @@ constexpr static const JfrSamplerParams _disabled_params = { false // reconfigure }; +static JfrEventThrottler* _disabled_cpu_time_sample_throttler = nullptr; static JfrEventThrottler* _object_allocation_throttler = nullptr; static JfrEventThrottler* _safepoint_latency_throttler = nullptr; @@ -48,6 +49,9 @@ JfrEventThrottler::JfrEventThrottler(JfrEventId event_id) : _update(false) {} bool JfrEventThrottler::create() { + assert(_disabled_cpu_time_sample_throttler == nullptr, "invariant"); + _disabled_cpu_time_sample_throttler = new JfrEventThrottler(JfrCPUTimeSampleEvent); + _disabled_cpu_time_sample_throttler->_disabled = true; assert(_object_allocation_throttler == nullptr, "invariant"); _object_allocation_throttler = new JfrEventThrottler(JfrObjectAllocationSampleEvent); if (_object_allocation_throttler == nullptr || !_object_allocation_throttler->initialize()) { @@ -59,6 +63,8 @@ bool JfrEventThrottler::create() { } void JfrEventThrottler::destroy() { + delete _disabled_cpu_time_sample_throttler; + _disabled_cpu_time_sample_throttler = nullptr; delete _object_allocation_throttler; _object_allocation_throttler = nullptr; delete _safepoint_latency_throttler; @@ -69,15 +75,19 @@ void JfrEventThrottler::destroy() { // and another for the SamplingLatency event. // When introducing many more throttlers, consider adding a lookup map keyed by event id. JfrEventThrottler* JfrEventThrottler::for_event(JfrEventId event_id) { + assert(_disabled_cpu_time_sample_throttler != nullptr, "Disabled CPU time throttler has not been properly initialized"); assert(_object_allocation_throttler != nullptr, "ObjectAllocation throttler has not been properly initialized"); assert(_safepoint_latency_throttler != nullptr, "SafepointLatency throttler has not been properly initialized"); - assert(event_id == JfrObjectAllocationSampleEvent || event_id == JfrSafepointLatencyEvent, "Event type has an unconfigured throttler"); + assert(event_id == JfrObjectAllocationSampleEvent || event_id == JfrSafepointLatencyEvent || event_id == JfrCPUTimeSampleEvent, "Event type has an unconfigured throttler"); if (event_id == JfrObjectAllocationSampleEvent) { return _object_allocation_throttler; } if (event_id == JfrSafepointLatencyEvent) { return _safepoint_latency_throttler; } + if (event_id == JfrCPUTimeSampleEvent) { + return _disabled_cpu_time_sample_throttler; + } return nullptr; } diff --git a/src/hotspot/share/jfr/support/jfrThreadLocal.cpp b/src/hotspot/share/jfr/support/jfrThreadLocal.cpp index 503aa85e02f..4b805a98a32 100644 --- a/src/hotspot/share/jfr/support/jfrThreadLocal.cpp +++ b/src/hotspot/share/jfr/support/jfrThreadLocal.cpp @@ -26,6 +26,7 @@ #include "jfr/jni/jfrJavaSupport.hpp" #include "jfr/leakprofiler/checkpoint/objectSampleCheckpoint.hpp" #include "jfr/periodic/jfrThreadCPULoadEvent.hpp" +#include "jfr/periodic/sampling/jfrCPUTimeThreadSampler.hpp" #include "jfr/recorder/checkpoint/jfrCheckpointManager.hpp" #include "jfr/recorder/checkpoint/types/traceid/jfrOopTraceId.inline.hpp" #include "jfr/recorder/jfrRecorder.hpp" @@ -78,7 +79,15 @@ JfrThreadLocal::JfrThreadLocal() : _enqueued_requests(false), _vthread(false), _notified(false), - _dead(false) { + _dead(false) +#ifdef LINUX + ,_cpu_timer(nullptr), + _cpu_time_jfr_locked(UNLOCKED), + _has_cpu_time_jfr_requests(false), + _cpu_time_jfr_queue(0), + _do_async_processing_of_cpu_time_jfr_requests(false) +#endif + { Thread* thread = Thread::current_or_null(); _parent_trace_id = thread != nullptr ? jvm_thread_id(thread) : (traceid)0; } @@ -129,7 +138,9 @@ void JfrThreadLocal::on_start(Thread* t) { if (JfrRecorder::is_recording()) { JfrCheckpointManager::write_checkpoint(t); if (t->is_Java_thread()) { - send_java_thread_start_event(JavaThread::cast(t)); + JavaThread *const jt = JavaThread::cast(t); + JfrCPUTimeThreadSampling::on_javathread_create(jt); + send_java_thread_start_event(jt); } } if (t->jfr_thread_local()->has_cached_stack_trace()) { @@ -221,6 +232,7 @@ void JfrThreadLocal::on_exit(Thread* t) { if (t->is_Java_thread()) { JavaThread* const jt = JavaThread::cast(t); send_java_thread_end_event(jt, JfrThreadLocal::jvm_thread_id(jt)); + JfrCPUTimeThreadSampling::on_javathread_terminate(jt); JfrThreadCPULoadEvent::send_event_for_thread(jt); } release(tl, Thread::current()); // because it could be that Thread::current() != t @@ -537,3 +549,85 @@ Arena* JfrThreadLocal::dcmd_arena(JavaThread* jt) { tl->_dcmd_arena = arena; return arena; } + + +#ifdef LINUX + +void JfrThreadLocal::set_cpu_timer(timer_t* timer) { + if (_cpu_timer == nullptr) { + _cpu_timer = JfrCHeapObj::new_array(1); + } + *_cpu_timer = *timer; +} + +void JfrThreadLocal::unset_cpu_timer() { + if (_cpu_timer != nullptr) { + timer_delete(*_cpu_timer); + JfrCHeapObj::free(_cpu_timer, sizeof(timer_t)); + _cpu_timer = nullptr; + } +} + +timer_t* JfrThreadLocal::cpu_timer() const { + return _cpu_timer; +} + +bool JfrThreadLocal::is_cpu_time_jfr_enqueue_locked() { + return Atomic::load_acquire(&_cpu_time_jfr_locked) == ENQUEUE; +} + +bool JfrThreadLocal::is_cpu_time_jfr_dequeue_locked() { + return Atomic::load_acquire(&_cpu_time_jfr_locked) == DEQUEUE; +} + +bool JfrThreadLocal::try_acquire_cpu_time_jfr_enqueue_lock() { + return Atomic::cmpxchg(&_cpu_time_jfr_locked, UNLOCKED, ENQUEUE) == UNLOCKED; +} + +bool JfrThreadLocal::try_acquire_cpu_time_jfr_dequeue_lock() { + CPUTimeLockState got; + while (true) { + CPUTimeLockState got = Atomic::cmpxchg(&_cpu_time_jfr_locked, UNLOCKED, DEQUEUE); + if (got == UNLOCKED) { + return true; // successfully locked for dequeue + } + if (got == DEQUEUE) { + return false; // already locked for dequeue + } + // else wait for the lock to be released from a signal handler + } +} + +void JfrThreadLocal::acquire_cpu_time_jfr_dequeue_lock() { + while (Atomic::cmpxchg(&_cpu_time_jfr_locked, UNLOCKED, DEQUEUE) != UNLOCKED); +} + +void JfrThreadLocal::release_cpu_time_jfr_queue_lock() { + Atomic::release_store(&_cpu_time_jfr_locked, UNLOCKED); +} + +void JfrThreadLocal::set_has_cpu_time_jfr_requests(bool has_requests) { + Atomic::release_store(&_has_cpu_time_jfr_requests, has_requests); +} + +bool JfrThreadLocal::has_cpu_time_jfr_requests() { + return Atomic::load_acquire(&_has_cpu_time_jfr_requests); +} + +JfrCPUTimeTraceQueue& JfrThreadLocal::cpu_time_jfr_queue() { + return _cpu_time_jfr_queue; +} + +void JfrThreadLocal::deallocate_cpu_time_jfr_queue() { + cpu_time_jfr_queue().resize(0); +} + +void JfrThreadLocal::set_do_async_processing_of_cpu_time_jfr_requests(bool wants) { + Atomic::release_store(&_do_async_processing_of_cpu_time_jfr_requests, wants); +} + +bool JfrThreadLocal::wants_async_processing_of_cpu_time_jfr_requests() { + return Atomic::load_acquire(&_do_async_processing_of_cpu_time_jfr_requests); +} + +#endif diff --git a/src/hotspot/share/jfr/support/jfrThreadLocal.hpp b/src/hotspot/share/jfr/support/jfrThreadLocal.hpp index 8e545d9c429..715a2c44f93 100644 --- a/src/hotspot/share/jfr/support/jfrThreadLocal.hpp +++ b/src/hotspot/share/jfr/support/jfrThreadLocal.hpp @@ -33,6 +33,10 @@ #include "runtime/atomic.hpp" #include "runtime/mutexLocker.hpp" +#ifdef LINUX +#include "jfr/periodic/sampling/jfrCPUTimeThreadSampler.hpp" +#endif + class Arena; class JavaThread; class JfrBuffer; @@ -79,6 +83,22 @@ class JfrThreadLocal { bool _dead; bool _sampling_critical_section; +#ifdef LINUX + timer_t* _cpu_timer; + + enum CPUTimeLockState { + UNLOCKED, + // locked for enqueuing + ENQUEUE, + // locked for dequeuing + DEQUEUE + }; + volatile CPUTimeLockState _cpu_time_jfr_locked; + volatile bool _has_cpu_time_jfr_requests; + JfrCPUTimeTraceQueue _cpu_time_jfr_queue; + volatile bool _do_async_processing_of_cpu_time_jfr_requests; +#endif + JfrBuffer* install_native_buffer() const; JfrBuffer* install_java_buffer() const; void release(Thread* t); @@ -342,6 +362,39 @@ class JfrThreadLocal { void set_thread_blob(const JfrBlobHandle& handle); const JfrBlobHandle& thread_blob() const; + // CPU time sampling +#ifdef LINUX + void set_cpu_timer(timer_t* timer); + void unset_cpu_timer(); + timer_t* cpu_timer() const; + + // The CPU time JFR lock has three different states: + // - ENQUEUE: lock for enqueuing CPU time requests + // - DEQUEUE: lock for dequeuing CPU time requests + // - UNLOCKED: no lock held + // This ensures that we can safely enqueue and dequeue CPU time requests, + // without interleaving + + bool is_cpu_time_jfr_enqueue_locked(); + bool is_cpu_time_jfr_dequeue_locked(); + + bool try_acquire_cpu_time_jfr_enqueue_lock(); + bool try_acquire_cpu_time_jfr_dequeue_lock(); + void acquire_cpu_time_jfr_dequeue_lock(); + void release_cpu_time_jfr_queue_lock(); + + void set_has_cpu_time_jfr_requests(bool has_events); + bool has_cpu_time_jfr_requests(); + + JfrCPUTimeTraceQueue& cpu_time_jfr_queue(); + void deallocate_cpu_time_jfr_queue(); + + void set_do_async_processing_of_cpu_time_jfr_requests(bool wants); + bool wants_async_processing_of_cpu_time_jfr_requests(); +#else + bool has_cpu_time_jfr_requests() { return false; } +#endif + // Hooks static void on_start(Thread* t); static void on_exit(Thread* t); diff --git a/src/hotspot/share/runtime/thread.hpp b/src/hotspot/share/runtime/thread.hpp index f3a06d5efd2..772ef7bbe82 100644 --- a/src/hotspot/share/runtime/thread.hpp +++ b/src/hotspot/share/runtime/thread.hpp @@ -78,6 +78,7 @@ class JavaThread; // - WorkerThread // - WatcherThread // - JfrThreadSampler +// - JfrCPUSamplerThread // - LogAsyncWriter // // All Thread subclasses must be either JavaThread or NonJavaThread. diff --git a/src/hotspot/share/runtime/vmOperation.hpp b/src/hotspot/share/runtime/vmOperation.hpp index 50d85944485..ac5d37381f9 100644 --- a/src/hotspot/share/runtime/vmOperation.hpp +++ b/src/hotspot/share/runtime/vmOperation.hpp @@ -115,6 +115,8 @@ template(JFROldObject) \ template(JvmtiPostObjectFree) \ template(RendezvousGCThreads) \ + template(JFRInitializeCPUTimeSampler) \ + template(JFRTerminateCPUTimeSampler) \ template(ReinitializeMDO) class Thread; diff --git a/src/hotspot/share/utilities/ticks.hpp b/src/hotspot/share/utilities/ticks.hpp index 8d2bbc7ce1f..88dc9a787f9 100644 --- a/src/hotspot/share/utilities/ticks.hpp +++ b/src/hotspot/share/utilities/ticks.hpp @@ -236,6 +236,7 @@ class TimeInstant : public Rep { friend class TimePartitionsTest; friend class GCTimerTest; friend class CompilerEvent; + friend class JfrCPUSamplerThread; }; #if INCLUDE_JFR diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/EventControl.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/EventControl.java index f43e45b724e..2ea4725abc8 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/EventControl.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/EventControl.java @@ -51,6 +51,7 @@ import jdk.jfr.internal.settings.EnabledSetting; import jdk.jfr.internal.settings.LevelSetting; import jdk.jfr.internal.settings.MethodSetting; import jdk.jfr.internal.settings.PeriodSetting; +import jdk.jfr.internal.settings.CPUThrottleSetting; import jdk.jfr.internal.settings.StackTraceSetting; import jdk.jfr.internal.settings.ThresholdSetting; import jdk.jfr.internal.settings.ThrottleSetting; @@ -326,6 +327,9 @@ public final class EventControl { private static Control defineThrottle(PlatformEventType type) { String def = type.getAnnotationValue(Throttle.class, ThrottleSetting.DEFAULT_VALUE); type.add(PrivateAccess.getInstance().newSettingDescriptor(TYPE_THROTTLE, Throttle.NAME, def, Collections.emptyList())); + if (type.getName().equals("jdk.CPUTimeSample")) { + return new Control(new CPUThrottleSetting(type), def); + } return new Control(new ThrottleSetting(type, def), def); } diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/JVM.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/JVM.java index e0eaef74b4c..b91f0c337b2 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/JVM.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/JVM.java @@ -270,6 +270,16 @@ public final class JVM { */ public static native void setMethodSamplingPeriod(long type, long periodMillis); + /** + * Set the maximum event emission rate for the CPU time sampler + * + * Setting rate to 0 turns off the CPU time sampler. + * + * @param rate the new rate in events per second + * @param autoAdapt true if the rate should be adapted automatically + */ + public static native void setCPUThrottle(double rate, boolean autoAdapt); + /** * Sets the file where data should be written. * diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformEventType.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformEventType.java index 32b59bca4c0..769e7055305 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformEventType.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformEventType.java @@ -30,8 +30,10 @@ import java.util.List; import java.util.Objects; import jdk.jfr.SettingDescriptor; +import jdk.jfr.events.ActiveSettingEvent; import jdk.jfr.internal.periodic.PeriodicEvents; import jdk.jfr.internal.util.ImplicitFields; +import jdk.jfr.internal.util.TimespanRate; import jdk.jfr.internal.util.Utils; import jdk.jfr.internal.tracing.Modification; @@ -45,6 +47,7 @@ public final class PlatformEventType extends Type { private final boolean isJVM; private final boolean isJDK; private final boolean isMethodSampling; + private final boolean isCPUTimeMethodSampling; private final List settings = new ArrayList<>(5); private final boolean dynamicSettings; private final int stackTraceOffset; @@ -56,6 +59,7 @@ public final class PlatformEventType extends Type { private boolean stackTraceEnabled = true; private long thresholdTicks = 0; private long period = 0; + private TimespanRate cpuRate; private boolean hasHook; private boolean beginChunk; @@ -75,6 +79,7 @@ public final class PlatformEventType extends Type { this.dynamicSettings = dynamicSettings; this.isJVM = Type.isDefinedByJVM(id); this.isMethodSampling = determineMethodSampling(); + this.isCPUTimeMethodSampling = isJVM && name.equals(Type.EVENT_NAME_PREFIX + "CPUTimeSample"); this.isJDK = isJDK; this.stackTraceOffset = determineStackTraceOffset(); } @@ -191,6 +196,13 @@ public final class PlatformEventType extends Type { } } + public void setCPUThrottle(TimespanRate rate) { + if (isCPUTimeMethodSampling) { + this.cpuRate = rate; + JVM.setCPUThrottle(rate.rate(), rate.autoAdapt()); + } + } + public void setHasPeriod(boolean hasPeriod) { this.hasPeriod = hasPeriod; } @@ -251,6 +263,9 @@ public final class PlatformEventType extends Type { if (isMethodSampling) { long p = enabled ? period : 0; JVM.setMethodSamplingPeriod(getId(), p); + } else if (isCPUTimeMethodSampling) { + TimespanRate r = enabled ? cpuRate : new TimespanRate(0, false); + JVM.setCPUThrottle(r.rate(), r.autoAdapt()); } else { JVM.setEnabled(getId(), enabled); } @@ -388,6 +403,10 @@ public final class PlatformEventType extends Type { return isMethodSampling; } + public boolean isCPUTimeMethodSampling() { + return isCPUTimeMethodSampling; + } + public void setStackFilterId(long id) { startFilterId = id; } diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/view.ini b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/view.ini index a6192db1ab0..a2ac74142f4 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/view.ini +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/view.ini @@ -39,7 +39,8 @@ label = "Active Settings" table = "COLUMN 'Event Type', 'Enabled', 'Threshold', 'Stack Trace','Period','Cutoff', 'Throttle' FORMAT none, missing:whitespace, missing:whitespace, missing:whitespace, - missing:whitespace, missing:whitespace, missing:whitespace + missing:whitespace, missing:whitespace, missing:whitespace, + missing:whitespace SELECT E.id, LAST_BATCH(E.value), LAST_BATCH(T.value), LAST_BATCH(S.value), LAST_BATCH(P.value), LAST_BATCH(C.value), LAST_BATCH(U.value) @@ -400,6 +401,28 @@ table = "COLUMN 'Method', 'Samples', 'Percent' SELECT stackTrace.topFrame AS T, COUNT(*), COUNT(*) FROM ExecutionSample GROUP BY T LIMIT 25" +[application.cpu-time-hot-methods] +label = "Java Methods that Execute the Most from CPU Time Sampler" +table = "COLUMN 'Method', 'Samples', 'Percent' + FORMAT none, none, normalized + SELECT stackTrace.topFrame AS T, COUNT(*), COUNT(*) + FROM CPUTimeSample GROUP BY T LIMIT 25" + +[application.cpu-time-statistics] +label = "CPU Time Sample Statistics" +form = "COLUMN 'Successful Samples', 'Failed Samples', 'Biased Samples', 'Total Samples', 'Lost Samples' + SELECT COUNT(S.startTime), COUNT(F.startTime), COUNT(B.startTime), Count(A.startTime), SUM(L.lostSamples) + FROM + CPUTimeSample AS S, + CPUTimeSample AS F, + CPUTimeSample AS A, + CPUTimeSample AS B, + CPUTimeSamplesLost AS L + WHERE + S.failed = 'false' AND + F.failed = 'true' AND + B.biased = 'true'" + [jvm.jdk-agents] label = "JDK Agents" table = "COLUMN 'Time', 'Initialization', 'Name', 'Options' diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/settings/CPUThrottleSetting.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/settings/CPUThrottleSetting.java new file mode 100644 index 00000000000..c18aeef2132 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/settings/CPUThrottleSetting.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, Datadog, Inc. All rights reserved. + * Copyright (c) 2025 SAP SE. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jfr.internal.settings; + +import static jdk.jfr.internal.util.TimespanUnit.SECONDS; +import static jdk.jfr.internal.util.TimespanUnit.MILLISECONDS; + +import java.util.Objects; +import java.util.Set; + +import jdk.jfr.Description; +import jdk.jfr.SettingControl; +import jdk.jfr.Label; +import jdk.jfr.MetadataDefinition; +import jdk.jfr.Name; +import jdk.jfr.internal.PlatformEventType; +import jdk.jfr.internal.Type; +import jdk.jfr.internal.util.TimespanRate; +import jdk.jfr.internal.util.Utils; + +@MetadataDefinition +@Label("CPUThrottleSetting") +@Description("Upper bounds the emission rate for CPU time samples") +@Name(Type.SETTINGS_PREFIX + "Rate") +public final class CPUThrottleSetting extends SettingControl { + public static final String DEFAULT_VALUE = "0/s"; + private final PlatformEventType eventType; + private String value = DEFAULT_VALUE; + + public CPUThrottleSetting(PlatformEventType eventType) { + this.eventType = Objects.requireNonNull(eventType); + } + + @Override + public String combine(Set values) { + TimespanRate max = null; + for (String value : values) { + TimespanRate rate = TimespanRate.of(value); + if (rate != null) { + if (max == null || rate.isHigher(max)) { + max = rate; + } + max = new TimespanRate(max.rate(), max.autoAdapt() || rate.autoAdapt()); + } + } + // "off" is not supported + return Objects.requireNonNullElse(max.toString(), DEFAULT_VALUE); + } + + @Override + public void setValue(String value) { + TimespanRate rate = TimespanRate.of(value); + if (rate != null) { + eventType.setCPUThrottle(rate); + this.value = value; + } + } + + @Override + public String getValue() { + return value; + } +} + diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Rate.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Rate.java index f32436a5e0f..2632cd63848 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Rate.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Rate.java @@ -55,4 +55,8 @@ public record Rate(long amount, TimespanUnit unit) { private double inNanos() { return (double) amount / unit.nanos; } + + public double perSecond() { + return inNanos() * 1_000_000_000.0; + } } diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/util/TimespanRate.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/TimespanRate.java new file mode 100644 index 00000000000..5d671310e3c --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/TimespanRate.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.util; + +import jdk.jfr.internal.settings.CPUThrottleSetting; + +/** + * A rate or fixed period, see {@link jdk.jfr.internal.Rate} + */ +public record TimespanRate(double rate, boolean autoAdapt) { + + public static TimespanRate of(String text) { + if (text.equals("off")) { + text = CPUThrottleSetting.DEFAULT_VALUE; + } + boolean isPeriod = !text.contains("/"); + if (isPeriod) { + var period = ValueParser.parseTimespanWithInfinity(text, Long.MAX_VALUE); + if (period == Long.MAX_VALUE) { + return null; + } + if (period == 0) { + return new TimespanRate(0, false); + } + return new TimespanRate(Runtime.getRuntime().availableProcessors() / (period / 1_000_000_000.0), false); + } + Rate r = Rate.of(text); + if (r == null) { + return null; + } + return new TimespanRate(r.perSecond(), true); + } + + public boolean isHigher(TimespanRate that) { + return rate() > that.rate(); + } + + @Override + public String toString() { + if (autoAdapt) { + return String.format("%d/ns", (long)(rate * 1_000_000_000L)); + } + return String.format("%dns", (long)(Runtime.getRuntime().availableProcessors() / rate * 1_000_000_000L)); + } +} diff --git a/src/jdk.jfr/share/conf/jfr/default.jfc b/src/jdk.jfr/share/conf/jfr/default.jfc index 293af26746f..541d1d3aa2f 100644 --- a/src/jdk.jfr/share/conf/jfr/default.jfc +++ b/src/jdk.jfr/share/conf/jfr/default.jfc @@ -226,6 +226,16 @@ off + + false + 500/s + true + + + + true + + true 10 ms diff --git a/src/jdk.jfr/share/conf/jfr/profile.jfc b/src/jdk.jfr/share/conf/jfr/profile.jfc index 89a9022d11e..9cec2d9a70f 100644 --- a/src/jdk.jfr/share/conf/jfr/profile.jfc +++ b/src/jdk.jfr/share/conf/jfr/profile.jfc @@ -206,6 +206,16 @@ 20 ms + + false + 10ms + true + + + + true + + true diff --git a/test/jdk/jdk/jfr/event/metadata/TestLookForUntestedEvents.java b/test/jdk/jdk/jfr/event/metadata/TestLookForUntestedEvents.java index 5b8aacfb1d2..d6e126a493f 100644 --- a/test/jdk/jdk/jfr/event/metadata/TestLookForUntestedEvents.java +++ b/test/jdk/jdk/jfr/event/metadata/TestLookForUntestedEvents.java @@ -89,7 +89,10 @@ public class TestLookForUntestedEvents { // Experimental events private static final Set experimentalEvents = Set.of( - "Flush", "SyncOnValueBasedClass"); + "Flush", "SyncOnValueBasedClass", "CPUTimeSample", "CPUTimeSamplesLost"); + + // Subset of the experimental events that should have tests + private static final Set experimentalButTestedEvents = Set.of("CPUTimeSample"); public static void main(String[] args) throws Exception { for (EventType type : FlightRecorder.getFlightRecorder().getEventTypes()) { @@ -110,7 +113,9 @@ public class TestLookForUntestedEvents { .collect(Collectors.toList()); Set eventsNotCoveredByTest = new HashSet<>(jfrEventTypes); - for (String event : jfrEventTypes) { + Set checkedEvents = new HashSet<>(jfrEventTypes); + checkedEvents.addAll(experimentalButTestedEvents); + for (String event : checkedEvents) { for (Path p : paths) { if (findStringInFile(p, event)) { eventsNotCoveredByTest.remove(event); diff --git a/test/jdk/jdk/jfr/event/profiling/BaseTestFullStackTrace.java b/test/jdk/jdk/jfr/event/profiling/BaseTestFullStackTrace.java new file mode 100644 index 00000000000..de211b8c454 --- /dev/null +++ b/test/jdk/jdk/jfr/event/profiling/BaseTestFullStackTrace.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2013, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jfr.event.profiling; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordedFrame; +import jdk.jfr.consumer.RecordedStackTrace; +import jdk.test.lib.Asserts; +import jdk.test.lib.jfr.EventNames; +import jdk.test.lib.jfr.Events; +import jdk.test.lib.jfr.RecurseThread; + +public class BaseTestFullStackTrace { + private final static int MAX_DEPTH = 64; // currently hardcoded in jvm + + private final String eventName; + private final String threadFieldName; + + public BaseTestFullStackTrace(String eventName, String threadFieldName) { + this.eventName = eventName; + this.threadFieldName = threadFieldName; + } + + public void run() throws Throwable { + RecurseThread[] threads = new RecurseThread[3]; + for (int i = 0; i < threads.length; ++i) { + int depth = MAX_DEPTH - 1 + i; + threads[i] = new RecurseThread(depth); + threads[i].setName("recursethread-" + depth); + threads[i].start(); + } + + for (RecurseThread thread : threads) { + while (!thread.isInRunLoop()) { + Thread.sleep(20); + } + } + + assertStackTraces(threads); + + for (RecurseThread thread : threads) { + thread.quit(); + thread.join(); + } + } + + private void assertStackTraces(RecurseThread[] threads) throws Throwable { + while (true) { + try (Recording recording = new Recording()) { + if (eventName.equals(EventNames.CPUTimeSample)) { + recording.enable(eventName).with("throttle", "50ms"); + } else { + recording.enable(eventName).withPeriod(Duration.ofMillis(50)); + } + recording.start(); + Thread.sleep(500); + recording.stop(); + if (hasValidStackTraces(recording, threads)) { + break; + } + } + }; + } + + private boolean hasValidStackTraces(Recording recording, RecurseThread[] threads) throws Throwable { + boolean[] isEventFound = new boolean[threads.length]; + + for (RecordedEvent event : Events.fromRecording(recording)) { + System.out.println("Event: " + event); + String threadName = Events.assertField(event, threadFieldName + ".javaName").getValue(); + long threadId = Events.assertField(event, threadFieldName + ".javaThreadId").getValue(); + + for (int threadIndex = 0; threadIndex < threads.length; ++threadIndex) { + RecurseThread currThread = threads[threadIndex]; + if (threadId == currThread.getId()) { + System.out.println("ThreadName=" + currThread.getName() + ", depth=" + currThread.totalDepth); + Asserts.assertEquals(threadName, currThread.getName(), "Wrong thread name"); + if ("recurseEnd".equals(getTopMethodName(event))) { + isEventFound[threadIndex] = true; + checkEvent(event, currThread.totalDepth); + break; + } + } + } + } + + for (int i = 0; i < threads.length; ++i) { + String msg = "threadIndex=%d, recurseDepth=%d, isEventFound=%b%n"; + System.out.printf(msg, i, threads[i].totalDepth, isEventFound[i]); + } + for (int i = 0; i < threads.length; ++i) { + if(!isEventFound[i]) { + // no assertion, let's retry. + // Could be race condition, i.e safe point during Thread.sleep + System.out.println("Failed to validate all threads, will retry."); + return false; + } + } + return true; + } + + public String getTopMethodName(RecordedEvent event) { + List frames = event.getStackTrace().getFrames(); + Asserts.assertFalse(frames.isEmpty(), "JavaFrames was empty"); + return frames.getFirst().getMethod().getName(); + } + + private void checkEvent(RecordedEvent event, int expectedDepth) throws Throwable { + RecordedStackTrace stacktrace = null; + try { + stacktrace = event.getStackTrace(); + List frames = stacktrace.getFrames(); + Asserts.assertEquals(Math.min(MAX_DEPTH, expectedDepth), frames.size(), "Wrong stacktrace depth. Expected:" + expectedDepth); + List expectedMethods = getExpectedMethods(expectedDepth); + Asserts.assertEquals(expectedMethods.size(), frames.size(), "Wrong expectedMethods depth. Test error."); + + for (int i = 0; i < frames.size(); ++i) { + String name = frames.get(i).getMethod().getName(); + String expectedName = expectedMethods.get(i); + System.out.printf("method[%d]=%s, expected=%s%n", i, name, expectedName); + Asserts.assertEquals(name, expectedName, "Wrong method name"); + } + + boolean isTruncated = stacktrace.isTruncated(); + boolean isTruncateExpected = expectedDepth > MAX_DEPTH; + Asserts.assertEquals(isTruncated, isTruncateExpected, "Wrong value for isTruncated. Expected:" + isTruncateExpected); + + String firstMethod = frames.getLast().getMethod().getName(); + boolean isFullTrace = "run".equals(firstMethod); + String msg = String.format("Wrong values for isTruncated=%b, isFullTrace=%b", isTruncated, isFullTrace); + Asserts.assertTrue(isTruncated != isFullTrace, msg); + } catch (Throwable t) { + System.out.println(String.format("stacktrace:%n%s", stacktrace)); + throw t; + } + } + + private List getExpectedMethods(int depth) { + List methods = new ArrayList<>(); + methods.add("recurseEnd"); + for (int i = 0; i < depth - 2; ++i) { + methods.add((i % 2) == 0 ? "recurseA" : "recurseB"); + } + methods.add("run"); + if (depth > MAX_DEPTH) { + methods = methods.subList(0, MAX_DEPTH); + } + return methods; + } +} diff --git a/test/jdk/jdk/jfr/event/profiling/TestCPUTimeAndExecutionSample.java b/test/jdk/jdk/jfr/event/profiling/TestCPUTimeAndExecutionSample.java new file mode 100644 index 00000000000..eb8d33832b5 --- /dev/null +++ b/test/jdk/jdk/jfr/event/profiling/TestCPUTimeAndExecutionSample.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jfr.event.profiling; + +import java.time.Duration; + +import jdk.jfr.consumer.RecordingStream; +import jdk.test.lib.jfr.EventNames; +import jdk.test.lib.jfr.RecurseThread; + +/* + * @test + * @requires vm.hasJFR & os.family == "linux" + * @library /test/lib + * @modules jdk.jfr/jdk.jfr.internal + * @run main/timeout=30 jdk.jfr.event.profiling.TestCPUTimeAndExecutionSample + */ +public class TestCPUTimeAndExecutionSample { + + static String sampleEvent = EventNames.CPUTimeSample; + + // The period is set to 1100 ms to provoke the 1000 ms + // threshold in the JVM for os::naked_short_sleep(). + public static void main(String[] args) throws Exception { + run(EventNames.ExecutionSample); + run(EventNames.CPUTimeSample); + run(EventNames.ExecutionSample); + run(EventNames.CPUTimeSample); + } + + private static void run(String eventType) { + RecurseThread t = new RecurseThread(50); + t.setDaemon(true); + try (RecordingStream rs = new RecordingStream()) { + rs.enable(sampleEvent).with("throttle", "1000/s"); + rs.onEvent(sampleEvent, e -> { + t.quit(); + rs.close(); + }); + t.start(); + rs.start(); + } + } +} diff --git a/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleFullStackTrace.java b/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleFullStackTrace.java new file mode 100644 index 00000000000..0d1108346ab --- /dev/null +++ b/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleFullStackTrace.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jfr.event.profiling; + +import jdk.test.lib.jfr.EventNames; + +/** + * @test + * @requires vm.hasJFR & os.family == "linux" + * @library /test/lib + * @build jdk.jfr.event.profiling.BaseTestFullStackTrace + * @run main/othervm jdk.jfr.event.profiling.TestCPUTimeSampleFullStackTrace + */ +public class TestCPUTimeSampleFullStackTrace { + + public static void main(String[] args) throws Throwable { + new BaseTestFullStackTrace(EventNames.CPUTimeSample, "eventThread").run(); + } + +} diff --git a/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleMultipleRecordings.java b/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleMultipleRecordings.java new file mode 100644 index 00000000000..133df36684c --- /dev/null +++ b/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleMultipleRecordings.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jfr.event.profiling; + +import java.time.Duration; + +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordingStream; +import jdk.jfr.internal.JVM; +import jdk.test.lib.jfr.EventNames; + +/* + * Tests that creating multiple recordings after another is possible. + * @test + * @requires vm.hasJFR & os.family == "linux" + * @library /test/lib + * @modules jdk.jfr/jdk.jfr.internal + * @run main jdk.jfr.event.profiling.TestCPUTimeSampleMultipleRecordings + */ +public class TestCPUTimeSampleMultipleRecordings { + + static String nativeEvent = EventNames.CPUTimeSample; + + static volatile boolean alive = true; + + public static void main(String[] args) throws Exception { + Thread t = new Thread(TestCPUTimeSampleMultipleRecordings::nativeMethod); + t.setDaemon(true); + t.start(); + for (int i = 0; i < 2; i++) { + try (RecordingStream rs = new RecordingStream()) { + rs.enable(nativeEvent).with("throttle", "1ms"); + rs.onEvent(nativeEvent, e -> { + alive = false; + rs.close(); + }); + + rs.start(); + } + } + alive = false; + } + + public static void nativeMethod() { + while (alive) { + JVM.getPid(); + } + } +} diff --git a/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleNative.java b/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleNative.java new file mode 100644 index 00000000000..1617bce4ba3 --- /dev/null +++ b/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleNative.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jfr.event.profiling; + +import java.time.Duration; + +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordingStream; +import jdk.jfr.internal.JVM; +import jdk.test.lib.jfr.EventNames; + +/* + * @test + * @requires vm.hasJFR & os.family == "linux" + * @library /test/lib + * @modules jdk.jfr/jdk.jfr.internal + * @run main jdk.jfr.event.profiling.TestCPUTimeSampleNative + */ +public class TestCPUTimeSampleNative { + + static String nativeEvent = EventNames.CPUTimeSample; + + static volatile boolean alive = true; + + public static void main(String[] args) throws Exception { + try (RecordingStream rs = new RecordingStream()) { + rs.enable(nativeEvent).with("throttle", "1ms"); + rs.onEvent(nativeEvent, e -> { + alive = false; + rs.close(); + }); + Thread t = new Thread(TestCPUTimeSampleNative::nativeMethod); + t.setDaemon(true); + t.start(); + rs.start(); + } + + } + + public static void nativeMethod() { + while (alive) { + JVM.getPid(); + } + } +} diff --git a/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleThrottling.java b/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleThrottling.java new file mode 100644 index 00000000000..55b350ad096 --- /dev/null +++ b/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleThrottling.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jfr.event.profiling; +import java.lang.management.ManagementFactory; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Comparator; + +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.test.lib.Asserts; +import jdk.test.lib.jfr.EventNames; +import jdk.test.lib.jfr.Events; + +/** + * @test + * @requires vm.hasJFR & os.family == "linux" + * @library /test/lib + * @run main/othervm jdk.jfr.event.profiling.TestCPUTimeSampleThrottling + */ +public class TestCPUTimeSampleThrottling { + + public static void main(String[] args) throws Exception { + testZeroPerSecond(); + testThrottleSettings(); + testThrottleSettingsPeriod(); + } + + private static void testZeroPerSecond() throws Exception { + Asserts.assertTrue(0L == countEvents(1000, "0/s").count()); + } + + private static void testThrottleSettings() throws Exception { + long count = countEvents(1000, + Runtime.getRuntime().availableProcessors() * 2 + "/s").count(); + Asserts.assertTrue(count > 0 && count < 3, + "Expected between 0 and 3 events, got " + count); + } + + private static void testThrottleSettingsPeriod() throws Exception { + float rate = countEvents(1000, "10ms").rate(); + Asserts.assertTrue(rate > 90 && rate < 110, "Expected around 100 events per second, got " + rate); + } + + private record EventCount(long count, float time) { + float rate() { + return count / time; + } + } + + private static EventCount countEvents(int timeMs, String rate) throws Exception { + try(Recording recording = new Recording()) { + recording.enable(EventNames.CPUTimeSample) + .with("throttle", rate); + + var bean = ManagementFactory.getThreadMXBean(); + + recording.start(); + + long startThreadCpuTime = bean.getCurrentThreadCpuTime(); + + wasteCPU(timeMs); + + long spendCPUTime = bean.getCurrentThreadCpuTime() - startThreadCpuTime; + + recording.stop(); + + long eventCount = Events.fromRecording(recording).stream() + .filter(e -> e.getThread().getJavaName() + .equals(Thread.currentThread().getName())) + .count(); + + System.out.println("Event count: " + eventCount + ", CPU time: " + spendCPUTime / 1_000_000_000f + "s"); + + return new EventCount(eventCount, spendCPUTime / 1_000_000_000f); + } + } + + private static void wasteCPU(int durationMs) { + long start = System.currentTimeMillis(); + double i = 0; + while (System.currentTimeMillis() - start < durationMs) { + for (int j = 0; j < 100000; j++) { + i = Math.sqrt(i * Math.pow(Math.sqrt(Math.random()), Math.random())); + } + } + } + +} diff --git a/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSamplingLongPeriod.java b/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSamplingLongPeriod.java new file mode 100644 index 00000000000..69d32d48282 --- /dev/null +++ b/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSamplingLongPeriod.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jfr.event.profiling; + +import java.time.Duration; + +import jdk.jfr.consumer.RecordingStream; +import jdk.test.lib.jfr.EventNames; +import jdk.test.lib.jfr.RecurseThread; + +/* + * @test + * @requires vm.hasJFR & os.family == "linux" + * @library /test/lib + * @modules jdk.jfr/jdk.jfr.internal + * @run main jdk.jfr.event.profiling.TestCPUTimeSamplingLongPeriod + */ +public class TestCPUTimeSamplingLongPeriod { + + static String sampleEvent = EventNames.CPUTimeSample; + + // The period is set to 1100 ms to provoke the 1000 ms + // threshold in the JVM for os::naked_short_sleep(). + public static void main(String[] args) throws Exception { + RecurseThread t = new RecurseThread(50); + t.setDaemon(true); + try (RecordingStream rs = new RecordingStream()) { + rs.enable(sampleEvent).with("throttle", "1100ms"); + rs.onEvent(sampleEvent, e -> { + t.quit(); + rs.close(); + }); + t.start(); + rs.start(); + } + } +} diff --git a/test/jdk/jdk/jfr/event/profiling/TestFullStackTrace.java b/test/jdk/jdk/jfr/event/profiling/TestFullStackTrace.java index b06f9eed03d..c337a8128ab 100644 --- a/test/jdk/jdk/jfr/event/profiling/TestFullStackTrace.java +++ b/test/jdk/jdk/jfr/event/profiling/TestFullStackTrace.java @@ -23,147 +23,20 @@ package jdk.jfr.event.profiling; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; - -import jdk.jfr.Recording; -import jdk.jfr.consumer.RecordedEvent; -import jdk.jfr.consumer.RecordedFrame; -import jdk.jfr.consumer.RecordedStackTrace; -import jdk.test.lib.Asserts; import jdk.test.lib.jfr.EventNames; -import jdk.test.lib.jfr.Events; -import jdk.test.lib.jfr.RecurseThread; /** * @test * @requires vm.hasJFR * @requires vm.opt.DeoptimizeALot != true * @library /test/lib + * @build jdk.jfr.event.profiling.BaseTestFullStackTrace * @run main/othervm jdk.jfr.event.profiling.TestFullStackTrace */ public class TestFullStackTrace { - private final static String EVENT_NAME = EventNames.ExecutionSample; - private final static int MAX_DEPTH = 64; // currently hardcoded in jvm public static void main(String[] args) throws Throwable { - RecurseThread[] threads = new RecurseThread[3]; - for (int i = 0; i < threads.length; ++i) { - int depth = MAX_DEPTH - 1 + i; - threads[i] = new RecurseThread(depth); - threads[i].setName("recursethread-" + depth); - threads[i].start(); - } - - for (RecurseThread thread : threads) { - while (!thread.isInRunLoop()) { - Thread.sleep(20); - } - } - - assertStackTraces(threads); - - for (RecurseThread thread : threads) { - thread.quit(); - thread.join(); - } + new BaseTestFullStackTrace(EventNames.ExecutionSample, "sampledThread").run(); } - private static void assertStackTraces( RecurseThread[] threads) throws Throwable { - Recording recording= null; - do { - recording = new Recording(); - recording.enable(EVENT_NAME).withPeriod(Duration.ofMillis(50)); - recording.start(); - Thread.sleep(500); - recording.stop(); - } while (!hasValidStackTraces(recording, threads)); - } - - private static boolean hasValidStackTraces(Recording recording, RecurseThread[] threads) throws Throwable { - boolean[] isEventFound = new boolean[threads.length]; - - for (RecordedEvent event : Events.fromRecording(recording)) { - //System.out.println("Event: " + event); - String threadName = Events.assertField(event, "sampledThread.javaName").getValue(); - long threadId = Events.assertField(event, "sampledThread.javaThreadId").getValue(); - - for (int threadIndex = 0; threadIndex < threads.length; ++threadIndex) { - RecurseThread currThread = threads[threadIndex]; - if (threadId == currThread.getId()) { - System.out.println("ThreadName=" + currThread.getName() + ", depth=" + currThread.totalDepth); - Asserts.assertEquals(threadName, currThread.getName(), "Wrong thread name"); - if ("recurseEnd".equals(getTopMethodName(event))) { - isEventFound[threadIndex] = true; - checkEvent(event, currThread.totalDepth); - break; - } - } - } - } - - for (int i = 0; i < threads.length; ++i) { - String msg = "threadIndex=%d, recurseDepth=%d, isEventFound=%b%n"; - System.out.printf(msg, i, threads[i].totalDepth, isEventFound[i]); - } - for (int i = 0; i < threads.length; ++i) { - if(!isEventFound[i]) { - // no assertion, let's retry. - // Could be race condition, i.e safe point during Thread.sleep - System.out.println("Failed to validate all threads, will retry."); - return false; - } - } - return true; - } - - public static String getTopMethodName(RecordedEvent event) { - List frames = event.getStackTrace().getFrames(); - Asserts.assertFalse(frames.isEmpty(), "JavaFrames was empty"); - return frames.getFirst().getMethod().getName(); - } - - private static void checkEvent(RecordedEvent event, int expectedDepth) throws Throwable { - RecordedStackTrace stacktrace = null; - try { - stacktrace = event.getStackTrace(); - List frames = stacktrace.getFrames(); - Asserts.assertEquals(Math.min(MAX_DEPTH, expectedDepth), frames.size(), "Wrong stacktrace depth. Expected:" + expectedDepth); - List expectedMethods = getExpectedMethods(expectedDepth); - Asserts.assertEquals(expectedMethods.size(), frames.size(), "Wrong expectedMethods depth. Test error."); - - for (int i = 0; i < frames.size(); ++i) { - String name = frames.get(i).getMethod().getName(); - String expectedName = expectedMethods.get(i); - System.out.printf("method[%d]=%s, expected=%s%n", i, name, expectedName); - Asserts.assertEquals(name, expectedName, "Wrong method name"); - } - - boolean isTruncated = stacktrace.isTruncated(); - boolean isTruncateExpected = expectedDepth > MAX_DEPTH; - Asserts.assertEquals(isTruncated, isTruncateExpected, "Wrong value for isTruncated. Expected:" + isTruncateExpected); - - String firstMethod = frames.getLast().getMethod().getName(); - boolean isFullTrace = "run".equals(firstMethod); - String msg = String.format("Wrong values for isTruncated=%b, isFullTrace=%b", isTruncated, isFullTrace); - Asserts.assertTrue(isTruncated != isFullTrace, msg); - } catch (Throwable t) { - System.out.println(String.format("stacktrace:%n%s", stacktrace)); - throw t; - } - } - - private static List getExpectedMethods(int depth) { - List methods = new ArrayList<>(); - methods.add("recurseEnd"); - for (int i = 0; i < depth - 2; ++i) { - methods.add((i % 2) == 0 ? "recurseA" : "recurseB"); - } - methods.add("run"); - if (depth > MAX_DEPTH) { - methods = methods.subList(0, MAX_DEPTH); - } - return methods; - } } diff --git a/test/jdk/jdk/jfr/event/profiling/classes/test/RecursiveMethods.java b/test/jdk/jdk/jfr/event/profiling/classes/test/RecursiveMethods.java new file mode 100644 index 00000000000..8dc77fd38da --- /dev/null +++ b/test/jdk/jdk/jfr/event/profiling/classes/test/RecursiveMethods.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + package test; + + import java.time.Duration; + + /* + * A class used to create a simple deep call stack for testing purposes + */ + public class RecursiveMethods { + + /** Method that uses recursion to produce a call stack of at least {@code depth} depth */ + public static int entry(int depth) { + return method2(--depth); + } + + private static int method2(int depth) { + return method3(--depth); + } + + private static int method3(int depth) { + return method4(--depth); + } + + private static int method4(int depth) { + return method5(--depth); + } + + private static int method5(int depth) { + return method6(--depth); + } + + private static int method6(int depth) { + return method7(--depth); + } + + private static int method7(int depth) { + return method8(--depth); + } + + private static int method8(int depth) { + return method9(--depth); + } + + private static int method9(int depth) { + return method10(--depth); + } + + private static int method10(int depth) { + if (depth > 0) { + return entry(--depth); + } + return depth; + } +} diff --git a/test/lib/jdk/test/lib/jfr/EventNames.java b/test/lib/jdk/test/lib/jfr/EventNames.java index 904abe8e3e2..a00898358a8 100644 --- a/test/lib/jdk/test/lib/jfr/EventNames.java +++ b/test/lib/jdk/test/lib/jfr/EventNames.java @@ -77,6 +77,8 @@ public class EventNames { public static final String ThreadAllocationStatistics = PREFIX + "ThreadAllocationStatistics"; public static final String ExecutionSample = PREFIX + "ExecutionSample"; public static final String NativeMethodSample = PREFIX + "NativeMethodSample"; + public static final String CPUTimeSample = PREFIX + "CPUTimeSample"; + public static final String CPUTimeSamplesLost = PREFIX + "CPUTimeSamplesLost"; public static final String ThreadDump = PREFIX + "ThreadDump"; public static final String OldObjectSample = PREFIX + "OldObjectSample"; public static final String SymbolTableStatistics = PREFIX + "SymbolTableStatistics";