From 2b8276aa5285da524e3f6514bd4954cfbdd3108c Mon Sep 17 00:00:00 2001 From: Alex Menkov Date: Tue, 10 Oct 2023 20:13:09 +0000 Subject: [PATCH] 8316691: Heap dump: separate stack traces for mounted virtual threads Reviewed-by: lmesnik, sspitsyn --- src/hotspot/share/services/heapDumper.cpp | 546 ++++++++++++------ .../vthread/HeapDump/VThreadInHeapDump.java | 300 ++++++++++ test/lib/jdk/test/lib/hprof/model/Root.java | 6 +- .../jdk/test/lib/hprof/model/Snapshot.java | 13 +- .../test/lib/hprof/model/ThreadObject.java | 56 ++ .../test/lib/hprof/parser/HprofReader.java | 38 +- 6 files changed, 754 insertions(+), 205 deletions(-) create mode 100644 test/hotspot/jtreg/serviceability/jvmti/vthread/HeapDump/VThreadInHeapDump.java create mode 100644 test/lib/jdk/test/lib/hprof/model/ThreadObject.java diff --git a/src/hotspot/share/services/heapDumper.cpp b/src/hotspot/share/services/heapDumper.cpp index e58d3e542c3..347dfa3c17b 100644 --- a/src/hotspot/share/services/heapDumper.cpp +++ b/src/hotspot/share/services/heapDumper.cpp @@ -43,6 +43,7 @@ #include "oops/objArrayOop.inline.hpp" #include "oops/oop.inline.hpp" #include "oops/typeArrayOop.inline.hpp" +#include "runtime/continuationWrapper.inline.hpp" #include "runtime/frame.inline.hpp" #include "runtime/handles.inline.hpp" #include "runtime/javaCalls.hpp" @@ -1385,7 +1386,6 @@ class JNILocalsDumper : public OopClosure { void do_oop(narrowOop* obj_p) { ShouldNotReachHere(); } }; - void JNILocalsDumper::do_oop(oop* obj_p) { // ignore null handles oop o = *obj_p; @@ -1451,6 +1451,310 @@ class StickyClassDumper : public KlassClosure { } }; +// Support class used to generate HPROF_GC_ROOT_JAVA_FRAME records. + +class JavaStackRefDumper : public StackObj { +private: + AbstractDumpWriter* _writer; + u4 _thread_serial_num; + int _frame_num; + AbstractDumpWriter* writer() const { return _writer; } +public: + JavaStackRefDumper(AbstractDumpWriter* writer, u4 thread_serial_num) + : _writer(writer), _thread_serial_num(thread_serial_num), _frame_num(-1) // default - empty stack + { + } + + void set_frame_number(int n) { _frame_num = n; } + + void dump_java_stack_refs(StackValueCollection* values); +}; + +void JavaStackRefDumper::dump_java_stack_refs(StackValueCollection* values) { + for (int index = 0; index < values->size(); index++) { + if (values->at(index)->type() == T_OBJECT) { + oop o = values->obj_at(index)(); + if (o != nullptr) { + u4 size = 1 + sizeof(address) + 4 + 4; + writer()->start_sub_record(HPROF_GC_ROOT_JAVA_FRAME, size); + writer()->write_objectID(o); + writer()->write_u4(_thread_serial_num); + writer()->write_u4((u4)_frame_num); + writer()->end_sub_record(); + } + } + } +} + +// Class to collect, store and dump thread-related data: +// - HPROF_TRACE and HPROF_FRAME records; +// - HPROF_GC_ROOT_THREAD_OBJ/HPROF_GC_ROOT_JAVA_FRAME/HPROF_GC_ROOT_JNI_LOCAL subrecords. +class ThreadDumper : public CHeapObj { +public: + enum class ThreadType { Platform, MountedVirtual, UnmountedVirtual }; + +private: + ThreadType _thread_type; + JavaThread* _java_thread; + oop _thread_oop; + + GrowableArray* _frames; + // non-null if the thread is OOM thread + Method* _oome_constructor; + int _thread_serial_num; + int _start_frame_serial_num; + + vframe* get_top_frame() const; + +public: + static bool should_dump_pthread(JavaThread* thread) { + return thread->threadObj() != nullptr && !thread->is_exiting() && !thread->is_hidden_from_external_view(); + } + + static bool should_dump_vthread(oop vt) { + return java_lang_VirtualThread::state(vt) != java_lang_VirtualThread::NEW + && java_lang_VirtualThread::state(vt) != java_lang_VirtualThread::TERMINATED; + } + + ThreadDumper(ThreadType thread_type, JavaThread* java_thread, oop thread_oop); + + // affects frame_count + void add_oom_frame(Method* oome_constructor) { + assert(_start_frame_serial_num == 0, "add_oom_frame cannot be called after init_serial_nums"); + _oome_constructor = oome_constructor; + } + + void init_serial_nums(volatile int* thread_counter, volatile int* frame_counter) { + assert(_start_frame_serial_num == 0, "already initialized"); + _thread_serial_num = Atomic::fetch_then_add(thread_counter, 1); + _start_frame_serial_num = Atomic::fetch_then_add(frame_counter, frame_count()); + } + + bool oom_thread() const { + return _oome_constructor != nullptr; + } + + int frame_count() const { + return _frames->length() + (oom_thread() ? 1 : 0); + } + + u4 thread_serial_num() const { + return (u4)_thread_serial_num; + } + + u4 stack_trace_serial_num() const { + return (u4)(_thread_serial_num + STACK_TRACE_ID); + } + + // writes HPROF_TRACE and HPROF_FRAME records + // returns number of dumped frames + void dump_stack_traces(AbstractDumpWriter* writer, GrowableArray* klass_map); + + // writes HPROF_GC_ROOT_THREAD_OBJ subrecord + void dump_thread_obj(AbstractDumpWriter* writer); + + // Walk the stack of the thread. + // Dumps a HPROF_GC_ROOT_JAVA_FRAME subrecord for each local + // Dumps a HPROF_GC_ROOT_JNI_LOCAL subrecord for each JNI local + void dump_stack_refs(AbstractDumpWriter* writer); + +}; + +ThreadDumper::ThreadDumper(ThreadType thread_type, JavaThread* java_thread, oop thread_oop) + : _thread_type(thread_type), _java_thread(java_thread), _thread_oop(thread_oop), + _oome_constructor(nullptr), + _thread_serial_num(0), _start_frame_serial_num(0) +{ + // sanity checks + if (_thread_type == ThreadType::UnmountedVirtual) { + assert(_java_thread == nullptr, "sanity"); + assert(_thread_oop != nullptr, "sanity"); + } else { + assert(_java_thread != nullptr, "sanity"); + assert(_thread_oop != nullptr, "sanity"); + } + + _frames = new (mtServiceability) GrowableArray(10, mtServiceability); + bool stop_at_vthread_entry = _thread_type == ThreadType::MountedVirtual; + + // vframes are resource allocated + Thread* current_thread = Thread::current(); + ResourceMark rm(current_thread); + HandleMark hm(current_thread); + + for (vframe* vf = get_top_frame(); vf != nullptr; vf = vf->sender()) { + if (stop_at_vthread_entry && vf->is_vthread_entry()) { + break; + } + if (vf->is_java_frame()) { + javaVFrame* jvf = javaVFrame::cast(vf); + _frames->append(new StackFrameInfo(jvf, false)); + } else { + // ignore non-Java frames + } + } +} + +void ThreadDumper::dump_stack_traces(AbstractDumpWriter* writer, GrowableArray* klass_map) { + assert(_thread_serial_num != 0 && _start_frame_serial_num != 0, "serial_nums are not initialized"); + + // write HPROF_FRAME records for this thread's stack trace + int depth = _frames->length(); + int frame_serial_num = _start_frame_serial_num; + + if (oom_thread()) { + // OOM thread + // write fake frame that makes it look like the thread, which caused OOME, + // is in the OutOfMemoryError zero-parameter constructor + int oome_serial_num = klass_map->find(_oome_constructor->method_holder()); + // the class serial number starts from 1 + assert(oome_serial_num > 0, "OutOfMemoryError class not found"); + DumperSupport::dump_stack_frame(writer, ++frame_serial_num, oome_serial_num, _oome_constructor, 0); + depth++; + } + + for (int j = 0; j < _frames->length(); j++) { + StackFrameInfo* frame = _frames->at(j); + Method* m = frame->method(); + int class_serial_num = klass_map->find(m->method_holder()); + // the class serial number starts from 1 + assert(class_serial_num > 0, "class not found"); + DumperSupport::dump_stack_frame(writer, ++frame_serial_num, class_serial_num, m, frame->bci()); + } + + // write HPROF_TRACE record for the thread + DumperSupport::write_header(writer, HPROF_TRACE, checked_cast(3 * sizeof(u4) + depth * oopSize)); + writer->write_u4(stack_trace_serial_num()); // stack trace serial number + writer->write_u4(thread_serial_num()); // thread serial number + writer->write_u4((u4)depth); // frame count (including oom frame) + for (int j = 1; j <= depth; j++) { + writer->write_id(_start_frame_serial_num + j); + } +} + +void ThreadDumper::dump_thread_obj(AbstractDumpWriter * writer) { + assert(_thread_serial_num != 0 && _start_frame_serial_num != 0, "serial_num is not initialized"); + + u4 size = 1 + sizeof(address) + 4 + 4; + writer->start_sub_record(HPROF_GC_ROOT_THREAD_OBJ, size); + writer->write_objectID(_thread_oop); + writer->write_u4(thread_serial_num()); // thread serial number + writer->write_u4(stack_trace_serial_num()); // stack trace serial number + writer->end_sub_record(); +} + +void ThreadDumper::dump_stack_refs(AbstractDumpWriter * writer) { + assert(_thread_serial_num != 0 && _start_frame_serial_num != 0, "serial_num is not initialized"); + + JNILocalsDumper blk(writer, thread_serial_num()); + if (_thread_type == ThreadType::Platform) { + if (!_java_thread->has_last_Java_frame()) { + // no last java frame but there may be JNI locals + _java_thread->active_handles()->oops_do(&blk); + return; + } + } + + JavaStackRefDumper java_ref_dumper(writer, thread_serial_num()); + + // vframes are resource allocated + Thread* current_thread = Thread::current(); + ResourceMark rm(current_thread); + HandleMark hm(current_thread); + + bool stopAtVthreadEntry = _thread_type == ThreadType::MountedVirtual; + frame* last_entry_frame = nullptr; + bool is_top_frame = true; + int depth = 0; + if (oom_thread()) { + depth++; + } + + for (vframe* vf = get_top_frame(); vf != nullptr; vf = vf->sender()) { + if (stopAtVthreadEntry && vf->is_vthread_entry()) { + break; + } + + if (vf->is_java_frame()) { + javaVFrame* jvf = javaVFrame::cast(vf); + if (!(jvf->method()->is_native())) { + java_ref_dumper.set_frame_number(depth); + java_ref_dumper.dump_java_stack_refs(jvf->locals()); + java_ref_dumper.dump_java_stack_refs(jvf->expressions()); + } else { + // native frame + blk.set_frame_number(depth); + if (is_top_frame) { + // JNI locals for the top frame. + assert(_java_thread != nullptr, "impossible for unmounted vthread"); + _java_thread->active_handles()->oops_do(&blk); + } else { + if (last_entry_frame != nullptr) { + // JNI locals for the entry frame + assert(last_entry_frame->is_entry_frame(), "checking"); + last_entry_frame->entry_frame_call_wrapper()->handles()->oops_do(&blk); + } + } + } + last_entry_frame = nullptr; + // increment only for Java frames + depth++; + } else { + // externalVFrame - for an entry frame then we report the JNI locals + // when we find the corresponding javaVFrame + frame* fr = vf->frame_pointer(); + assert(fr != nullptr, "sanity check"); + if (fr->is_entry_frame()) { + last_entry_frame = fr; + } + } + is_top_frame = false; + } + assert(depth == frame_count(), "total number of Java frames not matched"); +} + +vframe* ThreadDumper::get_top_frame() const { + if (_thread_type == ThreadType::UnmountedVirtual) { + ContinuationWrapper cont(java_lang_VirtualThread::continuation(_thread_oop)); + if (cont.is_empty()) { + return nullptr; + } + assert(!cont.is_mounted(), "sanity check"); + stackChunkOop chunk = cont.last_nonempty_chunk(); + if (chunk == nullptr || chunk->is_empty()) { + return nullptr; + } + + RegisterMap reg_map(cont.continuation(), RegisterMap::UpdateMap::include); + frame fr = chunk->top_frame(®_map); + vframe* vf = vframe::new_vframe(&fr, ®_map, nullptr); // don't need JavaThread + return vf; + } + + RegisterMap reg_map(_java_thread, + RegisterMap::UpdateMap::include, + RegisterMap::ProcessFrames::include, + RegisterMap::WalkContinuation::skip); + switch (_thread_type) { + case ThreadType::Platform: + if (!_java_thread->has_last_Java_frame()) { + return nullptr; + } + return _java_thread->is_vthread_mounted() + ? _java_thread->carrier_last_java_vframe(®_map) + : _java_thread->platform_thread_last_java_vframe(®_map); + + case ThreadType::MountedVirtual: + return _java_thread->last_java_vframe(®_map); + + default: // make compilers happy + break; + } + ShouldNotReachHere(); + return nullptr; +} + + class VM_HeapDumper; // Support class using when iterating over the heap. @@ -1683,8 +1987,12 @@ class VM_HeapDumper : public VM_GC_Operation, public WorkerTask { Method* _oome_constructor; bool _gc_before_heap_dump; GrowableArray* _klass_map; - ThreadStackTrace** _stack_traces; - int _num_threads; + + ThreadDumper** _thread_dumpers; // platform, carrier and mounted virtual threads + int _thread_dumpers_count; + volatile int _thread_serial_num; + volatile int _frame_serial_num; + volatile int _dump_seq; // parallel heap dump support uint _num_dumper_threads; @@ -1721,15 +2029,18 @@ class VM_HeapDumper : public VM_GC_Operation, public WorkerTask { // writes a HPROF_GC_CLASS_DUMP record for the given class static void do_class_dump(Klass* k); - // HPROF_GC_ROOT_THREAD_OBJ records - int do_thread(JavaThread* thread, u4 thread_serial_num); - void do_threads(); + // HPROF_GC_ROOT_THREAD_OBJ records for platform and mounted virtual threads + void dump_threads(); void add_class_serial_number(Klass* k, int serial_num) { _klass_map->at_put_grow(serial_num, k); } - // HPROF_TRACE and HPROF_FRAME records + bool is_oom_thread(JavaThread* thread) const { + return thread == _oome_thread && _oome_constructor != nullptr; + } + + // HPROF_TRACE and HPROF_FRAME records for platform and mounted virtual threads void dump_stack_traces(); public: @@ -1742,8 +2053,12 @@ class VM_HeapDumper : public VM_GC_Operation, public WorkerTask { _local_writer = writer; _gc_before_heap_dump = gc_before_heap_dump; _klass_map = new (mtServiceability) GrowableArray(INITIAL_CLASS_COUNT, mtServiceability); - _stack_traces = nullptr; - _num_threads = 0; + + _thread_dumpers = nullptr; + _thread_dumpers_count = 0; + _thread_serial_num = 1; + _frame_serial_num = 1; + _dump_seq = 0; _num_dumper_threads = num_dump_threads; _dumper_controller = nullptr; @@ -1763,12 +2078,13 @@ class VM_HeapDumper : public VM_GC_Operation, public WorkerTask { } ~VM_HeapDumper() { - if (_stack_traces != nullptr) { - for (int i=0; i < _num_threads; i++) { - delete _stack_traces[i]; + if (_thread_dumpers != nullptr) { + for (int i = 0; i < _thread_dumpers_count; i++) { + delete _thread_dumpers[i]; } - FREE_C_HEAP_ARRAY(ThreadStackTrace*, _stack_traces); + FREE_C_HEAP_ARRAY(ThreadDumper*, _thread_dumpers); } + if (_dumper_controller != nullptr) { delete _dumper_controller; _dumper_controller = nullptr; @@ -1835,127 +2151,13 @@ void VM_HeapDumper::do_class_dump(Klass* k) { } } -// Walk the stack of the given thread. -// Dumps a HPROF_GC_ROOT_JAVA_FRAME record for each local -// Dumps a HPROF_GC_ROOT_JNI_LOCAL record for each JNI local -// -// It returns the number of Java frames in this thread stack -int VM_HeapDumper::do_thread(JavaThread* java_thread, u4 thread_serial_num) { - JNILocalsDumper blk(writer(), thread_serial_num); - - oop threadObj = java_thread->threadObj(); - assert(threadObj != nullptr, "sanity check"); - - int stack_depth = 0; - if (java_thread->has_last_Java_frame()) { - - // vframes are resource allocated - Thread* current_thread = Thread::current(); - ResourceMark rm(current_thread); - HandleMark hm(current_thread); - - RegisterMap reg_map(java_thread, - RegisterMap::UpdateMap::include, - RegisterMap::ProcessFrames::include, - RegisterMap::WalkContinuation::skip); - frame f = java_thread->last_frame(); - vframe* vf = vframe::new_vframe(&f, ®_map, java_thread); - frame* last_entry_frame = nullptr; - int extra_frames = 0; - - if (java_thread == _oome_thread && _oome_constructor != nullptr) { - extra_frames++; +// Write a HPROF_GC_ROOT_THREAD_OBJ record for platform/carrier and mounted virtual threads. +// Then walk the stack so that locals and JNI locals are dumped. +void VM_HeapDumper::dump_threads() { + for (int i = 0; i < _thread_dumpers_count; i++) { + _thread_dumpers[i]->dump_thread_obj(writer()); + _thread_dumpers[i]->dump_stack_refs(writer()); } - while (vf != nullptr) { - blk.set_frame_number(stack_depth); - if (vf->is_java_frame()) { - - // java frame (interpreted, compiled, ...) - javaVFrame *jvf = javaVFrame::cast(vf); - if (!(jvf->method()->is_native())) { - StackValueCollection* locals = jvf->locals(); - for (int slot=0; slotsize(); slot++) { - if (locals->at(slot)->type() == T_OBJECT) { - oop o = locals->obj_at(slot)(); - - if (o != nullptr) { - u4 size = 1 + sizeof(address) + 4 + 4; - writer()->start_sub_record(HPROF_GC_ROOT_JAVA_FRAME, size); - writer()->write_objectID(o); - writer()->write_u4(thread_serial_num); - writer()->write_u4((u4) (stack_depth + extra_frames)); - writer()->end_sub_record(); - } - } - } - StackValueCollection *exprs = jvf->expressions(); - for(int index = 0; index < exprs->size(); index++) { - if (exprs->at(index)->type() == T_OBJECT) { - oop o = exprs->obj_at(index)(); - if (o != nullptr) { - u4 size = 1 + sizeof(address) + 4 + 4; - writer()->start_sub_record(HPROF_GC_ROOT_JAVA_FRAME, size); - writer()->write_objectID(o); - writer()->write_u4(thread_serial_num); - writer()->write_u4((u4) (stack_depth + extra_frames)); - writer()->end_sub_record(); - } - } - } - } else { - // native frame - if (stack_depth == 0) { - // JNI locals for the top frame. - java_thread->active_handles()->oops_do(&blk); - } else { - if (last_entry_frame != nullptr) { - // JNI locals for the entry frame - assert(last_entry_frame->is_entry_frame(), "checking"); - last_entry_frame->entry_frame_call_wrapper()->handles()->oops_do(&blk); - } - } - } - // increment only for Java frames - stack_depth++; - last_entry_frame = nullptr; - - } else { - // externalVFrame - if it's an entry frame then report any JNI locals - // as roots when we find the corresponding native javaVFrame - frame* fr = vf->frame_pointer(); - assert(fr != nullptr, "sanity check"); - if (fr->is_entry_frame()) { - last_entry_frame = fr; - } - } - vf = vf->sender(); - } - } else { - // no last java frame but there may be JNI locals - java_thread->active_handles()->oops_do(&blk); - } - return stack_depth; -} - - -// write a HPROF_GC_ROOT_THREAD_OBJ record for each java thread. Then walk -// the stack so that locals and JNI locals are dumped. -void VM_HeapDumper::do_threads() { - for (int i=0; i < _num_threads; i++) { - JavaThread* thread = _stack_traces[i]->thread(); - oop threadObj = thread->threadObj(); - u4 thread_serial_num = i+1; - u4 stack_serial_num = thread_serial_num + STACK_TRACE_ID; - u4 size = 1 + sizeof(address) + 4 + 4; - writer()->start_sub_record(HPROF_GC_ROOT_THREAD_OBJ, size); - writer()->write_objectID(threadObj); - writer()->write_u4(thread_serial_num); // thread number - writer()->write_u4(stack_serial_num); // stack trace serial number - writer()->end_sub_record(); - int num_frames = do_thread(thread, thread_serial_num); - assert(num_frames == _stack_traces[i]->get_stack_depth(), - "total number of Java frames not matched"); - } } bool VM_HeapDumper::doit_prologue() { @@ -2100,6 +2302,8 @@ void VM_HeapDumper::work(uint worker_id) { // this must be called after _klass_map is built when iterating the classes above. dump_stack_traces(); + // HPROF_HEAP_DUMP/HPROF_HEAP_DUMP_SEGMENT starts here + // Writes HPROF_GC_CLASS_DUMP records { LockedClassesDo locked_dump_class(&do_class_dump); @@ -2107,7 +2311,7 @@ void VM_HeapDumper::work(uint worker_id) { } // HPROF_GC_ROOT_THREAD_OBJ + frames + jni locals - do_threads(); + dump_threads(); // HPROF_GC_ROOT_JNI_GLOBAL JNIGlobalsDumper jni_dumper(writer()); @@ -2163,58 +2367,44 @@ void VM_HeapDumper::work(uint worker_id) { void VM_HeapDumper::dump_stack_traces() { // write a HPROF_TRACE record without any frames to be referenced as object alloc sites - DumperSupport::write_header(writer(), HPROF_TRACE, 3*sizeof(u4)); - writer()->write_u4((u4) STACK_TRACE_ID); + DumperSupport::write_header(writer(), HPROF_TRACE, 3 * sizeof(u4)); + writer()->write_u4((u4)STACK_TRACE_ID); writer()->write_u4(0); // thread number writer()->write_u4(0); // frame count - _stack_traces = NEW_C_HEAP_ARRAY(ThreadStackTrace*, Threads::number_of_threads(), mtInternal); - int frame_serial_num = 0; - for (JavaThreadIteratorWithHandle jtiwh; JavaThread *thread = jtiwh.next(); ) { - oop threadObj = thread->threadObj(); - if (threadObj != nullptr && !thread->is_exiting() && !thread->is_hidden_from_external_view()) { - // dump thread stack trace - Thread* current_thread = Thread::current(); - ResourceMark rm(current_thread); - HandleMark hm(current_thread); + // max number if every platform thread is carrier with mounted virtual thread + _thread_dumpers = NEW_C_HEAP_ARRAY(ThreadDumper*, Threads::number_of_threads() * 2, mtInternal); - ThreadStackTrace* stack_trace = new ThreadStackTrace(thread, false); - stack_trace->dump_stack_at_safepoint(-1, /* ObjectMonitorsHashtable is not needed here */ nullptr, true); - _stack_traces[_num_threads++] = stack_trace; + for (JavaThreadIteratorWithHandle jtiwh; JavaThread * thread = jtiwh.next(); ) { + if (ThreadDumper::should_dump_pthread(thread)) { + bool add_oom_frame = is_oom_thread(thread); - // write HPROF_FRAME records for this thread's stack trace - int depth = stack_trace->get_stack_depth(); - int thread_frame_start = frame_serial_num; - int extra_frames = 0; - // write fake frame that makes it look like the thread, which caused OOME, - // is in the OutOfMemoryError zero-parameter constructor - if (thread == _oome_thread && _oome_constructor != nullptr) { - int oome_serial_num = _klass_map->find(_oome_constructor->method_holder()); - // the class serial number starts from 1 - assert(oome_serial_num > 0, "OutOfMemoryError class not found"); - DumperSupport::dump_stack_frame(writer(), ++frame_serial_num, oome_serial_num, - _oome_constructor, 0); - extra_frames++; + oop mounted_vt = thread->is_vthread_mounted() ? thread->vthread() : nullptr; + if (mounted_vt != nullptr && !ThreadDumper::should_dump_vthread(mounted_vt)) { + mounted_vt = nullptr; } - for (int j=0; j < depth; j++) { - StackFrameInfo* frame = stack_trace->stack_frame_at(j); - Method* m = frame->method(); - int class_serial_num = _klass_map->find(m->method_holder()); - // the class serial number starts from 1 - assert(class_serial_num > 0, "class not found"); - DumperSupport::dump_stack_frame(writer(), ++frame_serial_num, class_serial_num, m, frame->bci()); - } - depth += extra_frames; - // write HPROF_TRACE record for one thread - DumperSupport::write_header(writer(), HPROF_TRACE, checked_cast(3*sizeof(u4) + depth*oopSize)); - int stack_serial_num = _num_threads + STACK_TRACE_ID; - writer()->write_u4(stack_serial_num); // stack trace serial number - writer()->write_u4((u4) _num_threads); // thread serial number - writer()->write_u4(depth); // frame count - for (int j=1; j <= depth; j++) { - writer()->write_id(thread_frame_start + j); + // mounted vthread (if any) + if (mounted_vt != nullptr) { + ThreadDumper* thread_dumper = new ThreadDumper(ThreadDumper::ThreadType::MountedVirtual, thread, mounted_vt); + _thread_dumpers[_thread_dumpers_count++] = thread_dumper; + if (add_oom_frame) { + thread_dumper->add_oom_frame(_oome_constructor); + // we add oom frame to the VT stack, don't add it to the carrier thread stack + add_oom_frame = false; + } + thread_dumper->init_serial_nums(&_thread_serial_num, &_frame_serial_num); + thread_dumper->dump_stack_traces(writer(), _klass_map); } + + // platform or carrier thread + ThreadDumper* thread_dumper = new ThreadDumper(ThreadDumper::ThreadType::Platform, thread, thread->threadObj()); + _thread_dumpers[_thread_dumpers_count++] = thread_dumper; + if (add_oom_frame) { + thread_dumper->add_oom_frame(_oome_constructor); + } + thread_dumper->init_serial_nums(&_thread_serial_num, &_frame_serial_num); + thread_dumper->dump_stack_traces(writer(), _klass_map); } } } diff --git a/test/hotspot/jtreg/serviceability/jvmti/vthread/HeapDump/VThreadInHeapDump.java b/test/hotspot/jtreg/serviceability/jvmti/vthread/HeapDump/VThreadInHeapDump.java new file mode 100644 index 00000000000..ec9362bf53f --- /dev/null +++ b/test/hotspot/jtreg/serviceability/jvmti/vthread/HeapDump/VThreadInHeapDump.java @@ -0,0 +1,300 @@ +/* + * Copyright (c) 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. + */ + +import java.io.File; +import java.lang.ref.Reference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jdk.test.lib.Asserts; +import jdk.test.lib.JDKToolLauncher; +import jdk.test.lib.apps.LingeredApp; +import jdk.test.lib.process.ProcessTools; + +import jdk.test.lib.hprof.model.JavaClass; +import jdk.test.lib.hprof.model.JavaHeapObject; +import jdk.test.lib.hprof.model.Root; +import jdk.test.lib.hprof.model.Snapshot; +import jdk.test.lib.hprof.model.StackFrame; +import jdk.test.lib.hprof.model.StackTrace; +import jdk.test.lib.hprof.model.ThreadObject; +import jdk.test.lib.hprof.parser.Reader; + +/** + * @test id=default + * @requires vm.jvmti + * @requires vm.continuations + * @library /test/lib + * @run main VThreadInHeapDump + */ + +/** + * @test id=no-vmcontinuations + * @requires vm.jvmti + * @library /test/lib + * @comment pass extra VM arguments as the test arguments + * @run main VThreadInHeapDump + * -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations + */ + +class VThreadInHeapDumpTarg extends LingeredApp { + + public static class VThreadUnmountedReferenced { + } + public static class VThreadMountedReferenced { + } + public static class PThreadReferenced { + } + + public class ThreadBase { + private volatile boolean threadReady = false; + + protected void ready() { + threadReady = true; + } + + public void waitReady() { + while (!threadReady) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + } + } + } + } + + public class VthreadUnmounted extends ThreadBase implements Runnable { + public void run() { + Object referenced = new VThreadUnmountedReferenced(); + ready(); + // The thread will be unmounted in awaitToStop(). + awaitToStop(); + Reference.reachabilityFence(referenced); + } + } + + public class VthreadMounted extends ThreadBase implements Runnable { + int dummy = -1; + + public void run() { + Object referenced = new VThreadMountedReferenced(); + ready(); + // Don't give a chance for the thread to unmount. + while (!timeToStop) { + if (++dummy == 10000) { + dummy = 0; + } + } + Reference.reachabilityFence(referenced); + } + } + + public class Pthread extends ThreadBase implements Runnable { + public void run() { + Object referenced = new PThreadReferenced(); + ready(); + awaitToStop(); + Reference.reachabilityFence(referenced); + } + } + + CountDownLatch timeToStopLatch = new CountDownLatch(1); + volatile boolean timeToStop = false; + + void awaitToStop() { + try { + timeToStopLatch.await(); + } catch (InterruptedException e) { + } + } + + private void runTest(String[] args) { + try { + // Unmounted virtual thread. + VthreadUnmounted vthreadUnmounted = new VthreadUnmounted(); + Thread.ofVirtual().start(vthreadUnmounted); + vthreadUnmounted.waitReady(); + + // Mounted virtual thread. + VthreadMounted vthreadMounted = new VthreadMounted(); + Thread.ofVirtual().start(vthreadMounted); + vthreadMounted.waitReady(); + + // Platform thread. + Pthread pthread = new Pthread(); + Thread.ofPlatform().start(pthread); + pthread.waitReady(); + + // We are ready. + LingeredApp.main(args); + + } finally { + // Signal all threads to finish. + timeToStop = true; + timeToStopLatch.countDown(); + } + } + + public static void main(String[] args) { + VThreadInHeapDumpTarg test = new VThreadInHeapDumpTarg(); + test.runTest(args); + } + +} + + +public class VThreadInHeapDump { + + // test arguments are extra VM options for target process + public static void main(String[] args) throws Exception { + File dumpFile = new File("Myheapdump.hprof"); + createDump(dumpFile, args); + verifyDump(dumpFile); + } + + private static void createDump(File dumpFile, String[] extraOptions) throws Exception { + LingeredApp theApp = null; + try { + theApp = new VThreadInHeapDumpTarg(); + + List extraVMArgs = new ArrayList<>(); + extraVMArgs.add("-Djdk.virtualThreadScheduler.parallelism=1"); + extraVMArgs.addAll(Arrays.asList(extraOptions)); + LingeredApp.startApp(theApp, extraVMArgs.toArray(new String[0])); + + //jcmd GC.heap_dump + JDKToolLauncher launcher = JDKToolLauncher + .createUsingTestJDK("jcmd") + .addToolArg(Long.toString(theApp.getPid())) + .addToolArg("GC.heap_dump") + .addToolArg(dumpFile.getAbsolutePath()); + Process p = ProcessTools.startProcess("jcmd", new ProcessBuilder(launcher.getCommand())); + // If something goes wrong with heap dumping most likely we'll get crash of the target VM. + while (!p.waitFor(5, TimeUnit.SECONDS)) { + if (!theApp.getProcess().isAlive()) { + log("ERROR: target VM died, killing jcmd..."); + p.destroyForcibly(); + throw new Exception("Target VM died"); + } + } + + if (p.exitValue() != 0) { + throw new Exception("Jcmd exited with code " + p.exitValue()); + } + } finally { + LingeredApp.stopApp(theApp); + } + } + + private static void verifyDump(File dumpFile) throws Exception { + Asserts.assertTrue(dumpFile.exists(), "Heap dump file not found."); + + log("Reading " + dumpFile + "..."); + try (Snapshot snapshot = Reader.readFile(dumpFile.getPath(), true, 0)) { + log("Resolving snapshot..."); + snapshot.resolve(true); + log("Snapshot resolved."); + + // Log all threads with stack traces and stack references. + List threads = snapshot.getThreads(); + List roots = Collections.list(snapshot.getRoots()); + log("Threads:"); + for (ThreadObject thread: threads) { + StackTrace st = thread.getStackTrace(); + StackFrame[] frames = st.getFrames(); + log("thread " + thread.getIdString() + ", " + frames.length + " frames"); + + List stackRoots = findStackRoot(roots, thread); + for (int i = 0; i < frames.length; i++) { + log(" - [" + i + "] " + + frames[i].getClassName() + "." + frames[i].getMethodName() + + frames[i].getMethodSignature() + + " (" + frames[i].getSourceFileName() + + ":" + frames[i].getLineNumber() + ")"); + + for (Root r: stackRoots) { + StackFrame[] rootFrames = r.getStackTrace().getFrames(); + // the frame this local belongs to + StackFrame frame = rootFrames[rootFrames.length - 1]; + if (frame == frames[i]) { + JavaHeapObject obj = snapshot.findThing(r.getId()); + JavaClass objClass = obj.getClazz(); + log(" " + r.getDescription() + ": " + objClass.getName()); + } + } + } + } + + // Verify objects from thread stacks are dumped. + test(snapshot, VThreadInHeapDumpTarg.VThreadMountedReferenced.class); + test(snapshot, VThreadInHeapDumpTarg.PThreadReferenced.class); + // Dumping of unmounted vthreads is not implemented yet + //test(snapshot, VThreadInHeapDumpTarg.VThreadUnmountedReferenced.class); + } + + } + + private static List findStackRoot(List roots, ThreadObject thread) { + List result = new ArrayList<>(); + for (Root root: roots) { + if (root.getRefererId() == thread.getId()) { + result.add(root); + } + } + return result; + } + + private static void test(Snapshot snapshot, String className) { + log("Testing " + className + "..."); + JavaClass jClass = snapshot.findClass(className); + if (jClass == null) { + throw new RuntimeException("'" + className + "' not found"); + } + int instanceCount = jClass.getInstancesCount(false); + if (instanceCount != 1) { + throw new RuntimeException("Expected 1 instance, " + instanceCount + " instances found"); + } + // There is the only instance. + JavaHeapObject heapObj = jClass.getInstances(false).nextElement(); + + Root root = heapObj.getRoot(); + if (root == null) { + throw new RuntimeException("No root for " + className + " instance"); + } + log(" root: " + root.getDescription()); + JavaHeapObject referrer = root.getReferer(); + log(" referrer: " + referrer); + } + + private static void test(Snapshot snapshot, Class cls) { + test(snapshot, cls.getName()); + } + + private static void log(Object s) { + System.out.println(s); + } +} diff --git a/test/lib/jdk/test/lib/hprof/model/Root.java b/test/lib/jdk/test/lib/hprof/model/Root.java index b223c1370a5..f8375ed6c4c 100644 --- a/test/lib/jdk/test/lib/hprof/model/Root.java +++ b/test/lib/jdk/test/lib/hprof/model/Root.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 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 @@ -141,6 +141,10 @@ public class Root { return referer; } + public long getRefererId() { + return refererId; + } + /** * @return the stack trace responsible for this root, or null if there * is none. diff --git a/test/lib/jdk/test/lib/hprof/model/Snapshot.java b/test/lib/jdk/test/lib/hprof/model/Snapshot.java index 05902d7028e..219f8011794 100644 --- a/test/lib/jdk/test/lib/hprof/model/Snapshot.java +++ b/test/lib/jdk/test/lib/hprof/model/Snapshot.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 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 @@ -82,6 +82,9 @@ public class Snapshot implements AutoCloseable { // soft cache of finalizeable objects - lazily initialized private SoftReference> finalizablesCache; + // threads + private ArrayList threads = new ArrayList<>(); + // represents null reference private JavaThing nullThing; @@ -175,6 +178,10 @@ public class Snapshot implements AutoCloseable { putInClassesMap(c); } + public void addThreadObject(ThreadObject thread) { + threads.add(thread); + } + JavaClass addFakeInstanceClass(long classID, int instSize) { // Create a fake class name based on ID. String name = "unknown-class<@" + Misc.toHex(classID) + ">"; @@ -433,6 +440,10 @@ public class Snapshot implements AutoCloseable { return roots.elementAt(i); } + public List getThreads() { + return Collections.unmodifiableList(threads); + } + public ReferenceChain[] rootsetReferencesTo(JavaHeapObject target, boolean includeWeak) { Vector fifo = new Vector(); // This is slow... A real fifo would help diff --git a/test/lib/jdk/test/lib/hprof/model/ThreadObject.java b/test/lib/jdk/test/lib/hprof/model/ThreadObject.java new file mode 100644 index 00000000000..86a3d1c5fe1 --- /dev/null +++ b/test/lib/jdk/test/lib/hprof/model/ThreadObject.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 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.test.lib.hprof.model; + +import jdk.test.lib.hprof.util.Misc; + +public class ThreadObject { + + private final long id; // ID of the JavaThing we refer to + private final StackTrace stackTrace; + + public ThreadObject(long id, StackTrace stackTrace) { + this.id = id; + this.stackTrace = stackTrace; + } + + public long getId() { + return id; + } + + public String getIdString() { + return Misc.toHex(id); + } + + public StackTrace getStackTrace() { + return stackTrace; + } + + void resolve(Snapshot ss) { + if (stackTrace != null) { + stackTrace.resolve(ss); + } + } + +} diff --git a/test/lib/jdk/test/lib/hprof/parser/HprofReader.java b/test/lib/jdk/test/lib/hprof/parser/HprofReader.java index ac766636771..08deac80c0e 100644 --- a/test/lib/jdk/test/lib/hprof/parser/HprofReader.java +++ b/test/lib/jdk/test/lib/hprof/parser/HprofReader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 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 @@ -436,8 +436,10 @@ public class HprofReader extends Reader /* imports */ implements ArrayTypeCodes int threadSeq = in.readInt(); int stackSeq = in.readInt(); bytesLeft -= identifierSize + 8; - threadObjects.put(threadSeq, - new ThreadObject(id, stackSeq)); + StackTrace st = getStackTraceFromSerial(stackSeq); + ThreadObject threadObj = new ThreadObject(id, st); + threadObjects.put(threadSeq, threadObj); + snapshot.addThreadObject(threadObj); break; } case HPROF_GC_ROOT_JNI_GLOBAL: { @@ -453,11 +455,11 @@ public class HprofReader extends Reader /* imports */ implements ArrayTypeCodes int depth = in.readInt(); bytesLeft -= identifierSize + 8; ThreadObject to = getThreadObjectFromSequence(threadSeq); - StackTrace st = getStackTraceFromSerial(to.stackSeq); + StackTrace st = to.getStackTrace(); if (st != null) { st = st.traceForDepth(depth+1); } - snapshot.addRoot(new Root(id, to.threadId, + snapshot.addRoot(new Root(id, to.getId(), Root.NATIVE_LOCAL, "", st)); break; } @@ -467,11 +469,11 @@ public class HprofReader extends Reader /* imports */ implements ArrayTypeCodes int depth = in.readInt(); bytesLeft -= identifierSize + 8; ThreadObject to = getThreadObjectFromSequence(threadSeq); - StackTrace st = getStackTraceFromSerial(to.stackSeq); + StackTrace st = to.getStackTrace();; if (st != null) { st = st.traceForDepth(depth+1); } - snapshot.addRoot(new Root(id, to.threadId, + snapshot.addRoot(new Root(id, to.getId(), Root.JAVA_LOCAL, "", st)); break; } @@ -480,8 +482,8 @@ public class HprofReader extends Reader /* imports */ implements ArrayTypeCodes int threadSeq = in.readInt(); bytesLeft -= identifierSize + 4; ThreadObject to = getThreadObjectFromSequence(threadSeq); - StackTrace st = getStackTraceFromSerial(to.stackSeq); - snapshot.addRoot(new Root(id, to.threadId, + StackTrace st = to.getStackTrace();; + snapshot.addRoot(new Root(id, to.getId(), Root.NATIVE_STACK, "", st)); break; } @@ -496,8 +498,8 @@ public class HprofReader extends Reader /* imports */ implements ArrayTypeCodes int threadSeq = in.readInt(); bytesLeft -= identifierSize + 4; ThreadObject to = getThreadObjectFromSequence(threadSeq); - StackTrace st = getStackTraceFromSerial(to.stackSeq); - snapshot.addRoot(new Root(id, to.threadId, + StackTrace st = to.getStackTrace(); + snapshot.addRoot(new Root(id, to.getId(), Root.THREAD_BLOCK, "", st)); break; } @@ -913,18 +915,4 @@ public class HprofReader extends Reader /* imports */ implements ArrayTypeCodes System.out.println("WARNING: " + msg); } - // - // A trivial data-holder class for HPROF_GC_ROOT_THREAD_OBJ. - // - private class ThreadObject { - - long threadId; - int stackSeq; - - ThreadObject(long threadId, int stackSeq) { - this.threadId = threadId; - this.stackSeq = stackSeq; - } - } - }