diff --git a/ddprof-lib/src/main/cpp/common.h b/ddprof-lib/src/main/cpp/common.h new file mode 100644 index 00000000..c51ec40c --- /dev/null +++ b/ddprof-lib/src/main/cpp/common.h @@ -0,0 +1,13 @@ +#ifndef _COMMON_H +#define _COMMON_H + +#ifdef DEBUG +#define TEST_LOG(fmt, ...) do { \ + fprintf(stdout, "[TEST::INFO] " fmt "\n", ##__VA_ARGS__); \ + fflush(stdout); \ +} while (0) +#else +#define TEST_LOG(fmt, ...) // No-op in non-debug mode +#endif + +#endif // _COMMON_H \ No newline at end of file diff --git a/ddprof-lib/src/main/cpp/ctimer_linux.cpp b/ddprof-lib/src/main/cpp/ctimer_linux.cpp index ac474c0c..a28d1626 100644 --- a/ddprof-lib/src/main/cpp/ctimer_linux.cpp +++ b/ddprof-lib/src/main/cpp/ctimer_linux.cpp @@ -18,6 +18,7 @@ #include "ctimer.h" #include "debugSupport.h" +#include "libraries.h" #include "profiler.h" #include "vmStructs.h" #include @@ -64,7 +65,7 @@ static void **lookupThreadEntry() { // Depending on Zing version, pthread_setspecific is called either from // libazsys.so or from libjvm.so if (VM::isZing()) { - CodeCache *libazsys = Profiler::instance()->findLibraryByName("libazsys"); + CodeCache *libazsys = Libraries::instance()->findLibraryByName("libazsys"); if (libazsys != NULL) { void **entry = libazsys->findImport(im_pthread_setspecific); if (entry != NULL) { @@ -73,7 +74,7 @@ static void **lookupThreadEntry() { } } - CodeCache *lib = Profiler::instance()->findJvmLibrary("libj9thr"); + CodeCache *lib = Libraries::instance()->findJvmLibrary("libj9thr"); return lib != NULL ? lib->findImport(im_pthread_setspecific) : NULL; } diff --git a/ddprof-lib/src/main/cpp/javaApi.cpp b/ddprof-lib/src/main/cpp/javaApi.cpp index 13e0c55a..8b61c25a 100644 --- a/ddprof-lib/src/main/cpp/javaApi.cpp +++ b/ddprof-lib/src/main/cpp/javaApi.cpp @@ -24,6 +24,7 @@ #include "profiler.h" #include "thread.h" #include "tsc.h" +#include "vmEntry.h" #include "vmStructs.h" #include "wallClock.h" #include @@ -59,6 +60,12 @@ class JniString { int length() const { return _length; } }; +extern "C" DLLEXPORT jboolean JNICALL +Java_com_datadoghq_profiler_JavaProfiler_init0(JNIEnv *env, jclass unused) { + // JavaVM* has already been stored when the native library was loaded so we can pass nullptr here + return VM::initProfilerBridge(nullptr, true); +} + extern "C" DLLEXPORT void JNICALL Java_com_datadoghq_profiler_JavaProfiler_stop0(JNIEnv *env, jobject unused) { Error error = Profiler::instance()->stop(); @@ -283,3 +290,82 @@ Java_com_datadoghq_profiler_JavaProfiler_mallocArenaMax0(JNIEnv *env, jint maxArenas) { OS::mallocArenaMax(maxArenas); } + +extern "C" DLLEXPORT jstring JNICALL +Java_com_datadoghq_profiler_JVMAccess_findStringJVMFlag0(JNIEnv *env, + jobject unused, + jstring flagName) { + JniString flag_str(env, flagName); + char** value = static_cast(JVMFlag::find(flag_str.c_str(), {JVMFlag::Type::String})); + if (value != NULL && *value != NULL) { + return env->NewStringUTF(*value); + } + return NULL; +} + +extern "C" DLLEXPORT void JNICALL +Java_com_datadoghq_profiler_JVMAccess_setStringJVMFlag0(JNIEnv *env, + jobject unused, + jstring flagName, + jstring flagValue) { + JniString flag_str(env, flagName); + JniString value_str(env, flagValue); + char** value = static_cast(JVMFlag::find(flag_str.c_str(), {JVMFlag::Type::String})); + if (value != NULL) { + *value = strdup(value_str.c_str()); + } +} + +extern "C" DLLEXPORT jboolean JNICALL +Java_com_datadoghq_profiler_JVMAccess_findBooleanJVMFlag0(JNIEnv *env, + jobject unused, + jstring flagName) { + JniString flag_str(env, flagName); + char* value = static_cast(JVMFlag::find(flag_str.c_str(), {JVMFlag::Type::Bool})); + if (value != NULL) { + return ((*value) & 0xff) == 1; + } + return false; +} + +extern "C" DLLEXPORT void JNICALL +Java_com_datadoghq_profiler_JVMAccess_setBooleanJVMFlag0(JNIEnv *env, + jobject unused, + jstring flagName, + jboolean flagValue) { + JniString flag_str(env, flagName); + char* value = static_cast(JVMFlag::find(flag_str.c_str(), {JVMFlag::Type::Bool})); + if (value != NULL) { + *value = flagValue ? 1 : 0; + } +} + +extern "C" DLLEXPORT jlong JNICALL +Java_com_datadoghq_profiler_JVMAccess_findIntJVMFlag0(JNIEnv *env, + jobject unused, + jstring flagName) { + JniString flag_str(env, flagName); + long* value = static_cast(JVMFlag::find(flag_str.c_str(), {JVMFlag::Type::Int, JVMFlag::Type::Uint, JVMFlag::Type::Intx, JVMFlag::Type::Uintx, JVMFlag::Type::Uint64_t, JVMFlag::Type::Size_t})); + if (value != NULL) { + return *value; + } + return 0; +} + +extern "C" DLLEXPORT jdouble JNICALL +Java_com_datadoghq_profiler_JVMAccess_findFloatJVMFlag0(JNIEnv *env, + jobject unused, + jstring flagName) { + JniString flag_str(env, flagName); + double* value = static_cast(JVMFlag::find(flag_str.c_str(),{ JVMFlag::Type::Double})); + if (value != NULL) { + return *value; + } + return 0.0; +} + +extern "C" DLLEXPORT jboolean JNICALL +Java_com_datadoghq_profiler_JVMAccess_healthCheck0(JNIEnv *env, + jobject unused) { + return true; +} diff --git a/ddprof-lib/src/main/cpp/libraries.cpp b/ddprof-lib/src/main/cpp/libraries.cpp new file mode 100644 index 00000000..31be6fab --- /dev/null +++ b/ddprof-lib/src/main/cpp/libraries.cpp @@ -0,0 +1,96 @@ +#include "codeCache.h" +#include "libraries.h" +#include "log.h" +#include "symbols.h" +#include "vmEntry.h" +#include "vmStructs.h" + +Libraries* Libraries::_instance = new Libraries(); + +void Libraries::mangle(const char *name, char *buf, size_t size) { + char *buf_end = buf + size; + strcpy(buf, "_ZN"); + buf += 3; + + const char *c; + while ((c = strstr(name, "::")) != NULL && buf + (c - name) + 4 < buf_end) { + int n = snprintf(buf, buf_end - buf, "%d", (int)(c - name)); + if (n < 0 || n >= buf_end - buf) { + if (n < 0) { + Log::debug("Error in snprintf."); + } + goto end; + } + buf += n; + memcpy(buf, name, c - name); + buf += c - name; + name = c + 2; + } + if (buf < buf_end) { + snprintf(buf, buf_end - buf, "%d%sE*", (int)strlen(name), name); + } + +end: + buf_end[-1] = '\0'; +} + +void Libraries::updateSymbols(bool kernel_symbols) { + Symbols::parseLibraries(&_native_libs, kernel_symbols); +} + +const void *Libraries::resolveSymbol(const char *name) { + char mangled_name[256]; + if (strstr(name, "::") != NULL) { + mangle(name, mangled_name, sizeof(mangled_name)); + name = mangled_name; + } + + size_t len = strlen(name); + int native_lib_count = _native_libs.count(); + if (len > 0 && name[len - 1] == '*') { + for (int i = 0; i < native_lib_count; i++) { + const void *address = _native_libs[i]->findSymbolByPrefix(name, len - 1); + if (address != NULL) { + return address; + } + } + } else { + for (int i = 0; i < native_lib_count; i++) { + const void *address = _native_libs[i]->findSymbol(name); + if (address != NULL) { + return address; + } + } + } + + return NULL; +} + +CodeCache *Libraries::findJvmLibrary(const char *j9_lib_name) { + return VM::isOpenJ9() ? findLibraryByName(j9_lib_name) : VMStructs::libjvm(); +} + +CodeCache *Libraries::findLibraryByName(const char *lib_name) { + const size_t lib_name_len = strlen(lib_name); + const int native_lib_count = _native_libs.count(); + for (int i = 0; i < native_lib_count; i++) { + const char *s = _native_libs[i]->name(); + if (s != NULL) { + const char *p = strrchr(s, '/'); + if (p != NULL && strncmp(p + 1, lib_name, lib_name_len) == 0) { + return _native_libs[i]; + } + } + } + return NULL; +} + +CodeCache *Libraries::findLibraryByAddress(const void *address) { + const int native_lib_count = _native_libs.count(); + for (int i = 0; i < native_lib_count; i++) { + if (_native_libs[i]->contains(address)) { + return _native_libs[i]; + } + } + return NULL; +} \ No newline at end of file diff --git a/ddprof-lib/src/main/cpp/libraries.h b/ddprof-lib/src/main/cpp/libraries.h new file mode 100644 index 00000000..db7a1cd8 --- /dev/null +++ b/ddprof-lib/src/main/cpp/libraries.h @@ -0,0 +1,27 @@ +#ifndef _LIBRARIES_H +#define _LIBRARIES_H + +#include "codeCache.h" + +class Libraries { + private: + static Libraries * _instance; + + CodeCacheArray _native_libs; + CodeCache _runtime_stubs; + + static void mangle(const char *name, char *buf, size_t size); + public: + Libraries() : _native_libs(), _runtime_stubs("runtime stubs") {} + void updateSymbols(bool kernel_symbols); + const void *resolveSymbol(const char *name); + // In J9 the 'libjvm' functionality is spread across multiple libraries + // This function will return the 'libjvm' on non-J9 VMs and the library with the given name on J9 VMs + CodeCache *findJvmLibrary(const char *j9_lib_name); + CodeCache *findLibraryByName(const char *lib_name); + CodeCache *findLibraryByAddress(const void *address); + + static Libraries *instance() { return _instance; } +}; + +#endif // _LIBRARIES_H \ No newline at end of file diff --git a/ddprof-lib/src/main/cpp/log.cpp b/ddprof-lib/src/main/cpp/log.cpp index c0f6414d..14fd1c25 100644 --- a/ddprof-lib/src/main/cpp/log.cpp +++ b/ddprof-lib/src/main/cpp/log.cpp @@ -85,7 +85,7 @@ void Log::log(LogLevel level, const char *msg, va_list args) { // be done at WARN level, and any logging done which prevents creation of the // JFR should be done at ERROR level if (level == LOG_WARN || (level >= _level && level < LOG_ERROR)) { - Profiler::instance()->writeLog(level, buf, len); +// Profiler::instance()->writeLog(level, buf, len); } // always log errors, but only errors diff --git a/ddprof-lib/src/main/cpp/perfEvents_linux.cpp b/ddprof-lib/src/main/cpp/perfEvents_linux.cpp index 5a5fe4ca..10df8696 100644 --- a/ddprof-lib/src/main/cpp/perfEvents_linux.cpp +++ b/ddprof-lib/src/main/cpp/perfEvents_linux.cpp @@ -19,6 +19,7 @@ #include "arch.h" #include "context.h" #include "debugSupport.h" +#include "libraries.h" #include "log.h" #include "os.h" #include "perfEvents.h" @@ -188,7 +189,7 @@ static void **lookupThreadEntry() { // Depending on Zing version, pthread_setspecific is called either from // libazsys.so or from libjvm.so if (VM::isZing()) { - CodeCache *libazsys = Profiler::instance()->findLibraryByName("libazsys"); + CodeCache *libazsys = Libraries::instance()->findLibraryByName("libazsys"); if (libazsys != NULL) { void **entry = libazsys->findImport(im_pthread_setspecific); if (entry != NULL) { @@ -197,7 +198,7 @@ static void **lookupThreadEntry() { } } - CodeCache *lib = Profiler::instance()->findJvmLibrary("libj9thr"); + CodeCache *lib = Libraries::instance()->findJvmLibrary("libj9thr"); return lib != NULL ? lib->findImport(im_pthread_setspecific) : NULL; } @@ -295,7 +296,7 @@ struct PerfEventType { } else { addr = (__u64)(uintptr_t)dlsym(RTLD_DEFAULT, buf); if (addr == 0) { - addr = (__u64)(uintptr_t)Profiler::instance()->resolveSymbol(buf); + addr = (__u64)(uintptr_t)Libraries::instance()->resolveSymbol(buf); } if (c == NULL) { // If offset is not specified explicitly, add the default breakpoint @@ -794,7 +795,7 @@ Error PerfEvents::check(Arguments &args) { if (!(_ring & RING_KERNEL)) { attr.exclude_kernel = 1; } else if (!Symbols::haveKernelSymbols()) { - Profiler::instance()->updateSymbols(true); + Libraries::instance()->updateSymbols(true); attr.exclude_kernel = Symbols::haveKernelSymbols() ? 0 : 1; } if (!(_ring & RING_USER)) { diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 46cd5e46..cf02e549 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -185,10 +185,6 @@ inline u32 Profiler::getLockIndex(int tid) { return lock_index % CONCURRENCY_LEVEL; } -void Profiler::updateSymbols(bool kernel_symbols) { - Symbols::parseLibraries(&_native_libs, kernel_symbols); -} - void Profiler::mangle(const char *name, char *buf, size_t size) { char *buf_end = buf + size; strcpy(buf, "_ZN"); @@ -257,37 +253,8 @@ const char *Profiler::getLibraryName(const char *native_symbol) { return NULL; } -CodeCache *Profiler::findJvmLibrary(const char *lib_name) { - return VM::isOpenJ9() ? findLibraryByName(lib_name) : VMStructs::libjvm(); -} - -CodeCache *Profiler::findLibraryByName(const char *lib_name) { - const size_t lib_name_len = strlen(lib_name); - const int native_lib_count = _native_libs.count(); - for (int i = 0; i < native_lib_count; i++) { - const char *s = _native_libs[i]->name(); - if (s != NULL) { - const char *p = strrchr(s, '/'); - if (p != NULL && strncmp(p + 1, lib_name, lib_name_len) == 0) { - return _native_libs[i]; - } - } - } - return NULL; -} - -CodeCache *Profiler::findLibraryByAddress(const void *address) { - const int native_lib_count = _native_libs.count(); - for (int i = 0; i < native_lib_count; i++) { - if (_native_libs[i]->contains(address)) { - return _native_libs[i]; - } - } - return NULL; -} - const char *Profiler::findNativeMethod(const void *address) { - CodeCache *lib = findLibraryByAddress(address); + CodeCache *lib = _libs->findLibraryByAddress(address); return lib == NULL ? NULL : lib->binarySearch(address); } @@ -300,7 +267,7 @@ bool Profiler::isAddressInCode(const void *pc) { return CodeHeap::findNMethod(pc) != NULL && !(pc >= _call_stub_begin && pc < _call_stub_end); } else { - return findLibraryByAddress(pc) != NULL; + return _libs->findLibraryByAddress(pc) != NULL; } } @@ -534,7 +501,7 @@ int Profiler::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, m->setFrameCompleteOffset(0); } VM::_asyncGetCallTrace(&trace, max_depth, ucontext); - } else if (findLibraryByAddress((const void *)pc) != NULL) { + } else if (_libs->findLibraryByAddress((const void *)pc) != NULL) { VM::_asyncGetCallTrace(&trace, max_depth, ucontext); } @@ -875,7 +842,9 @@ void Profiler::writeHeapUsage(long value, bool live) { void *Profiler::dlopen_hook(const char *filename, int flags) { void *result = dlopen(filename, flags); if (result != NULL) { - instance()->updateSymbols(false); + // Static function of Profiler -> can not use the instance variable _libs + // Since Libraries is a singleton, this does not matter + Libraries::instance()->updateSymbols(false); } return result; } @@ -1126,7 +1095,7 @@ Error Profiler::checkJvmCapabilities() { } if (_dlopen_entry == NULL) { - CodeCache *lib = findJvmLibrary("libj9prt"); + CodeCache *lib = _libs->findJvmLibrary("libj9prt"); if (lib == NULL || (_dlopen_entry = lib->findImport(im_dlopen)) == NULL) { return Error("Could not set dlopen hook. Unsupported JVM?"); } @@ -1231,7 +1200,7 @@ Error Profiler::start(Arguments &args, bool reset) { } // Kernel symbols are useful only for perf_events without --all-user - updateSymbols(_cpu_engine == &perf_events && (args._ring & RING_KERNEL)); + _libs->updateSymbols(_cpu_engine == &perf_events && (args._ring & RING_KERNEL)); enableEngines(); diff --git a/ddprof-lib/src/main/cpp/profiler.h b/ddprof-lib/src/main/cpp/profiler.h index 66fe76f8..a28917c1 100644 --- a/ddprof-lib/src/main/cpp/profiler.h +++ b/ddprof-lib/src/main/cpp/profiler.h @@ -25,6 +25,7 @@ #include "engine.h" #include "event.h" #include "flightRecorder.h" +#include "libraries.h" #include "log.h" #include "mutex.h" #include "objectSampler.h" @@ -107,6 +108,7 @@ class Profiler { volatile jvmtiEventMode _thread_events_state; + Libraries* _libs; SpinLock _stubs_lock; CodeCache _runtime_stubs; CodeCacheArray _native_libs; @@ -167,7 +169,7 @@ class Profiler { _notify_class_unloaded_func(NULL), _thread_filter(), _call_trace_storage(), _jfr(), _start_time(0), _epoch(0), _timer_id(NULL), _max_stack_depth(0), _safe_mode(0), _thread_events_state(JVMTI_DISABLE), - _stubs_lock(), _runtime_stubs("[stubs]"), _native_libs(), + _libs(Libraries::instance()), _stubs_lock(), _runtime_stubs("[stubs]"), _native_libs(), _call_stub_begin(NULL), _call_stub_end(NULL), _dlopen_entry(NULL), _num_context_attributes(0), _class_map(1), _string_label_map(2), _context_value_map(3), _cpu_engine(), _alloc_engine(), _event_mask(0), @@ -179,7 +181,9 @@ class Profiler { } } - static Profiler *instance() { return _instance; } + static Profiler *instance() { + return _instance; + } u64 total_samples() { return _total_samples; } int max_stack_depth() { return _max_stack_depth; } @@ -235,12 +239,8 @@ class Profiler { void writeHeapUsage(long value, bool live); int eventMask() const { return _event_mask; } - void updateSymbols(bool kernel_symbols); const void *resolveSymbol(const char *name); const char *getLibraryName(const char *native_symbol); - CodeCache *findJvmLibrary(const char *lib_name); - CodeCache *findLibraryByName(const char *lib_name); - CodeCache *findLibraryByAddress(const void *address); const char *findNativeMethod(const void *address); CodeBlob *findRuntimeStub(const void *address); bool isAddressInCode(const void *pc); diff --git a/ddprof-lib/src/main/cpp/stackWalker.cpp b/ddprof-lib/src/main/cpp/stackWalker.cpp index d02f757d..5907cf90 100644 --- a/ddprof-lib/src/main/cpp/stackWalker.cpp +++ b/ddprof-lib/src/main/cpp/stackWalker.cpp @@ -16,6 +16,7 @@ #include "stackWalker.h" #include "dwarf.h" +#include "libraries.h" #include "profiler.h" #include "safeAccess.h" #include "stackFrame.h" @@ -143,7 +144,7 @@ int StackWalker::walkDwarf(void *ucontext, const void **callchain, } int depth = 0; - Profiler *profiler = Profiler::instance(); + Libraries *libraries = Libraries::instance(); *truncated = false; @@ -161,7 +162,7 @@ int StackWalker::walkDwarf(void *ucontext, const void **callchain, callchain[depth++] = pc; prev_sp = sp; - CodeCache *cc = profiler->findLibraryByAddress(pc); + CodeCache *cc = libraries->findLibraryByAddress(pc); FrameDesc *f = cc != NULL ? cc->findFrameDesc(pc) : &FrameDesc::default_frame; @@ -241,6 +242,8 @@ int StackWalker::walkVM(void *ucontext, ASGCT_CallFrame *frames, int max_depth, } Profiler *profiler = Profiler::instance(); + Libraries *libraries = Libraries::instance(); + int bcp_offset = InterpreterFrame::bcp_offset(); jmp_buf crash_protection_ctx; @@ -382,7 +385,7 @@ int StackWalker::walkVM(void *ucontext, ASGCT_CallFrame *frames, int max_depth, } } else { if (cc == NULL || !cc->contains(pc)) { - cc = profiler->findLibraryByAddress(pc); + cc = libraries->findLibraryByAddress(pc); } const char *name = cc == NULL ? NULL : cc->binarySearch(pc); fillFrame(frames[depth++], BCI_NATIVE_FRAME, name); @@ -394,7 +397,7 @@ int StackWalker::walkVM(void *ucontext, ASGCT_CallFrame *frames, int max_depth, break; } if (cc == NULL || !cc->contains(pc)) { - cc = profiler->findLibraryByAddress(pc); + cc = libraries->findLibraryByAddress(pc); } FrameDesc *f = cc != NULL ? cc->findFrameDesc(pc) : &FrameDesc::default_frame; diff --git a/ddprof-lib/src/main/cpp/vmEntry.cpp b/ddprof-lib/src/main/cpp/vmEntry.cpp index 1baaadf0..4190b555 100644 --- a/ddprof-lib/src/main/cpp/vmEntry.cpp +++ b/ddprof-lib/src/main/cpp/vmEntry.cpp @@ -15,11 +15,13 @@ * limitations under the License. */ +#include "common.h" #include "vmEntry.h" #include "arguments.h" #include "context.h" #include "j9Ext.h" #include "jniHelper.h" +#include "libraries.h" #include "log.h" #include "os.h" #include "profiler.h" @@ -143,7 +145,20 @@ int JavaVersionAccess::get_hotspot_version(char* prop_value) { return hs_version; } -bool VM::init(JavaVM *vm, bool attach) { +CodeCache* VM::openJvmLibrary() { + if ((void*)_asyncGetCallTrace == nullptr) { + return nullptr; + } + + Libraries* libraries = Libraries::instance(); + CodeCache *lib = + isOpenJ9() + ? libraries->findJvmLibrary("libj9vm") + : libraries->findLibraryByAddress((const void *)_asyncGetCallTrace); + return lib; +} + +bool VM::initShared(JavaVM* vm) { if (_jvmti != NULL) return true; @@ -181,11 +196,11 @@ bool VM::init(JavaVM *vm, bool attach) { _asyncGetCallTrace = (AsyncGetCallTrace)dlsym(_libjvm, "AsyncGetCallTrace"); _getManagement = (JVM_GetManagement)dlsym(_libjvm, "JVM_GetManagement"); - Profiler *profiler = Profiler::instance(); - profiler->updateSymbols(false); + Libraries *libraries = Libraries::instance(); + libraries->updateSymbols(false); _openj9 = !_hotspot && J9Ext::initialize( - _jvmti, profiler->resolveSymbol("j9thread_self*")); + _jvmti, libraries->resolveSymbol("j9thread_self*")); if (_openj9) { if (_jvmti->GetSystemProperty("jdk.extensions.version", &prop) == 0) { @@ -227,12 +242,6 @@ bool VM::init(JavaVM *vm, bool attach) { prop = NULL; } if (_jvmti->GetSystemProperty("java.vm.version", &prop) == 0) { - if (_java_version == 0) { - // java.runtime.version was not found; try using java.vm.version to extract the information - JavaFullVersion version = JavaVersionAccess::get_java_version(prop); - _java_version = version.major; - _java_update_version = version.update; - } _hotspot_version = JavaVersionAccess::get_hotspot_version(prop); _jvmti->Deallocate((unsigned char *)prop); prop = NULL; @@ -248,13 +257,8 @@ bool VM::init(JavaVM *vm, bool attach) { _java_version = _hotspot_version; } - _can_sample_objects = !_hotspot || hotspot_version() >= 11; - - CodeCache *lib = - isOpenJ9() - ? profiler->findJvmLibrary("libj9vm") - : profiler->findLibraryByAddress((const void *)_asyncGetCallTrace); - if (lib == NULL) { + CodeCache *lib = openJvmLibrary(); + if (lib == nullptr) { return false; } @@ -263,11 +267,33 @@ bool VM::init(JavaVM *vm, bool attach) { lib->mark(isZeroInterpreterMethod); } else if (isOpenJ9()) { lib->mark(isOpenJ9InterpreterMethod); - CodeCache *libjit = profiler->findJvmLibrary("libj9jit"); + CodeCache *libjit = libraries->findJvmLibrary("libj9jit"); if (libjit != NULL) { libjit->mark(isOpenJ9JitStub); } } + return true; +} + +bool VM::initLibrary(JavaVM *vm) { + TEST_LOG("VM::initLibrary"); + if (!initShared(vm)) { + return false; + } + ready(jvmti(), jni()); + return true; +} + +bool VM::initProfilerBridge(JavaVM *vm, bool attach) { + TEST_LOG("VM::initProfilerBridge"); + if (!initShared(vm)) { + return false; + } + + CodeCache *lib = openJvmLibrary(); + if (lib == nullptr) { + return false; + } if (!attach && hotspot_version() == 8 && OS::isLinux()) { // Workaround for JDK-8185348 @@ -279,10 +305,6 @@ bool VM::init(JavaVM *vm, bool attach) { } } - if (attach) { - ready(jvmti(), jni()); - } - jvmtiCapabilities potential_capabilities = {0}; _jvmti->GetPotentialCapabilities(&potential_capabilities); @@ -343,12 +365,12 @@ bool VM::init(JavaVM *vm, bool attach) { } else { // DebugNonSafepoints is automatically enabled with CompiledMethodLoad, // otherwise we set the flag manually - char *flag_addr = (char *)JVMFlag::find("DebugNonSafepoints"); + char *flag_addr = (char *)JVMFlag::find("DebugNonSafepoints", {JVMFlag::Type::Bool}); if (flag_addr != NULL) { *flag_addr = 1; } } - char *flag_addr = (char *)JVMFlag::find("KeepJNIIDs"); + char *flag_addr = (char *)JVMFlag::find("KeepJNIIDs", {JVMFlag::Type::Bool}); if (flag_addr != NULL) { *flag_addr = 1; } @@ -357,12 +379,19 @@ bool VM::init(JavaVM *vm, bool attach) { // profiler to avoid the risk of crashing flag was made obsolete (inert) in 15 // (see JDK-8228991) and removed in 16 (see JDK-8231560) if (hotspot_version() < 15) { - char *flag_addr = (char *)JVMFlag::find("UseAdaptiveGCBoundary"); + char *flag_addr = (char *)JVMFlag::find("UseAdaptiveGCBoundary", {JVMFlag::Type::Bool}); _is_adaptive_gc_boundary_flag_set = flag_addr != NULL && *flag_addr == 1; } + // Make sure we reload method IDs upon class retransformation + JVMTIFunctions *functions = *(JVMTIFunctions **)_jvmti; + _orig_RedefineClasses = functions->RedefineClasses; + _orig_RetransformClasses = functions->RetransformClasses; + functions->RedefineClasses = RedefineClassesHook; + functions->RetransformClasses = RetransformClassesHook; + if (attach) { - loadAllMethodIDs(jvmti(), jni()); + loadAllMethodIDs(_jvmti, jni()); _jvmti->GenerateEvents(JVMTI_EVENT_DYNAMIC_CODE_GENERATED); _jvmti->GenerateEvents(JVMTI_EVENT_COMPILED_METHOD_LOAD); } else { @@ -382,13 +411,6 @@ void VM::ready(jvmtiEnv *jvmti, JNIEnv *jni) { VMStructs::ready(); } _libjava = getLibraryHandle("libjava.so"); - - // Make sure we reload method IDs upon class retransformation - JVMTIFunctions *functions = *(JVMTIFunctions **)_jvmti; - _orig_RedefineClasses = functions->RedefineClasses; - _orig_RetransformClasses = functions->RetransformClasses; - functions->RedefineClasses = RedefineClassesHook; - functions->RetransformClasses = RetransformClassesHook; } void VM::applyPatch(char *func, const char *patch, const char *end_patch) { @@ -445,14 +467,14 @@ void VM::loadMethodIDs(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass) { } void VM::loadAllMethodIDs(jvmtiEnv *jvmti, JNIEnv *jni) { - jint class_count; - jclass *classes; - if (jvmti->GetLoadedClasses(&class_count, &classes) == 0) { - for (int i = 0; i < class_count; i++) { - loadMethodIDs(jvmti, jni, classes[i]); + jint class_count; + jclass *classes; + if (jvmti->GetLoadedClasses(&class_count, &classes) == 0) { + for (int i = 0; i < class_count; i++) { + loadMethodIDs(jvmti, jni, classes[i]); + } + jvmti->Deallocate((unsigned char *)classes); } - jvmti->Deallocate((unsigned char *)classes); - } } void JNICALL VM::VMInit(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread) { @@ -517,7 +539,7 @@ Agent_OnLoad(JavaVM* vm, char* options, void* reserved) { return ARGUMENTS_ERROR; } - if (!VM::init(vm, false)) { + if (!VM::initProfilerBridge(vm, false)) { Log::error("JVM does not support Tool Interface"); return COMMAND_ERROR; } @@ -526,7 +548,7 @@ Agent_OnLoad(JavaVM* vm, char* options, void* reserved) { } extern "C" DLLEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { - if (!VM::init(vm, true)) { + if (!VM::initLibrary(vm)) { return 0; } return JNI_VERSION_1_6; diff --git a/ddprof-lib/src/main/cpp/vmEntry.h b/ddprof-lib/src/main/cpp/vmEntry.h index a329db0e..14c4803e 100644 --- a/ddprof-lib/src/main/cpp/vmEntry.h +++ b/ddprof-lib/src/main/cpp/vmEntry.h @@ -20,6 +20,7 @@ #include +#include "codeCache.h" #include "frame.h" #ifdef __clang__ @@ -124,13 +125,18 @@ class VM { static void loadMethodIDs(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass); static void loadAllMethodIDs(jvmtiEnv *jvmti, JNIEnv *jni); + static bool initShared(JavaVM *vm); + + static CodeCache* openJvmLibrary(); + public: static void *_libjvm; static void *_libjava; static AsyncGetCallTrace _asyncGetCallTrace; static JVM_GetManagement _getManagement; - static bool init(JavaVM *vm, bool attach); + static bool initLibrary(JavaVM *vm); + static bool initProfilerBridge(JavaVM *vm, bool attach); static jvmtiEnv *jvmti() { return _jvmti; } diff --git a/ddprof-lib/src/main/cpp/vmStructs.cpp b/ddprof-lib/src/main/cpp/vmStructs.cpp index 42ad69cf..27d8ccd2 100644 --- a/ddprof-lib/src/main/cpp/vmStructs.cpp +++ b/ddprof-lib/src/main/cpp/vmStructs.cpp @@ -84,6 +84,7 @@ int VMStructs::_vs_low_bound_offset = -1; int VMStructs::_vs_high_bound_offset = -1; int VMStructs::_vs_low_offset = -1; int VMStructs::_vs_high_offset = -1; +int VMStructs::_flag_type_offset = -1; int VMStructs::_flag_name_offset = -1; int VMStructs::_flag_addr_offset = -1; const char *VMStructs::_flags_addr = NULL; @@ -321,7 +322,9 @@ void VMStructs::initOffsets() { _array_data_offset = *(int *)(entry + offset_offset); } } else if (strcmp(type, "JVMFlag") == 0 || strcmp(type, "Flag") == 0) { - if (strcmp(field, "_name") == 0 || strcmp(field, "name") == 0) { + if (strcmp(field, "_type") == 0 || strcmp(field, "type") == 0) { + _flag_type_offset = *(int *)(entry + offset_offset); + } else if (strcmp(field, "_name") == 0 || strcmp(field, "name") == 0) { _flag_name_offset = *(int *)(entry + offset_offset); } else if (strcmp(field, "_addr") == 0 || strcmp(field, "addr") == 0) { _flag_addr_offset = *(int *)(entry + offset_offset); @@ -781,6 +784,63 @@ void *JVMFlag::find(const char *name) { return NULL; } +void *JVMFlag::find(const char *name, std::initializer_list types) { + int mask = 0; + for (int type : types) { + mask |= 0x1 << type; + } + return find(name, mask); +} + +int JVMFlag::type() { + if (VM::hotspot_version() < 16) { // in JDK 16 the JVM flag implementation has changed + char* type_name = *(char **)at(_flag_type_offset); + if (type_name == NULL) { + return JVMFlag::Type::Unknown; + } + if (strcmp(type_name, "bool") == 0) { + return JVMFlag::Type::Bool; + } else if (strcmp(type_name, "int") == 0) { + return JVMFlag::Type::Int; + } else if (strcmp(type_name, "uint") == 0) { + return JVMFlag::Type::Uint; + } else if (strcmp(type_name, "intx") == 0) { + return JVMFlag::Type::Intx; + } else if (strcmp(type_name, "uintx") == 0) { + return JVMFlag::Type::Uintx; + } else if (strcmp(type_name, "uint64_t") == 0) { + return JVMFlag::Type::Uint64_t; + } else if (strcmp(type_name, "size_t") == 0) { + return JVMFlag::Type::Size_t; + } else if (strcmp(type_name, "double") == 0) { + return JVMFlag::Type::Double; + } else if (strcmp(type_name, "ccstr") == 0) { + return JVMFlag::Type::String; + } else if (strcmp(type_name, "ccstrlist") == 0) { + return JVMFlag::Type::Stringlist; + } else { + return JVMFlag::Type::Unknown; + } + } else { + return *(int *)at(_flag_type_offset); + } +} + +void *JVMFlag::find(const char *name, int type_mask) { + if (_flags_addr != NULL && _flag_size > 0) { + for (int i = 0; i < _flag_count; i++) { + JVMFlag *f = (JVMFlag *)(_flags_addr + i * _flag_size); + if (f->name() != NULL && strcmp(f->name(), name) == 0) { + int masked = 0x1 << f->type(); + if (masked & type_mask) { + return f->addr(); + } + } + } + } + return NULL; +} + int NMethod::findScopeOffset(const void *pc) { intptr_t pc_offset = (const char *)pc - code(); if (pc_offset < 0 || pc_offset > 0x7fffffff) { diff --git a/ddprof-lib/src/main/cpp/vmStructs.h b/ddprof-lib/src/main/cpp/vmStructs.h index 206bcb01..8b277984 100644 --- a/ddprof-lib/src/main/cpp/vmStructs.h +++ b/ddprof-lib/src/main/cpp/vmStructs.h @@ -24,6 +24,8 @@ #include "safeAccess.h" #include "threadState.h" #include "vmEntry.h" + +#include #include #include #include @@ -92,6 +94,7 @@ class VMStructs { static int _vs_high_bound_offset; static int _vs_low_offset; static int _vs_high_offset; + static int _flag_type_offset; static int _flag_name_offset; static int _flag_addr_offset; static const char *_flags_addr; @@ -526,10 +529,28 @@ class CodeHeap : VMStructs { }; class JVMFlag : VMStructs { +private: + static void *find(const char *name, int type_mask); public: + enum Type { + Bool = 0, + Int = 1, + Uint = 2, + Intx = 3, + Uintx = 4, + Uint64_t = 5, + Size_t = 6, + Double = 7, + String = 8, + Stringlist = 9, + Unknown = -1 + }; + static void *find(const char *name); + static void *find(const char *name, std::initializer_list types); const char *name() { return *(const char **)at(_flag_name_offset); } + int type(); void *addr() { return *(void **)at(_flag_addr_offset); } }; diff --git a/ddprof-lib/src/main/cpp/wallClock.cpp b/ddprof-lib/src/main/cpp/wallClock.cpp index 8525fea0..a1e859ea 100644 --- a/ddprof-lib/src/main/cpp/wallClock.cpp +++ b/ddprof-lib/src/main/cpp/wallClock.cpp @@ -18,6 +18,7 @@ #include "stackFrame.h" #include "context.h" #include "debugSupport.h" +#include "libraries.h" #include "log.h" #include "profiler.h" #include "stackFrame.h" @@ -42,7 +43,7 @@ bool WallClockASGCT::inSyscall(void *ucontext) { // Make sure the previous instruction address is readable uintptr_t prev_pc = pc - SYSCALL_SIZE; if ((pc & 0xfff) >= SYSCALL_SIZE || - Profiler::instance()->findLibraryByAddress((instruction_t *)prev_pc) != + Libraries::instance()->findLibraryByAddress((instruction_t *)prev_pc) != NULL) { if (StackFrame::isSyscall((instruction_t *)prev_pc) && frame.checkInterruptedSyscall()) { diff --git a/ddprof-lib/src/main/cpp/wallClock.h b/ddprof-lib/src/main/cpp/wallClock.h index 4fa168e0..1416022d 100644 --- a/ddprof-lib/src/main/cpp/wallClock.h +++ b/ddprof-lib/src/main/cpp/wallClock.h @@ -33,20 +33,20 @@ class BaseWallClock : public Engine { protected: long _interval; // Maximum number of threads sampled in one iteration. This limit serves as a - // throttle when generating profiling signals. Otherwise applications with too - // many threads may suffer from a big profiling overhead. Also, keeping this - // limit low enough helps to avoid contention on a spin lock inside - // Profiler::recordSample(). - int _reservoir_size; - - pthread_t _thread; - virtual void timerLoop() = 0; - virtual void initialize(Arguments& args) {}; - - static void *threadEntry(void *wall_clock) { - ((BaseWallClock *)wall_clock)->timerLoop(); - return NULL; - } + // throttle when generating profiling signals. Otherwise applications with too + // many threads may suffer from a big profiling overhead. Also, keeping this + // limit low enough helps to avoid contention on a spin lock inside + // Profiler::recordSample(). + int _reservoir_size; + + pthread_t _thread; + virtual void timerLoop() = 0; + virtual void initialize(Arguments& args) {}; + + static void *threadEntry(void *wall_clock) { + ((BaseWallClock *)wall_clock)->timerLoop(); + return NULL; + } bool isEnabled() const; diff --git a/ddprof-lib/src/main/java/com/datadoghq/profiler/JVMAccess.java b/ddprof-lib/src/main/java/com/datadoghq/profiler/JVMAccess.java new file mode 100644 index 00000000..57db50e3 --- /dev/null +++ b/ddprof-lib/src/main/java/com/datadoghq/profiler/JVMAccess.java @@ -0,0 +1,178 @@ +package com.datadoghq.profiler; + +import java.util.function.Consumer; + +/** + * An internal JVM access support. + *

