diff options
author | 2019-10-25 09:54:32 -0700 | |
---|---|---|
committer | 2019-10-29 19:09:12 +0000 | |
commit | ab7bea2abdd13622b30cd742dde73b2b741f0173 (patch) | |
tree | c33d083a6f2f46aed808c4747c17363f4bd47e2b | |
parent | e3884e357566d739a33f22a0bdc4b76173cd6093 (diff) |
Add allocation sampling jvmti agent
Add an agent that can be used to sample heap allocations and produce
flame graphs.
Bug: none
Test: am attach-agent and run
Change-Id: Ic840b924cd52dc48938dd7ae4bc397241215fc5a
-rw-r--r-- | tools/jvmti-agents/ti-alloc-sample/Android.bp | 73 | ||||
-rw-r--r-- | tools/jvmti-agents/ti-alloc-sample/README.md | 90 | ||||
-rw-r--r-- | tools/jvmti-agents/ti-alloc-sample/ti_alloc_sample.cc | 454 |
3 files changed, 617 insertions, 0 deletions
diff --git a/tools/jvmti-agents/ti-alloc-sample/Android.bp b/tools/jvmti-agents/ti-alloc-sample/Android.bp new file mode 100644 index 0000000000..0dc2dd8fb0 --- /dev/null +++ b/tools/jvmti-agents/ti-alloc-sample/Android.bp @@ -0,0 +1,73 @@ +// +// 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. +// + +// Build variants {target,host} x {debug,ndebug} x {32,64} +cc_defaults { + name: "ti-alloc-sample-base-defaults", + srcs: ["ti_alloc_sample.cc"], + defaults: ["art_defaults"], + + // Note that this tool needs to be built for both 32-bit and 64-bit since it requires + // to be same ISA as what it is attached to. + compile_multilib: "both", + header_libs: [ + "libopenjdkjvmti_headers", + "libnativehelper_header_only", + "jni_headers", + ], +} + +cc_defaults { + name: "ti-alloc-sample-defaults", + host_supported: true, + shared_libs: [ + "libbase", + ], + defaults: ["ti-alloc-sample-base-defaults"], +} + +cc_defaults { + name: "ti-alloc-sample-static-defaults", + host_supported: false, + defaults: ["ti-alloc-sample-base-defaults"], + + shared_libs: [ + "liblog", + ], + static_libs: [ + "libbase_ndk", + ], + sdk_version: "current", + stl: "c++_static", +} + +art_cc_library { + name: "libtiallocsamples", + defaults: ["ti-alloc-sample-static-defaults"], +} + +art_cc_library { + name: "libtiallocsample", + defaults: ["ti-alloc-sample-defaults"], +} + +art_cc_library { + name: "libtiallocsampled", + defaults: [ + "art_debug_defaults", + "ti-alloc-sample-defaults", + ], +} diff --git a/tools/jvmti-agents/ti-alloc-sample/README.md b/tools/jvmti-agents/ti-alloc-sample/README.md new file mode 100644 index 0000000000..bfd78a011f --- /dev/null +++ b/tools/jvmti-agents/ti-alloc-sample/README.md @@ -0,0 +1,90 @@ +# tiallocsample + +tiallocsample is a JVMTI agent designed to track the call stacks of allocations +in the heap. + +# Usage +### Build +> `m libtiallocsample` + +The libraries will be built for 32-bit, 64-bit, host and target. Below examples +assume you want to use the 64-bit version. + +Use `libtiallocsamples` if you wish to build a version without non-NDK dynamic dependencies. + +### Command Line + +The agent is loaded using -agentpath like normal. It takes arguments in the +following format: +> `sample_rate,log_path` + +* sample_rate is an integer specifying how frequently an event is reported. + E.g., 10 means every tenth call to new will be logged. +* log_path is an absolued file path specifying where the log is to be written. + +#### Output Format + +The resulting file is a sequence of object allocations, with a limited form of +text compression. For example a single stack frame might look like: + +``` +#20(VMObjectAlloc(#0(jthread[main], jclass[Ljava/lang/String; file: String.java], size[56, hex: 0x38 +> ])) + #1(nativeReadString(J)Ljava/lang/String;) + #2(readString(Landroid/os/Parcel;)Ljava/lang/String;) + #3(readString()Ljava/lang/String;) + #4(readParcelableCreator(Ljava/lang/ClassLoader;)Landroid/os/Parcelable$Creator;) + #5(readParcelable(Ljava/lang/ClassLoader;)Landroid/os/Parcelable;) + #6(readFromParcel(Landroid/os/Parcel;)V) + #7(<init>(Landroid/os/Parcel;)V) + #8(<init>(Landroid/os/Parcel;Landroid/view/DisplayInfo$1;)V) + #9(createFromParcel(Landroid/os/Parcel;)Landroid/view/DisplayInfo;) + #10(createFromParcel(Landroid/os/Parcel;)Ljava/lang/Object;) + #11(getDisplayInfo(I)Landroid/view/DisplayInfo;) + #11 + #12(updateDisplayInfoLocked()V) + #13(getState()I) + #14(onDisplayChanged(I)V) + #15(handleMessage(Landroid/os/Message;)V) + #16(dispatchMessage(Landroid/os/Message;)V) + #17(loop()V) + #18(main([Ljava/lang/String;)V) + #19(invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;)) +``` + +The first line tells what thread the allocation occurred on, what type is +allocated, and what size the allocation was. The remaining lines are the call +stack, starting with the function in which the allocation occured. The depth +limit is 20 frames. + +String compression is rudimentary. + +``` + #1(nativeReadString(J)Ljava/lang/String;) +``` + +Indicates that the string inside the parenthesis is the first entry in a string +table. Later occurences in the printout of that string will print as + +``` + #1 +``` + +Stack frame entries are compressed by this method, as are entire allocation +records. + + +#### ART +> `art -Xplugin:$ANDROID_HOST_OUT/lib64/libopenjdkjvmti.so '-agentpath:libtiallocsample.so=100' -cp tmp/java/helloworld.dex -Xint helloworld` + +* `-Xplugin` and `-agentpath` need to be used, otherwise the agent will fail during init. +* If using `libartd.so`, make sure to use the debug version of jvmti. + +> `adb shell setenforce 0` +> +> `adb push $ANDROID_PRODUCT_OUT/system/lib64/libtiallocsample.so /data/local/tmp/` +> +> `adb shell am start-activity --attach-agent /data/local/tmp/libtiallocsample.so=100 some.debuggable.apps/.the.app.MainActivity` + +#### RI +> `java '-agentpath:libtiallocsample.so=MethodEntry' -cp tmp/helloworld/classes helloworld` diff --git a/tools/jvmti-agents/ti-alloc-sample/ti_alloc_sample.cc b/tools/jvmti-agents/ti-alloc-sample/ti_alloc_sample.cc new file mode 100644 index 0000000000..08ec434728 --- /dev/null +++ b/tools/jvmti-agents/ti-alloc-sample/ti_alloc_sample.cc @@ -0,0 +1,454 @@ +// 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 <android-base/logging.h> + +#include <atomic> +#include <fstream> +#include <iostream> +#include <istream> +#include <iomanip> +#include <jni.h> +#include <jvmti.h> +#include <limits> +#include <map> +#include <memory> +#include <mutex> +#include <string> +#include <sstream> +#include <vector> + +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<char*>("<NULLPTR>"); + } else if (jvmtienv->GetThreadInfo(thread, &info_) != JVMTI_ERROR_NONE) { + info_.name = const_cast<char*>("<UNKNOWN THREAD>"); + } else { + free_name_ = true; + } + } + + ~ScopedThreadInfo() { + if (free_name_) { + jvmtienv_->Deallocate(reinterpret_cast<unsigned char*>(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<unsigned char*>(name_)); + jvmtienv_->Deallocate(reinterpret_cast<unsigned char*>(generic_)); + jvmtienv_->Deallocate(reinterpret_cast<unsigned char*>(file_)); + jvmtienv_->Deallocate(reinterpret_cast<unsigned char*>(debug_ext_)); + } + } + + bool Init(bool get_generic = true) { + if (class_ == nullptr) { + name_ = const_cast<char*>("<NONE>"); + generic_ = const_cast<char*>("<NONE>"); + 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 "<UNKNOWN_SOURCE_DEBUG_EXTENSION>"; + } else { + return debug_ext_; + } + } + const char* GetSourceFileName() const { + if (file_ == nullptr) { + return "<UNKNOWN_FILE>"; + } 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<unsigned char*>(name_)); + jvmtienv_->Deallocate(reinterpret_cast<unsigned char*>(signature_)); + jvmtienv_->Deallocate(reinterpret_cast<unsigned char*>(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<unsigned char*>(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<ScopedClassInfo> 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 UniqueStringTable { + public: + UniqueStringTable() = default; + ~UniqueStringTable() = default; + std::string Intern(const std::string& key) { + if (map_.find(key) != map_.end()) { + return std::string("#") + std::to_string(map_[key]); + } else { + map_[key] = next_index_; + ++next_index_; + return std::string("#") + std::to_string(map_[key]) + "(" + key + ")"; + } + } + private: + int32_t next_index_; + std::map<std::string, int32_t> map_; +}; + +static UniqueStringTable* string_table = nullptr; + +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; + +// An RAII class to turn a boolean flag on/off. +class ScopedFlag { + public: + explicit ScopedFlag(bool* flag) : flag_(flag) { + *flag_ = true; + } + ~ScopedFlag() { + *flag_ = false; + } + private: + bool* flag_; +}; + +// 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, jmethodID method_id) { + char *method_name; + char *method_signature; + char *generic_pointer; + jvmtiError err = jvmti->GetMethodName(method_id, + &method_name, + &method_signature, + &generic_pointer); + if (err == JVMTI_ERROR_NONE) { + std::string method; + method = ((method_name == nullptr) ? "UNKNOWN" : method_name); + method += ((method_signature == nullptr) ? "(UNKNOWN)" : method_signature); + return string_table->Intern(method); + } else { + return "METHODERROR"; + } +} + +static int sampling_rate = 10; + +static void JNICALL logVMObjectAlloc(jvmtiEnv* jvmti, + JNIEnv* jni, + jthread thread, + jobject obj ATTRIBUTE_UNUSED, + jclass klass, + jlong size) { + // Prevent recursive allocation tracking, and the stack overflow it causes. + static thread_local bool currently_logging; + if (currently_logging) { + return; + } + ScopedFlag sf(¤tly_logging); + + // Guard accesses to log skip count, string table, etc. + static std::mutex mutex; + std::lock_guard<std::mutex> lg(mutex); + + // Only process every nth log call. + static int logs_skipped = 0; + if (logs_skipped < sampling_rate) { + logs_skipped++; + return; + } else { + logs_skipped = 0; + } + + std::string record = + "VMObjectAlloc(" + formatAllocation(jvmti, + jni, + jthreadContainer{.thread = thread}, + klass, + jlongContainer{.val = size}) + ")"; + + static constexpr size_t kFrameDepthLimit = 20; + jvmtiFrameInfo stack_frames[kFrameDepthLimit]; + jint stack_depth; + jvmtiError err = jvmti->GetStackTrace(thread, 0, kFrameDepthLimit, stack_frames, &stack_depth); + if (err == JVMTI_ERROR_NONE) { + for (int i = 0; i < stack_depth; ++i) { + record += "\n " + formatMethod(jvmti, stack_frames[i].method); + } + } + stream->Write(string_table->Intern(record) + "\n"); +} + +static jvmtiEventCallbacks kLogCallbacks { + .VMObjectAlloc = logVMObjectAlloc, +}; + +static jint SetupJvmtiEnv(JavaVM* vm, jvmtiEnv** jvmti) { + jint res = vm->GetEnv(reinterpret_cast<void**>(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<void**>(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 jint AgentStart(JavaVM* vm, + char* options, + void* reserved ATTRIBUTE_UNUSED) { + // options string should contain "sampling_rate,output_file_path". + std::string args(options); + size_t comma_pos = args.find(','); + if (comma_pos == std::string::npos) { + return JNI_ERR; + } + sampling_rate = std::stoi(args.substr(0, comma_pos)); + std::string output_file_path = args.substr(comma_pos + 1); + + // 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<jint>(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(); + + stream = new LockedStream(output_file_path); + + 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 + |