cpp/profiler/ArtCompatibilityRunner.cpp (242 lines of code) (raw):

/** * Copyright 2004-present, Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ArtCompatibilityRunner.h" #include <fb/log.h> #include <fbjni/fbjni.h> #include <profilo/profiler/JavaBaseTracer.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <algorithm> #include <array> #include <mutex> #include <set> #include <string> #include <system_error> #include <vector> #include <time.h> #include <profilo/profiler/SignalHandler.h> #include <profilo/util/common.h> namespace fbjni = facebook::jni; namespace facebook { namespace profilo { using profiler::SignalHandler; namespace artcompat { struct JavaFrame { std::string class_descriptor{""}; std::string name{""}; }; struct CppUnwinderJavaFrame { const char* class_descriptor; const char* name; int32_t identifier; }; namespace { static const int kStackSize = 128; fbjni::local_ref<jclass> getThreadClass() { return fbjni::findClassLocal("java/lang/Thread"); } std::vector<JavaFrame> getJavaStackTrace( versions::AndroidVersion version, fbjni::alias_ref<jobject> thread) { auto jlThread_class = getThreadClass(); auto jlThread_getStackTrace = jlThread_class->getMethod<fbjni::jtypeArray<jobject>()>( "getStackTrace", "()[Ljava/lang/StackTraceElement;"); auto jlStackTraceElement_class = fbjni::findClassLocal("java/lang/StackTraceElement"); auto jlStackTraceElement_getClassName = jlStackTraceElement_class->getMethod<jstring()>("getClassName"); auto jlStackTraceElement_getMethodName = jlStackTraceElement_class->getMethod<jstring()>("getMethodName"); auto stacktrace = jlThread_getStackTrace(thread); auto stacktrace_len = stacktrace->size(); std::vector<JavaFrame> result; for (size_t idx = 0; idx < stacktrace_len; idx++) { auto element = stacktrace->getElement(idx); auto classname = jlStackTraceElement_getClassName(element)->toStdString(); auto methodname = jlStackTraceElement_getMethodName(element)->toStdString(); std::replace(classname.begin(), classname.end(), '.', '/'); JavaFrame frame{}; frame.class_descriptor = "L" + classname + ";"; frame.name = methodname; result.emplace_back(std::move(frame)); } return result; } size_t getCppStackTrace( profiler::JavaBaseTracer* tracer, std::array<CppUnwinderJavaFrame, kStackSize>& result) { uint16_t depth = 0; int64_t ints[kStackSize]; char const* method_names[kStackSize]; char const* class_descriptors[kStackSize]; auto ret = tracer->collectJavaStack( nullptr, ints, method_names, class_descriptors, depth, kStackSize); for (size_t idx = 0; idx < depth; idx++) { result[idx] = CppUnwinderJavaFrame{ .class_descriptor = class_descriptors[idx], .name = method_names[idx], .identifier = static_cast<int32_t>(ints[idx] >> 32), }; } if (ret != StackCollectionRetcode::SUCCESS) { return 0; } return depth; } bool compareStackTraces( std::array<CppUnwinderJavaFrame, kStackSize>& cppStack, size_t cppStackSize, std::vector<JavaFrame>& javaStack) { size_t javaStackSize = javaStack.size(); // We expect the C++ stack trace to be a subset of the Java one (which has to // also call getStackTrace). if (cppStackSize > javaStackSize || cppStackSize == 0) { return false; } // We may get different types of data from the Java and C++ sides. // In particular, we may not have method idxs on the Java side or // we may not have class descriptors and method names on the C++ side. // // Attempt to match what we have and complain if there's no // intersection of available information. for (size_t i = 0; i < cppStackSize; i++) { auto& cpp_frame = cppStack[cppStackSize - i - 1]; auto& java_frame = javaStack[javaStackSize - i - 1]; if (cpp_frame.class_descriptor == nullptr || cpp_frame.name == nullptr) { FBLOGW("Cpp Unwind returned empty class or method symbol(s)"); return false; } // We want Class + Name to be extra sure. if (java_frame.class_descriptor.compare(cpp_frame.class_descriptor) != 0) { FBLOGW( "Class descriptors did not match Java:%s C++:%s", java_frame.class_descriptor.c_str(), cpp_frame.class_descriptor); return false; } if (java_frame.name.compare(cpp_frame.name) != 0) { FBLOGW( "Method names did not match Java:%s C++:%s", java_frame.name.c_str(), cpp_frame.name); return false; } } return true; } uint64_t now() { struct timespec ts {}; clock_gettime(CLOCK_MONOTONIC, &ts); return ((uint64_t)ts.tv_sec) * 1'000'000'000 + ts.tv_nsec; } namespace { struct SignalState { sigjmp_buf sigJmpBuf; std::atomic_bool inSection; uint64_t tid; }; // Signal handler to safely bail out. This is inspired by how the // SamplingProfiler signal handling works, but is way simpler. void JumpToSafetySignalHandler( SignalHandler::HandlerScope scope, int signum, siginfo_t* siginfo, void* ucontext) { SignalState& state = *(SignalState*)scope.GetData(); if (state.tid == threadID() && state.inSection.load()) { scope.siglongjmp(state.sigJmpBuf, 1); } scope.CallPreviousHandler(signum, siginfo, ucontext); } } // namespace // // We collect two stack traces, from the same JNI function (and therefore VM // frame) - one from Java, using normal VM APIs and one using our // async-safe C++ stack unwinder. // // The Java part effectively calls Thread.currentThread().getStackTrace() and // then proceeds to look up the java.lang.reflect.Method object for each // element and get its method index (stored in a private field by the // runtime). // // Due to overloads and imprecise data, there is sometimes more than one // method index per Java stack trace element. // // Once we have that, we execute the C++ counterpart safely in a ForkJail. The // jail compares the C++ result against the Java source of truth and if they // match, we exit with kExitCodeSuccess. Otherwise, we exit with a different // status code. // bool runJavaCompatibilityCheckInternal( versions::AndroidVersion version, profiler::JavaBaseTracer* tracer) { // Because we only have one instance of the signal handling state, // we wrap everything in a lock to serialize all callers and simplify the // logic. static std::mutex exclusiveRunLock; std::lock_guard<std::mutex> lg{exclusiveRunLock}; auto begin = now(); auto jlThread_class = getThreadClass(); auto jlThread_currentThread = jlThread_class->getStaticMethod<jobject()>( "currentThread", "()Ljava/lang/Thread;"); auto jlThread = jlThread_currentThread(jlThread_class); // Collect the Java stack trace auto beginJava = now(); std::vector<JavaFrame> javaStack; try { javaStack = getJavaStackTrace(version, jlThread); } catch (...) { return false; } auto endJava = now(); // Collect our tracer's stack trace try { tracer->prepare(); auto beginCpp = now(); tracer->startTracing(); bool cppSuccess = false; size_t cppStackSize = 0; std::array<CppUnwinderJavaFrame, kStackSize> cppStack; // Sets up signal handlers for SIGSEGV and SIGBUS. Uses SignalHandler, so // cannot be run concurrently with SamplingProfiler's usage (but that's // okay, compatibility checks gate the SamplingProfiler usage). // // Ultimately, we do need to use the exact same safety mechanism as the // profiler to work around the exact same bugs in Android's signal handling. static SignalState state{}; auto& handlerSegv = SignalHandler::Initialize(SIGSEGV, JumpToSafetySignalHandler); handlerSegv.SetData(&state); handlerSegv.Enable(); auto& handlerBus = SignalHandler::Initialize(SIGBUS, JumpToSafetySignalHandler); handlerBus.SetData(&state); handlerBus.Enable(); if (sigsetjmp(state.sigJmpBuf, 1) == 0) { state.tid = threadID(); state.inSection.store(true); cppStackSize = getCppStackTrace(tracer, cppStack); state.inSection.store(false); cppSuccess = true; } else { // Long jump from signal handler state.inSection.store(false); cppSuccess = false; } handlerSegv.Disable(); handlerBus.Disable(); auto end = now(); FBLOGD( "Art compat check finished in %" PRIu64 " ms, java: %" PRIu64 " ms, cpp: %" PRIu64 " ms", (end - begin) / 1'000'000, (endJava - beginJava) / 1'000'000, (end - beginCpp) / 1'000'000); if (!cppSuccess) { FBLOGE("getCppStackTrace signalled"); return false; } if (!compareStackTraces(cppStack, cppStackSize, javaStack)) { FBLOGE("compareStackTraces returned false"); return false; } FBLOGI("Compatibility check succeeded"); return true; } catch (std::system_error& ex) { FBLOGE("Caught system error: %s", ex.what()); return false; } } } // namespace bool runJavaCompatibilityCheck( versions::AndroidVersion version, profiler::JavaBaseTracer* tracer) { return runJavaCompatibilityCheckInternal(version, tracer); } } // namespace artcompat } // namespace profilo } // namespace facebook