+ * We are using vmstructs and dynamic symbol lookups to provide access to some JVM internals. + * There will be dragons here. We are touching and possibly mutating JVM internals. Do not use + * unless you know what you are doing. + *

+ */ +public final class JVMAccess { + private static final class SingletonHolder { + static final JVMAccess INSTANCE = new JVMAccess(); + } + + /** + * Flags interface to access JVM flags. + * In general, the flags are read-only. However, some flags can be modified at runtime. + * Currently, only string and boolean flags can be modified. Allowing modification of numeric + * flags would require exact specification of the flag type (int, long, float, double) such + * that the correct number of bytes would be written to the flag and not overwrite the surrounding + * memory. + */ + public interface Flags { + Flags NONE = new Flags() { + @Override + public String getStringFlag(String name) { + return null; + } + + @Override + public void setStringFlag(String name, String value) { + } + + @Override + public boolean getBooleanFlag(String name) { + return false; + } + + @Override + public void setBooleanFlag(String name, boolean value) { + } + + @Override + public long getIntFlag(String name) { + return 0; + } + + @Override + public double getFloatFlag(String name) { + return 0; + } + }; + + String getStringFlag(String name); + void setStringFlag(String name, String value); + boolean getBooleanFlag(String name); + void setBooleanFlag(String name, boolean value); + long getIntFlag(String name); + double getFloatFlag(String name); + } + + private class FlagsImpl implements Flags { + public String getStringFlag(String name) { + return findStringJVMFlag0(name); + } + + public void setStringFlag(String name, String value) { + setStringJVMFlag0(name, value); + } + + public boolean getBooleanFlag(String name) { + return findBooleanJVMFlag0(name); + } + + public void setBooleanFlag(String name, boolean value) { + setBooleanJVMFlag0(name, value); + } + + public long getIntFlag(String name) { + return findIntJVMFlag0(name); + } + + public double getFloatFlag(String name) { + return findFloatJVMFlag0(name); + } + } + + /** + * Get the JVM access instance. + * + * @return the JVM access instance + */ + public static JVMAccess getInstance() { + return SingletonHolder.INSTANCE; + } + + private final LibraryLoader.Result libraryLoadResult; + private final Flags flags; + + private JVMAccess() { + LibraryLoader.Result result = LibraryLoader.builder().load();; + if (result.succeeded) { + // library loaded successfully, check if we can access JVM + try { + healthCheck0(); + } catch (Throwable t) { + // failed to access JVM; update the result + result = new LibraryLoader.Result(false, t); + } + + } + if (!result.succeeded && result.error != null) { + System.out.println("[WARNING] Failed to obtain JVM access.\n" + result.error); + } + flags = result.succeeded ? new FlagsImpl() : Flags.NONE; + libraryLoadResult = result; + } + + /** + * Create a JVM access instance. + * @param libLocation the library location or {@literal null} + * @param scratchDir the scratch directory or {@literal null} + * @param errorHandler the error handler or {@literal null} + */ + public JVMAccess(String libLocation, String scratchDir, Consumer errorHandler) { + LibraryLoader.Result result = LibraryLoader.builder().withLibraryLocation(libLocation).withScratchDir(scratchDir).load(); + if (result.succeeded) { + // library loaded successfully, check if we can access JVM + try { + healthCheck0(); + } catch (Throwable t) { + // failed to access JVM; update the result + result = new LibraryLoader.Result(false, t); + } + + } + if (!result.succeeded && result.error != null) { + if (errorHandler != null) { + errorHandler.accept(result.error); + } else { + System.out.println("[WARNING] Failed to obtain JVM access.\n" + result.error); + } + } + flags = result.succeeded ? new FlagsImpl() : Flags.NONE; + libraryLoadResult = result; + } + + /** + * Get the JVM flags. + * + * @return the JVM flags + */ + public Flags flags() { + return flags; + } + + /** + * Check if the JVM access is active. + * + * @return {@literal true} if the JVM access is active, {@literal false} otherwise + */ + public boolean isActive() { + return libraryLoadResult.succeeded; + } + + // a dummy method to check if the library has loaded properly + private native boolean healthCheck0(); + + private native String findStringJVMFlag0(String name); + private native void setStringJVMFlag0(String name, String value); + private native boolean findBooleanJVMFlag0(String name); + private native void setBooleanJVMFlag0(String name, boolean value); + private native long findIntJVMFlag0(String name); + private native double findFloatJVMFlag0(String name); +} diff --git a/ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java b/ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java index 3d4536f9..9edb2615 100644 --- a/ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java +++ b/ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java @@ -18,17 +18,11 @@ import sun.misc.Unsafe; -import java.io.BufferedReader; -import java.io.FileReader; import java.io.IOException; -import java.io.InputStream; import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -40,9 +34,6 @@ * libjavaProfiler.so. */ public final class JavaProfiler { - private static final String NATIVE_LIBS = "/META-INF/native-libs"; - private static final String LIBRARY_NAME = "libjavaProfiler." + (OperatingSystem.current() == OperatingSystem.macos ? "dylib" : "so"); - private static final Unsafe UNSAFE; static { Unsafe unsafe = null; @@ -111,15 +102,12 @@ public static synchronized JavaProfiler getInstance(String libLocation, String s } JavaProfiler profiler = new JavaProfiler(); - Path libraryPath = null; - if (libLocation == null) { - OperatingSystem os = OperatingSystem.current(); - String qualifier = (os == OperatingSystem.linux && isMusl()) ? "musl" : null; - - libraryPath = libraryFromClasspath(os, Arch.current(), qualifier, Paths.get(scratchDir != null ? scratchDir : System.getProperty("java.io.tmpdir"))); - libLocation = libraryPath.toString(); + LibraryLoader.Result result = LibraryLoader.builder().withLibraryLocation(libLocation).withScratchDir(scratchDir).load(); + if (!result.succeeded) { + throw new IOException("Failed to load Datadog Java profiler library", result.error); } - System.load(libLocation); + init0(); + profiler.initializeContextStorage(); instance = profiler; @@ -135,32 +123,6 @@ public static synchronized JavaProfiler getInstance(String libLocation, String s return profiler; } - /** - * Locates a library on class-path (eg. in a JAR) and creates a publicly accessible temporary copy - * of the library which can then be used by the application by its absolute path. - * - * @param os The operating system - * @param arch The architecture - * @param qualifier An optional qualifier (eg. musl) - * @param tempDir The working scratch dir where to store the temp library file - * @return The library absolute path. The caller should properly dispose of the file once it is - * not needed. The file is marked for 'delete-on-exit' so it won't survive a JVM restart. - * @throws IOException, IllegalStateException if the resource is not found on the classpath - */ - private static Path libraryFromClasspath(OperatingSystem os, Arch arch, String qualifier, Path tempDir) throws IOException { - String resourcePath = NATIVE_LIBS + "/" + os.name().toLowerCase() + "-" + arch.name().toLowerCase() + ((qualifier != null && !qualifier.isEmpty()) ? "-" + qualifier : "") + "/" + LIBRARY_NAME; - - InputStream libraryData = JavaProfiler.class.getResourceAsStream(resourcePath); - - if (libraryData != null) { - Path libFile = Files.createTempFile(tempDir, "libjavaProfiler", ".so"); - Files.copy(libraryData, libFile, StandardCopyOption.REPLACE_EXISTING); - libFile.toFile().deleteOnExit(); - return libFile; - } - throw new IllegalStateException(resourcePath + " not found on classpath"); - } - private void initializeContextStorage() { if (this.contextStorage == null) { int maxPages = getMaxContextPages0(); @@ -472,87 +434,7 @@ public Map getDebugCounters() { return counters; } - static boolean isMusl() throws IOException { - // check the Java exe then fall back to proc/self maps - try { - return isMuslJavaExecutable(); - } catch (IOException e) { - try { - return isMuslProcSelfMaps(); - } catch (IOException ignore) { - // not finding the Java exe is more interesting than failing to parse /proc/self/maps - throw e; - } - } - } - - static boolean isMuslProcSelfMaps() throws IOException { - try (BufferedReader reader = new BufferedReader(new FileReader("/proc/self/maps"))) { - String line; - while ((line = reader.readLine()) != null) { - if (line.contains("-musl-")) { - return true; - } - if (line.contains("/libc.")) { - return false; - } - } - } - return false; - } - - /** - * There is information about the linking in the ELF file. Since properly parsing ELF is not - * trivial this code will attempt a brute-force approach and will scan the first 4096 bytes - * of the 'java' program image for anything prefixed with `/ld-` - in practice this will contain - * `/ld-musl` for musl systems and probably something else for non-musl systems (eg. `/ld-linux-...`). - * However, if such string is missing should indicate that the system is not a musl one. - */ - static boolean isMuslJavaExecutable() throws IOException { - - byte[] magic = new byte[]{(byte)0x7f, (byte)'E', (byte)'L', (byte)'F'}; - byte[] prefix = new byte[]{(byte)'/', (byte)'l', (byte)'d', (byte)'-'}; // '/ld-*' - byte[] musl = new byte[]{(byte)'m', (byte)'u', (byte)'s', (byte)'l'}; // 'musl' - - Path binary = Paths.get(System.getProperty("java.home"), "bin", "java"); - byte[] buffer = new byte[4096]; - - try (InputStream is = Files.newInputStream(binary)) { - int read = is.read(buffer, 0, 4); - if (read != 4 || !containsArray(buffer, 0, magic)) { - throw new IOException(Arrays.toString(buffer)); - } - read = is.read(buffer); - if (read <= 0) { - throw new IOException(); - } - int prefixPos = 0; - for (int i = 0; i < read; i++) { - if (buffer[i] == prefix[prefixPos]) { - if (++prefixPos == prefix.length) { - return containsArray(buffer, i + 1, musl); - } - } else { - prefixPos = 0; - } - } - } - return false; - } - - private static boolean containsArray(byte[] container, int offset, byte[] contained) { - for (int i = 0; i < contained.length; i++) { - int leftPos = offset + i; - if (leftPos >= container.length) { - return false; - } - if (container[leftPos] != contained[i]) { - return false; - } - } - return true; - } - + private static native boolean init0(); private native void stop0() throws IllegalStateException; private native String execute0(String command) throws IllegalArgumentException, IllegalStateException, IOException; private native void filterThread0(boolean enable); diff --git a/ddprof-lib/src/main/java/com/datadoghq/profiler/LibraryLoader.java b/ddprof-lib/src/main/java/com/datadoghq/profiler/LibraryLoader.java new file mode 100644 index 00000000..f7643e8f --- /dev/null +++ b/ddprof-lib/src/main/java/com/datadoghq/profiler/LibraryLoader.java @@ -0,0 +1,152 @@ +package com.datadoghq.profiler; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.LockSupport; + +/** + * Encapsulates dynamic library loading logic. + * It is used to load the native library from the classpath or from a custom location. + * When loading from the classpath, the library is extracted to a temporary file and loaded from there. + * + */ +public final class LibraryLoader { + enum LoadingState { + NOT_LOADED, + LOADING, + LOADED, + UNAVAILABLE + } + + /** + * Represents the result of a library loading operation. + */ + public static final class Result { + public static final Result SUCCESS = new Result(true, null); + public static final Result UNAVAILABLE = new Result(false, null); + + public final boolean succeeded; + public final Throwable error; + + public Result(boolean succeeded, Throwable error) { + this.succeeded = succeeded; + this.error = error; + } + } + + /** + * Builder for {@link LibraryLoader}. It allows to specify the library location and the scratch directory. + */ + public static final class Builder { + private String libraryLocation; + private String scratchDir; + + private Builder() {} + + /** + * Sets the library location. + * @param libraryLocation the library location + * @return this builder + */ + public Builder withLibraryLocation(String libraryLocation) { + this.libraryLocation = libraryLocation; + return this; + } + + /** + * Sets the scratch directory where the temp library file can be created. + * @param scratchDir the scratch directory + * @return this builder + */ + public Builder withScratchDir(String scratchDir) { + this.scratchDir = scratchDir; + return this; + } + + /** + * Loads the library. + * @return the result of the library loading operation + */ + public Result load() { + return loadLibrary(libraryLocation, scratchDir); + } + } + + private static final String NATIVE_LIBS = "/META-INF/native-libs"; + private static final String JAVA_PROFILER_LIBRARY_NAME_BASE = "libjavaProfiler"; + private static final String JAVA_PROFILER_LIBRARY_NAME = JAVA_PROFILER_LIBRARY_NAME_BASE + "." + (OperatingSystem.current() == OperatingSystem.macos ? "dylib" : "so"); + + private static final Map> loadingStateMap = new ConcurrentHashMap<>(); + + public static Builder builder() { + return new Builder(); + } + + private static Result loadLibrary(final String libraryLocation, String scratchDir) { + String key = libraryLocation == null ? JAVA_PROFILER_LIBRARY_NAME : libraryLocation; + AtomicReference state = loadingStateMap.computeIfAbsent(key, (k) -> new AtomicReference<>(LoadingState.NOT_LOADED)); + + try { + // first thread to arrive will set the flag to 'loading' and will load the library + if (!state.compareAndSet(LoadingState.NOT_LOADED, LoadingState.LOADING)) { + // if there is already a different thread loading the library we will wait for it to finish + while (state.get() == LoadingState.LOADING) { + LockSupport.parkNanos(5_000_000L); // 5ms + } + // the library has been loaded by another thread, we can return + return state.get() == LoadingState.LOADED ? Result.SUCCESS : Result.UNAVAILABLE; + } + // if the attempt to load the library failed do not try again + if (state.get() == LoadingState.UNAVAILABLE) { + return Result.UNAVAILABLE; + } + Path libraryPath = libraryLocation != null ? Paths.get(libraryLocation) : null; + if (libraryPath == null) { + OperatingSystem os = OperatingSystem.current(); + String qualifier = (os == OperatingSystem.linux && os.isMusl()) ? "musl" : null; + + libraryPath = libraryFromClasspath(os, Arch.current(), qualifier, Paths.get(scratchDir != null ? scratchDir : System.getProperty("java.io.tmpdir"))); + } + System.load(libraryPath.toAbsolutePath().toString()); + return Result.SUCCESS; + } catch (Throwable t) { + state.set(LoadingState.UNAVAILABLE); + return new Result(false, t); + } finally { + state.compareAndSet(LoadingState.LOADING, LoadingState.LOADED); + } + } + + /** + * Locates a library on class-path (eg. in a JAR) and creates a publicly accessible temporary copy + * of the library which can then be used by the application by its absolute path. + * + * @param os The operating system + * @param arch The architecture + * @param qualifier An optional qualifier (eg. musl) + * @param tempDir The working scratch dir where to store the temp library file + * @return The library absolute path. The caller should properly dispose of the file once it is + * not needed. The file is marked for 'delete-on-exit' so it won't survive a JVM restart. + * @throws IOException, IllegalStateException if the resource is not found on the classpath + */ + private static Path libraryFromClasspath(OperatingSystem os, Arch arch, String qualifier, Path tempDir) throws IOException { + String resourcePath = NATIVE_LIBS + "/" + os.name().toLowerCase() + "-" + arch.name().toLowerCase() + ((qualifier != null && !qualifier.isEmpty()) ? "-" + qualifier : "") + "/" + JAVA_PROFILER_LIBRARY_NAME; + + InputStream libraryData = JavaProfiler.class.getResourceAsStream(resourcePath); + + if (libraryData != null) { + Path libFile = Files.createTempFile(tempDir, JAVA_PROFILER_LIBRARY_NAME_BASE + "-dd-tmp", ".so"); + Files.copy(libraryData, libFile, StandardCopyOption.REPLACE_EXISTING); + libFile.toFile().deleteOnExit(); + return libFile; + } + throw new IllegalStateException(resourcePath + " not found on classpath"); + } +} diff --git a/ddprof-lib/src/main/java/com/datadoghq/profiler/OperatingSystem.java b/ddprof-lib/src/main/java/com/datadoghq/profiler/OperatingSystem.java index 0aa8d5aa..2289914e 100644 --- a/ddprof-lib/src/main/java/com/datadoghq/profiler/OperatingSystem.java +++ b/ddprof-lib/src/main/java/com/datadoghq/profiler/OperatingSystem.java @@ -1,5 +1,12 @@ package com.datadoghq.profiler; +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.EnumSet; import java.util.HashSet; @@ -29,4 +36,87 @@ public static OperatingSystem of(String identifier) { public static OperatingSystem current() { return OperatingSystem.of(System.getProperty("os.name")); } + + public boolean isMusl() throws IOException { + // check the Java exe then fall back to proc/self maps + try { + return isMuslJavaExecutable(); + } catch (IOException e) { + try { + return isMuslProcSelfMaps(); + } catch (IOException ignore) { + // not finding the Java exe is more interesting than failing to parse /proc/self/maps + throw e; + } + } + } + + // package-private access for testing only + boolean isMuslProcSelfMaps() throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader("/proc/self/maps"))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.contains("-musl-")) { + return true; + } + if (line.contains("/libc.")) { + return false; + } + } + } + return false; + } + + /** + * There is information about the linking in the ELF file. Since properly parsing ELF is not + * trivial this code will attempt a brute-force approach and will scan the first 4096 bytes + * of the 'java' program image for anything prefixed with `/ld-` - in practice this will contain + * `/ld-musl` for musl systems and probably something else for non-musl systems (eg. `/ld-linux-...`). + * However, if such string is missing should indicate that the system is not a musl one. + */ + // package-private access for testing only + boolean isMuslJavaExecutable() throws IOException { + + byte[] magic = new byte[]{(byte)0x7f, (byte)'E', (byte)'L', (byte)'F'}; + byte[] prefix = new byte[]{(byte)'/', (byte)'l', (byte)'d', (byte)'-'}; // '/ld-*' + byte[] musl = new byte[]{(byte)'m', (byte)'u', (byte)'s', (byte)'l'}; // 'musl' + + Path binary = Paths.get(System.getProperty("java.home"), "bin", "java"); + byte[] buffer = new byte[4096]; + + try (InputStream is = Files.newInputStream(binary)) { + int read = is.read(buffer, 0, 4); + if (read != 4 || !containsArray(buffer, 0, magic)) { + throw new IOException(Arrays.toString(buffer)); + } + read = is.read(buffer); + if (read <= 0) { + throw new IOException(); + } + int prefixPos = 0; + for (int i = 0; i < read; i++) { + if (buffer[i] == prefix[prefixPos]) { + if (++prefixPos == prefix.length) { + return containsArray(buffer, i + 1, musl); + } + } else { + prefixPos = 0; + } + } + } + return false; + } + + private static boolean containsArray(byte[] container, int offset, byte[] contained) { + for (int i = 0; i < contained.length; i++) { + int leftPos = offset + i; + if (leftPos >= container.length) { + return false; + } + if (container[leftPos] != contained[i]) { + return false; + } + } + return true; + } } diff --git a/ddprof-test/build.gradle b/ddprof-test/build.gradle index 46ce90bc..9d5774d0 100644 --- a/ddprof-test/build.gradle +++ b/ddprof-test/build.gradle @@ -76,7 +76,11 @@ tasks.withType(Test).configureEach { !project.hasProperty('skip-tests') } - jvmArgs '-Djdk.attach.allowAttachSelf', '-Djol.tryWithSudo=true' + def config = it.name.replace("test", "") + + jvmArgs '-Djdk.attach.allowAttachSelf', '-Djol.tryWithSudo=true', + "-Dddprof_test.config=${config}", '-XX:ErrorFile=build/hs_err_pid%p.log', '-XX:+ResizeTLAB', + '-Xmx512m' def javaHome = System.getenv("JAVA_TEST_HOME") if (javaHome == null) { diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/ExternalLauncher.java b/ddprof-test/src/test/java/com/datadoghq/profiler/ExternalLauncher.java new file mode 100644 index 00000000..0a9590aa --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/ExternalLauncher.java @@ -0,0 +1,14 @@ +package com.datadoghq.profiler; + +public class ExternalLauncher { + public static void main(String[] args) throws Exception { + if (args.length != 1) { + throw new RuntimeException(); + } + if (args[0].equals("library")) { + JVMAccess.getInstance(); + } else if (args[0].equals("profiler")) { + JavaProfiler.getInstance(); + } + } +} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/JVMAccessTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/JVMAccessTest.java new file mode 100644 index 00000000..d3cdbf70 --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/JVMAccessTest.java @@ -0,0 +1,96 @@ +package com.datadoghq.profiler; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.file.Files; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +public class JVMAccessTest { + @BeforeAll + static void setUp() { + assumeFalse(Platform.isJ9() || Platform.isZing()); // J9 and Zing do not support vmstructs + } + + @Test + void sanityInitailizationTest() throws Exception { + String config = System.getProperty("ddprof_test.config"); + assumeTrue("debug".equals(config)); + + String javaHome = System.getenv("JAVA_TEST_HOME"); + if (javaHome == null) { + javaHome = System.getenv("JAVA_HOME"); + } + if (javaHome == null) { + javaHome = System.getProperty("java.home"); + } + assertNotNull(javaHome); + + File outFile = Files.createTempFile("jvmaccess", ".out").toFile(); + outFile.deleteOnExit(); + File errFile = Files.createTempFile("jvmaccess", ".err").toFile(); + errFile.deleteOnExit(); + + ProcessBuilder pb = new ProcessBuilder(javaHome + "/bin/java", "-cp", System.getProperty("java.class.path"), ExternalLauncher.class.getName(), "library"); + pb.redirectOutput(outFile); + pb.redirectError(errFile); + Process p = pb.start(); + int val = p.waitFor(); + assertEquals(0, val); + + boolean initLibraryFound = false; + boolean initProfilerFound = false; + for (String line : Files.readAllLines(outFile.toPath())) { + initLibraryFound |= line.contains("[TEST::INFO] VM::initLibrary"); + if (line.contains("[TEST::INFO] VM::initProfilerBridge")) { + // profiler is not expected to get initialized here; first occurrence means the test failed + initProfilerFound = true; + break; + } + } + assertTrue(initLibraryFound, "initLibrary not found"); + assertFalse(initProfilerFound, "initProfilerBridge found"); + } + + @Test + void testGetFlag() { + JVMAccess.Flags flags = JVMAccess.getInstance().flags(); + // non-existent flag + assertNull(flags.getStringFlag("test")); + + // The test relies on the gradle test task setting the JVM flags to expected values + assertEquals("build/hs_err_pid%p.log", flags.getStringFlag("ErrorFile")); // set to 'build/hs_err_pid%p.log' in the test task + assertTrue(flags.getBooleanFlag("ResizeTLAB")); // set to 'true' in the test task + assertEquals(512 * 1024 * 1024, flags.getIntFlag("MaxHeapSize")); // set to 512m in the test task + } + + @Test + void testGetFlagMismatch() { + JVMAccess.Flags flags = JVMAccess.getInstance().flags(); + + assertNull(flags.getStringFlag("ResizeTLAB")); // default is 'null' + assertFalse(flags.getBooleanFlag("ErrorFile")); // default is 'false' + assertEquals(0, flags.getFloatFlag("MaxHeapSize")); // default is '0' + } + + @Test + void testMutableFlags() { + JVMAccess.Flags flags = JVMAccess.getInstance().flags(); + String errorFile = "/tmp/hs_err_pid%p.log"; + flags.setStringFlag("ErrorFile", errorFile); + assertEquals(errorFile, flags.getStringFlag("ErrorFile")); + } + + @Test + void testMutableFlagsMismatch() { + JVMAccess.Flags flags = JVMAccess.getInstance().flags(); + String val = flags.getStringFlag("ErrorFile"); + flags.setBooleanFlag("ErrorFile", true); + // make sure the flag value is not changed and overwritten with rubbish + assertEquals(val, flags.getStringFlag("ErrorFile")); + } +} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/JavaProfilerTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/JavaProfilerTest.java new file mode 100644 index 00000000..e3cc3223 --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/JavaProfilerTest.java @@ -0,0 +1,56 @@ +package com.datadoghq.profiler; + +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.file.Files; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +public class JavaProfilerTest { + @Test + void sanityInitailizationTest() throws Exception { + String config = System.getProperty("ddprof_test.config"); + assumeTrue(config != null && "debug".equals(config)); + + String javaHome = System.getenv("JAVA_TEST_HOME"); + if (javaHome == null) { + javaHome = System.getenv("JAVA_HOME"); + } + if (javaHome == null) { + javaHome = System.getProperty("java.home"); + } + assertNotNull(javaHome); + + File outFile = Files.createTempFile("jvmaccess", ".out").toFile(); + outFile.deleteOnExit(); + File errFile = Files.createTempFile("jvmaccess", ".err").toFile(); + errFile.deleteOnExit(); + + ProcessBuilder pb = new ProcessBuilder(javaHome + "/bin/java", "-cp", System.getProperty("java.class.path"), ExternalLauncher.class.getName(), "profiler"); + pb.redirectOutput(outFile); + pb.redirectError(errFile); + Process p = pb.start(); + int val = p.waitFor(); + + boolean initLibraryFound = false; + boolean initProfilerFound = false; + for (String line : Files.readAllLines(outFile.toPath())) { + initLibraryFound |= line.contains("[TEST::INFO] VM::initLibrary"); + initProfilerFound |= line.contains("[TEST::INFO] VM::initProfilerBridge"); + System.out.println("[out] " + line); + } + + System.out.println(); + + for (String line : Files.readAllLines(errFile.toPath())) { + System.out.println("[err] " + line); + } + + assertEquals(0, val); + + assertTrue(initLibraryFound, "initLibrary not found"); + assertTrue(initProfilerFound, "initProfilerBridge found"); + } +} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/MuslDetectionTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/MuslDetectionTest.java index fe8542e0..5f93868d 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/MuslDetectionTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/MuslDetectionTest.java @@ -15,8 +15,9 @@ public void testIsMusl() throws IOException { String libc = System.getenv("LIBC"); Assumptions.assumeTrue(libc != null, "not running in CI, so LIBC envvar not set"); boolean isMusl = "musl".equalsIgnoreCase(libc); - assertEquals(isMusl, JavaProfiler.isMuslProcSelfMaps()); - assertEquals(isMusl, JavaProfiler.isMuslJavaExecutable()); - assertEquals(isMusl, JavaProfiler.isMusl()); + OperatingSystem os = OperatingSystem.current(); + assertEquals(isMusl, os.isMuslProcSelfMaps()); + assertEquals(isMusl, os.isMuslJavaExecutable()); + assertEquals(isMusl, os.isMusl()); } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/jfr/ObjectSampleDumpSmokeTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/jfr/ObjectSampleDumpSmokeTest.java index 8655d74b..15837706 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/jfr/ObjectSampleDumpSmokeTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/jfr/ObjectSampleDumpSmokeTest.java @@ -18,7 +18,7 @@ protected String getProfilerCommand() { return "memory=1024:a"; } - @RetryingTest(3) + @RetryingTest(5) @Timeout(value = 60) public void test() throws Exception { runTest("datadog.ObjectSample", "method3"); diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/loadlib/LoadLibraryTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/loadlib/LoadLibraryTest.java index 577100c7..ea323873 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/loadlib/LoadLibraryTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/loadlib/LoadLibraryTest.java @@ -3,7 +3,6 @@ import com.datadoghq.profiler.Platform; import com.datadoghq.profiler.AbstractProfilerTest; import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.RetryingTest; import java.lang.management.ClassLoadingMXBean;