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
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 0000000..0dc2dd8
--- /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 0000000..bfd78a0
--- /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 0000000..08ec434
--- /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
+