// Copyright (C) 2018 The Android Open Source Project // // 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace tifast { namespace { // Special art ti-version number. We will use this as a fallback if we cannot get a regular JVMTI // env. static constexpr jint kArtTiVersion = JVMTI_VERSION_1_2 | 0x40000000; // jthread is a typedef of jobject so we use this to allow the templates to distinguish them. struct jthreadContainer { jthread thread; }; // jlocation is a typedef of jlong so use this to distinguish the less common jlong. struct jlongContainer { jlong val; }; static void DeleteLocalRef(JNIEnv* env, jobject obj) { if (obj != nullptr && env != nullptr) { env->DeleteLocalRef(obj); } } class ScopedThreadInfo { public: ScopedThreadInfo(jvmtiEnv* jvmtienv, JNIEnv* env, jthread thread) : jvmtienv_(jvmtienv), env_(env), free_name_(false) { if (thread == nullptr) { info_.name = const_cast(""); } else if (jvmtienv->GetThreadInfo(thread, &info_) != JVMTI_ERROR_NONE) { info_.name = const_cast(""); } else { free_name_ = true; } } ~ScopedThreadInfo() { if (free_name_) { jvmtienv_->Deallocate(reinterpret_cast(info_.name)); } DeleteLocalRef(env_, info_.thread_group); DeleteLocalRef(env_, info_.context_class_loader); } const char* GetName() const { return info_.name; } private: jvmtiEnv* jvmtienv_; JNIEnv* env_; bool free_name_; jvmtiThreadInfo info_{}; }; class ScopedClassInfo { public: ScopedClassInfo(jvmtiEnv* jvmtienv, jclass c) : jvmtienv_(jvmtienv), class_(c) {} ~ScopedClassInfo() { if (class_ != nullptr) { jvmtienv_->Deallocate(reinterpret_cast(name_)); jvmtienv_->Deallocate(reinterpret_cast(generic_)); jvmtienv_->Deallocate(reinterpret_cast(file_)); jvmtienv_->Deallocate(reinterpret_cast(debug_ext_)); } } bool Init(bool get_generic = true) { if (class_ == nullptr) { name_ = const_cast(""); generic_ = const_cast(""); return true; } else { jvmtiError ret1 = jvmtienv_->GetSourceFileName(class_, &file_); jvmtiError ret2 = jvmtienv_->GetSourceDebugExtension(class_, &debug_ext_); char** gen_ptr = &generic_; if (!get_generic) { generic_ = nullptr; gen_ptr = nullptr; } return jvmtienv_->GetClassSignature(class_, &name_, gen_ptr) == JVMTI_ERROR_NONE && ret1 != JVMTI_ERROR_MUST_POSSESS_CAPABILITY && ret1 != JVMTI_ERROR_INVALID_CLASS && ret2 != JVMTI_ERROR_MUST_POSSESS_CAPABILITY && ret2 != JVMTI_ERROR_INVALID_CLASS; } } jclass GetClass() const { return class_; } const char* GetName() const { return name_; } const char* GetGeneric() const { return generic_; } const char* GetSourceDebugExtension() const { if (debug_ext_ == nullptr) { return ""; } else { return debug_ext_; } } const char* GetSourceFileName() const { if (file_ == nullptr) { return ""; } else { return file_; } } private: jvmtiEnv* jvmtienv_; jclass class_; char* name_ = nullptr; char* generic_ = nullptr; char* file_ = nullptr; char* debug_ext_ = nullptr; friend std::ostream& operator<<(std::ostream &os, ScopedClassInfo const& m); }; class ScopedMethodInfo { public: ScopedMethodInfo(jvmtiEnv* jvmtienv, JNIEnv* env, jmethodID m) : jvmtienv_(jvmtienv), env_(env), method_(m) {} ~ScopedMethodInfo() { DeleteLocalRef(env_, declaring_class_); jvmtienv_->Deallocate(reinterpret_cast(name_)); jvmtienv_->Deallocate(reinterpret_cast(signature_)); jvmtienv_->Deallocate(reinterpret_cast(generic_)); } bool Init(bool get_generic = true) { if (jvmtienv_->GetMethodDeclaringClass(method_, &declaring_class_) != JVMTI_ERROR_NONE) { return false; } class_info_.reset(new ScopedClassInfo(jvmtienv_, declaring_class_)); jint nlines; jvmtiLineNumberEntry* lines; jvmtiError err = jvmtienv_->GetLineNumberTable(method_, &nlines, &lines); if (err == JVMTI_ERROR_NONE) { if (nlines > 0) { first_line_ = lines[0].line_number; } jvmtienv_->Deallocate(reinterpret_cast(lines)); } else if (err != JVMTI_ERROR_ABSENT_INFORMATION && err != JVMTI_ERROR_NATIVE_METHOD) { return false; } return class_info_->Init(get_generic) && (jvmtienv_->GetMethodName(method_, &name_, &signature_, &generic_) == JVMTI_ERROR_NONE); } const ScopedClassInfo& GetDeclaringClassInfo() const { return *class_info_; } jclass GetDeclaringClass() const { return declaring_class_; } const char* GetName() const { return name_; } const char* GetSignature() const { return signature_; } const char* GetGeneric() const { return generic_; } jint GetFirstLine() const { return first_line_; } private: jvmtiEnv* jvmtienv_; JNIEnv* env_; jmethodID method_; jclass declaring_class_ = nullptr; std::unique_ptr class_info_; char* name_ = nullptr; char* signature_ = nullptr; char* generic_ = nullptr; jint first_line_ = -1; }; std::ostream& operator<<(std::ostream &os, ScopedClassInfo const& c) { const char* generic = c.GetGeneric(); if (generic != nullptr) { return os << c.GetName() << "<" << generic << ">" << " file: " << c.GetSourceFileName(); } else { return os << c.GetName() << " file: " << c.GetSourceFileName(); } } class LockedStream { public: explicit LockedStream(const std::string& filepath) { stream_.open(filepath, std::ofstream::out); if (!stream_.is_open()) { LOG(ERROR) << "====== JVMTI FAILED TO OPEN LOG FILE"; } } ~LockedStream() { stream_.close(); } void Write(const std::string& str) { stream_ << str; stream_.flush(); } private: std::ofstream stream_; }; static LockedStream* stream = nullptr; class UniqueStringTable { public: UniqueStringTable() = default; ~UniqueStringTable() = default; std::string Intern(const std::string& header, const std::string& key) { if (map_.find(key) == map_.end()) { map_[key] = next_index_; // Emit definition line. E.g., =123,string stream->Write(header + std::to_string(next_index_) + "," + key + "\n"); ++next_index_; } return std::to_string(map_[key]); } private: int32_t next_index_; std::map map_; }; static UniqueStringTable* string_table = nullptr; // Formatter for the thread, type, and size of an allocation. static std::string formatAllocation(jvmtiEnv* jvmti, JNIEnv* jni, jthreadContainer thr, jclass klass, jlongContainer size) { ScopedThreadInfo sti(jvmti, jni, thr.thread); std::ostringstream allocation; allocation << "jthread[" << sti.GetName() << "]"; ScopedClassInfo sci(jvmti, klass); if (sci.Init(/*get_generic=*/false)) { allocation << ", jclass[" << sci << "]"; } else { allocation << ", jclass[TYPE UNKNOWN]"; } allocation << ", size[" << size.val << ", hex: 0x" << std::hex << size.val << "]"; return string_table->Intern("+", allocation.str()); } // Formatter for a method entry on a call stack. static std::string formatMethod(jvmtiEnv* jvmti, JNIEnv* jni, jmethodID method_id) { ScopedMethodInfo smi(jvmti, jni, method_id); std::string method; if (smi.Init(/*get_generic=*/false)) { method = std::string(smi.GetDeclaringClassInfo().GetName()) + "::" + smi.GetName() + smi.GetSignature(); } else { method = "ERROR"; } return string_table->Intern("+", method); } static int sampling_rate; static int stack_depth_limit; static void JNICALL logVMObjectAlloc(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread, jobject obj ATTRIBUTE_UNUSED, jclass klass, jlong size) { // Sample only once out of sampling_rate tries, and prevent recursive allocation tracking, static thread_local int sample_countdown = sampling_rate; --sample_countdown; if (sample_countdown != 0) { return; } // Guard accesses to string table and emission. static std::mutex mutex; std::lock_guard lg(mutex); std::string record = formatAllocation(jvmti, jni, jthreadContainer{.thread = thread}, klass, jlongContainer{.val = size}); std::unique_ptr stack_frames(new jvmtiFrameInfo[stack_depth_limit]); jint stack_depth; jvmtiError err = jvmti->GetStackTrace(thread, 0, stack_depth_limit, stack_frames.get(), &stack_depth); if (err == JVMTI_ERROR_NONE) { // Emit stack frames in order from deepest in the stack to most recent. // This simplifies post-collection processing. for (int i = stack_depth - 1; i >= 0; --i) { record += ";" + formatMethod(jvmti, jni, stack_frames[i].method); } } stream->Write(string_table->Intern("=", record) + "\n"); sample_countdown = sampling_rate; } static jvmtiEventCallbacks kLogCallbacks { .VMObjectAlloc = logVMObjectAlloc, }; static jint SetupJvmtiEnv(JavaVM* vm, jvmtiEnv** jvmti) { jint res = vm->GetEnv(reinterpret_cast(jvmti), JVMTI_VERSION_1_1); if (res != JNI_OK || *jvmti == nullptr) { LOG(ERROR) << "Unable to access JVMTI, error code " << res; return vm->GetEnv(reinterpret_cast(jvmti), kArtTiVersion); } return res; } } // namespace static jvmtiError SetupCapabilities(jvmtiEnv* jvmti) { jvmtiCapabilities caps{}; caps.can_generate_vm_object_alloc_events = 1; caps.can_get_line_numbers = 1; caps.can_get_source_file_name = 1; caps.can_get_source_debug_extension = 1; return jvmti->AddCapabilities(&caps); } static bool ProcessOptions(std::string options) { std::string output_file_path; if (options.empty()) { static constexpr int kDefaultSamplingRate = 10; static constexpr int kDefaultStackDepthLimit = 50; static constexpr const char* kDefaultOutputFilePath = "/data/local/tmp/logstream.txt"; sampling_rate = kDefaultSamplingRate; stack_depth_limit = kDefaultStackDepthLimit; output_file_path = kDefaultOutputFilePath; } else { // options string should contain "sampling_rate,stack_depth_limit,output_file_path". size_t comma_pos = options.find(','); if (comma_pos == std::string::npos) { return false; } sampling_rate = std::stoi(options.substr(0, comma_pos)); options = options.substr(comma_pos + 1); comma_pos = options.find(','); if (comma_pos == std::string::npos) { return false; } stack_depth_limit = std::stoi(options.substr(0, comma_pos)); output_file_path = options.substr(comma_pos + 1); } LOG(INFO) << "Starting allocation tracing: sampling_rate=" << sampling_rate << ", stack_depth_limit=" << stack_depth_limit << ", output_file_path=" << output_file_path; stream = new LockedStream(output_file_path); return true; } static jint AgentStart(JavaVM* vm, char* options, void* reserved ATTRIBUTE_UNUSED) { // Handle the sampling rate, depth limit, and output path, if set. if (!ProcessOptions(options)) { return JNI_ERR; } // Create the environment. jvmtiEnv* jvmti = nullptr; if (SetupJvmtiEnv(vm, &jvmti) != JNI_OK) { LOG(ERROR) << "Could not get JVMTI env or ArtTiEnv!"; return JNI_ERR; } jvmtiError error = SetupCapabilities(jvmti); if (error != JVMTI_ERROR_NONE) { LOG(ERROR) << "Unable to set caps"; return JNI_ERR; } // Add callbacks and notification. error = jvmti->SetEventCallbacks(&kLogCallbacks, static_cast(sizeof(kLogCallbacks))); if (error != JVMTI_ERROR_NONE) { LOG(ERROR) << "Unable to set event callbacks."; return JNI_ERR; } error = jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_OBJECT_ALLOC, nullptr /* all threads */); if (error != JVMTI_ERROR_NONE) { LOG(ERROR) << "Unable to enable event " << JVMTI_EVENT_VM_OBJECT_ALLOC; return JNI_ERR; } string_table = new UniqueStringTable(); return JNI_OK; } // Late attachment (e.g. 'am attach-agent'). extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char* options, void* reserved) { return AgentStart(vm, options, reserved); } // Early attachment extern "C" JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* jvm, char* options, void* reserved) { return AgentStart(jvm, options, reserved); } } // namespace tifast