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(&currently_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
+