Add a simple profiling agent.

This agent will simply hold onto how many times each method is called
and dump the information in a JSON file wherever one wishes.

Test: manual, attach to google-maps
Test: ./art/test/run-test --host --dev --64 --with-agent $ANDROID_HOST_OUT/lib64/libsimpleprofileds.so=/proc/self/fd/2,dump_on_shutdown 001-HelloWorld
Test: ```
% adb root
% adb shell setenforce 0
% adb push $OUT/system/lib64/libsimpleprofileds.so /data/local/tmp/libsimpleprofileds.so
% adb shell
blueline:/ # cd /data/data/com.google.android.apps.maps
blueline:/data/data/com.google.android.apps.maps # cp /data/local/tmp/libsimpleprofileds.so .
blueline:/data/data/com.google.android.apps.maps # ps -A | grep maps
u0_a178        9143    927 15691440 190132 SyS_epoll_wait     0 S com.google.android.apps.maps
blueline:/data/data/com.google.android.apps.maps # cmd activity attach-agent com.google.android.apps.maps $PWD/libsimpleprofileds.so=$PWD/maps.json
blueline:/data/data/com.google.android.apps.maps # # Do things on the app.
blueline:/data/data/com.google.android.apps.maps # kill -3 9143
blueline:/data/data/com.google.android.apps.maps # wc -l maps.json
17901 maps.json
blueline:/data/data/com.google.android.apps.maps # ^D
% adb pull /data/data/com.google.android.apps.maps/maps.json
% jq 'sort_by(.count) | reverse' maps.json | head -13
[
  {
    "class_name": "Ljava/lang/String;",
    "method_name": "charAt",
    "method_descriptor": "(I)C",
    "count": 586996
  },
  {
    "class_name": "Ljava/lang/Object;",
    "method_name": "<init>",
    "method_descriptor": "()V",
    "count": 482491
  },
```

Change-Id: I87056f641648c496c63c9de715951b65707844ef
diff --git a/tools/jvmti-agents/README.md b/tools/jvmti-agents/README.md
index 69d4921..b093289 100644
--- a/tools/jvmti-agents/README.md
+++ b/tools/jvmti-agents/README.md
@@ -12,6 +12,7 @@
 * [libjitload](./jit-load)
 * [liblistextensions](./list-extensions)
 * [libforceredefine](./simple-force-redefine)
+* [libsimpleprofile](./simple-profile)
 * [litifast](./ti-fast)
 * [libtitrace](./titrace)
 * [libwrapagentproperties](./wrapagentproperties)
\ No newline at end of file
diff --git a/tools/jvmti-agents/simple-profile/Android.bp b/tools/jvmti-agents/simple-profile/Android.bp
new file mode 100644
index 0000000..f524cac
--- /dev/null
+++ b/tools/jvmti-agents/simple-profile/Android.bp
@@ -0,0 +1,92 @@
+//
+// Copyright (C) 2020 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: "simpleprofile-base-defaults",
+    host_supported: true,
+    srcs: ["simple_profile.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",
+
+    target: {
+        android: {
+        },
+        host: {
+        },
+    },
+    header_libs: [
+        "jni_headers",
+        "libopenjdkjvmti_headers",
+        "libnativehelper_header_only",
+    ],
+}
+
+cc_defaults {
+    name: "simpleprofile-defaults",
+    shared_libs: [
+        "libbase",
+    ],
+}
+
+cc_defaults {
+    name: "simpleprofile-static-defaults",
+    host_supported: false,
+    defaults: ["simpleprofile-base-defaults"],
+
+    shared_libs: [
+        "liblog",
+    ],
+    static_libs: [
+        "libbase_ndk",
+    ],
+    sdk_version: "current",
+    stl: "c++_static",
+}
+
+art_cc_library {
+    name: "libsimpleprofiles",
+    defaults: ["simpleprofile-static-defaults"],
+}
+
+art_cc_library {
+    name: "libsimpleprofileds",
+    defaults: [
+        "art_debug_defaults",
+        "simpleprofile-static-defaults",
+    ],
+    shared_libs: [],
+}
+
+art_cc_library {
+    name: "libsimpleprofile",
+    defaults: ["simpleprofile-defaults"],
+    shared_libs: [
+    ],
+}
+
+art_cc_library {
+    name: "libsimpleprofiled",
+    defaults: [
+        "art_debug_defaults",
+        "simpleprofile-defaults",
+    ],
+    shared_libs: [],
+}
diff --git a/tools/jvmti-agents/simple-profile/README.md b/tools/jvmti-agents/simple-profile/README.md
new file mode 100644
index 0000000..4a069fb
--- /dev/null
+++ b/tools/jvmti-agents/simple-profile/README.md
@@ -0,0 +1,63 @@
+# simpleprofile
+
+simpleprofile is a JVMTI agent that lets one get simple JSON profiles with JVMTI
+
+# Usage
+### Build
+>    `m libsimpleprofile`  # or 'm libsimpleprofiled' with debugging checks enabled
+
+For binaries with NDK shared libraries only.
+>    `m libsimpleprofiled` # or `m libsimpleprofileds` with debugging checks enabled.
+
+The libraries will be built for 32-bit, 64-bit, host and target. Below examples
+assume you want to use the 64-bit version.
+
+### Command Line
+
+The agent is loaded using -agentpath like normal. It takes arguments in the
+following format:
+>     `file-output[,dump_on_shutdown][,dump_on_main_stop]`
+
+
+#### ART
+>    `art -Xplugin:$ANDROID_HOST_OUT/lib64/libopenjdkjvmti.so '-agentpath:libsimpleprofiled.so=/proc/self/fd/2,dump_on_main_stop' -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.
+
+#### Device
+```
+% adb root
+% adb shell setenforce 0
+% adb push $OUT/system/lib64/libsimpleprofileds.so /data/local/tmp/libsimpleprofileds.so
+% adb shell
+blueline:/data/data/com.google.android.apps.maps # cp /data/local/tmp/libsimpleprofileds.so .
+blueline:/data/data/com.google.android.apps.maps # ps -A | grep maps
+u0_a178        9143    927 15691440 190132 SyS_epoll_wait     0 S com.google.android.apps.maps
+blueline:/data/data/com.google.android.apps.maps # cmd activity attach-agent com.google.android.apps.maps $PWD/libsimpleprofileds.so=$PWD/maps.json
+blueline:/data/data/com.google.android.apps.maps # # Do things on the app.
+blueline:/data/data/com.google.android.apps.maps # kill -3 9143
+blueline:/data/data/com.google.android.apps.maps # wc -l maps.json
+17901 maps.json
+blueline:/data/data/com.google.android.apps.maps # ^D
+% adb pull /data/data/com.google.android.apps.maps/maps.json
+```
+
+#### RI
+>    `java '-agentpath:libsimpleprofiled.so=/proc/self/fd/2,dump_on_main_stop' -cp tmp/helloworld/classes helloworld`
+
+### Output
+A normal run will look something like this:
+
+    % ./test/run-test --64 --host --dev --with-agent $ANDROID_HOST_OUT/lib64/libsimpleprofiled.so=dump_on_main_stop,/proc/self/fd/1 001-HelloWorld
+    <normal output removed>
+    Hello, world!
+    ...
+    {"class_name":"Ljava/util/HashMap$KeySet;","method_name":"iterator","method_descriptor":"()Ljava/util/Iterator;","count":6},
+    {"class_name":"Ljava/util/HashMap$KeyIterator;","method_name":"<init>","method_descriptor":"(Ljava/util/HashMap;)V","count":6},
+    {"class_name":"Ljava/util/HashMap$HashIterator;","method_name":"<init>","method_descriptor":"(Ljava/util/HashMap;)V","count":6},
+    {"class_name":"Ljava/lang/String;","method_name":"equals","method_descriptor":"(Ljava/lang/Object;)Z","count":128},
+    {"class_name":"Ljava/util/Collections$UnmodifiableCollection$1;","method_name":"next","method_descriptor":"()Ljava/lang/Object;","count":38},
+    {"class_name":"Ljava/util/HashMap$KeyIterator;","method_name":"next","method_descriptor":"()Ljava/lang/Object;","count":38},
+    {"class_name":"Lsun/misc/Cleaner;","method_name":"add","method_descriptor":"(Lsun/misc/Cleaner;)Lsun/misc/Cleaner;","count":1},
+    ...
diff --git a/tools/jvmti-agents/simple-profile/simple_profile.cc b/tools/jvmti-agents/simple-profile/simple_profile.cc
new file mode 100644
index 0000000..5ead97e
--- /dev/null
+++ b/tools/jvmti-agents/simple-profile/simple_profile.cc
@@ -0,0 +1,523 @@
+// Copyright (C) 2020 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 <fcntl.h>
+#include <jni.h>
+#include <jvmti.h>
+
+#include <atomic>
+#include <cstring>
+#include <iomanip>
+#include <iostream>
+#include <memory>
+#include <sstream>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "android-base/unique_fd.h"
+#include "nativehelper/scoped_local_ref.h"
+
+namespace simple_profile {
+
+static constexpr jint kArtTiVersion = JVMTI_VERSION_1_2 | 0x40000000;
+
+#define CHECK_JVMTI(a) CHECK_EQ(JVMTI_ERROR_NONE, a)
+
+struct DataDefinition {
+  std::string_view class_name;
+  std::string_view method_name;
+  std::string_view method_descriptor;
+  uint64_t count;
+};
+
+std::ostream& operator<<(std::ostream& os, const DataDefinition& dd) {
+  return os << "{\"class_name\":\"" << dd.class_name << "\",\"method_name\":\"" << dd.method_name
+            << "\",\"method_descriptor\":\"" << dd.method_descriptor << "\",\"count\":" << dd.count
+            << "}";
+}
+
+class SimpleProfileData {
+ public:
+  SimpleProfileData(
+      jvmtiEnv* env, std::string out_fd_name, int fd, bool dump_on_shutdown, bool dump_on_main_stop)
+      : dump_id_(0),
+        out_fd_name_(out_fd_name),
+        out_fd_(fd),
+        shutdown_(false),
+        dump_on_shutdown_(dump_on_shutdown || dump_on_main_stop),
+        dump_on_main_stop_(dump_on_main_stop) {
+    CHECK_JVMTI(env->CreateRawMonitor("simple_profile_mon", &mon_));
+    method_counts_.reserve(10000);
+  }
+
+  void Dump(jvmtiEnv* jvmti);
+  void Enter(jvmtiEnv* jvmti, JNIEnv* env, jmethodID meth);
+
+  void RunDumpLoop(jvmtiEnv* jvmti, JNIEnv* env);
+
+  static SimpleProfileData* GetProfileData(jvmtiEnv* env) {
+    void* data;
+    CHECK_JVMTI(env->GetEnvironmentLocalStorage(&data));
+    return static_cast<SimpleProfileData*>(data);
+  }
+
+  void FinishInitialization(jvmtiEnv* jvmti, JNIEnv* jni, jthread cur);
+  void Shutdown(jvmtiEnv* jvmti, JNIEnv* jni);
+
+ private:
+  void DoDump(jvmtiEnv* jvmti, JNIEnv* jni, std::unordered_map<jmethodID, uint64_t> copy);
+
+  jlong dump_id_;
+  jrawMonitorID mon_;
+  std::string out_fd_name_;
+  int out_fd_;
+  std::unordered_map<jmethodID, uint64_t> method_counts_;
+  bool shutdown_;
+  bool dump_on_shutdown_;
+  bool dump_on_main_stop_;
+};
+
+struct ScopedJvmtiMonitor {
+ public:
+  ScopedJvmtiMonitor(jvmtiEnv* env, jrawMonitorID mon) : jvmti_(env), mon_(mon) {
+    CHECK_JVMTI(jvmti_->RawMonitorEnter(mon_));
+  }
+
+  ~ScopedJvmtiMonitor() {
+    CHECK_JVMTI(jvmti_->RawMonitorExit(mon_));
+  }
+
+  void Notify() {
+    CHECK_JVMTI(jvmti_->RawMonitorNotifyAll(mon_));
+  }
+
+  void Wait() {
+    CHECK_JVMTI(jvmti_->RawMonitorWait(mon_, 0));
+  }
+
+ private:
+  jvmtiEnv* jvmti_;
+  jrawMonitorID mon_;
+};
+
+void SimpleProfileData::Enter(jvmtiEnv* jvmti, JNIEnv* env, jmethodID meth) {
+  ScopedJvmtiMonitor sjm(jvmti, mon_);
+  // Keep all classes from being unloaded to allow us to know we can get the method info later.
+  jclass tmp;
+  CHECK_JVMTI(jvmti->GetMethodDeclaringClass(meth, &tmp));
+  ScopedLocalRef<jclass> klass(env, tmp);
+  jlong tag;
+  CHECK_JVMTI(jvmti->GetTag(klass.get(), &tag));
+  if (tag == 0) {
+    CHECK_JVMTI(jvmti->SetTag(klass.get(), 1u));
+    env->NewGlobalRef(klass.get());
+  }
+  method_counts_.insert({ meth, 0u }).first->second++;
+}
+
+void SimpleProfileData::Dump(jvmtiEnv* jvmti) {
+  ScopedJvmtiMonitor sjm(jvmti, mon_);
+  dump_id_++;
+  sjm.Notify();
+}
+
+void SimpleProfileData::RunDumpLoop(jvmtiEnv* jvmti, JNIEnv* env) {
+  jlong current_id = 0;
+  do {
+    std::unordered_map<jmethodID, uint64_t> copy;
+    {
+      ScopedJvmtiMonitor sjm(jvmti, mon_);
+      while (!shutdown_ && current_id == dump_id_) {
+        sjm.Wait();
+      }
+      if (shutdown_) {
+        break;
+      }
+      current_id = dump_id_;
+      copy = method_counts_;
+    }
+    DoDump(jvmti, env, std::move(copy));
+  } while (true);
+}
+
+void SimpleProfileData::Shutdown(jvmtiEnv* jvmti, JNIEnv* jni) {
+  std::unordered_map<jmethodID, uint64_t> copy;
+  {
+    ScopedJvmtiMonitor sjm(jvmti, mon_);
+    if (shutdown_) {
+      return;
+    }
+    shutdown_ = true;
+    copy = method_counts_;
+    sjm.Notify();
+  }
+  if (dump_on_shutdown_) {
+    DoDump(jvmti, jni, std::move(copy));
+  }
+}
+
+void SimpleProfileData::FinishInitialization(jvmtiEnv* jvmti, JNIEnv* env, jthread cur) {
+  // Finish up startup.
+  // Create a Thread object.
+  std::string name = std::string("profile dump Thread: ") + this->out_fd_name_;
+  ScopedLocalRef<jobject> thread_name(env, env->NewStringUTF(name.c_str()));
+  CHECK_NE(thread_name.get(), nullptr);
+
+  ScopedLocalRef<jclass> thread_klass(env, env->FindClass("java/lang/Thread"));
+  CHECK_NE(thread_klass.get(), nullptr);
+  ScopedLocalRef<jobject> thread(env, env->AllocObject(thread_klass.get()));
+  CHECK_NE(thread.get(), nullptr);
+  jmethodID initID = env->GetMethodID(thread_klass.get(), "<init>", "(Ljava/lang/String;)V");
+  jmethodID setDaemonId = env->GetMethodID(thread_klass.get(), "setDaemon", "(Z)V");
+  CHECK_NE(initID, nullptr);
+  CHECK_NE(setDaemonId, nullptr);
+  env->CallNonvirtualVoidMethod(thread.get(), thread_klass.get(), initID, thread_name.get());
+  env->CallVoidMethod(thread.get(), setDaemonId, JNI_TRUE);
+  CHECK(!env->ExceptionCheck());
+
+  CHECK_JVMTI(jvmti->RunAgentThread(
+      thread.get(),
+      [](jvmtiEnv* jvmti, JNIEnv* jni, void* unused_data ATTRIBUTE_UNUSED) {
+        SimpleProfileData* data = SimpleProfileData::GetProfileData(jvmti);
+        data->RunDumpLoop(jvmti, jni);
+      },
+      nullptr,
+      JVMTI_THREAD_NORM_PRIORITY));
+
+  CHECK_JVMTI(jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, nullptr));
+  CHECK_JVMTI(
+      jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_DATA_DUMP_REQUEST, nullptr));
+  if (dump_on_main_stop_) {
+    CHECK_JVMTI(jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_THREAD_END, cur));
+  }
+  CHECK_JVMTI(jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_DEATH, nullptr));
+}
+
+class ScopedClassInfo {
+ public:
+  ScopedClassInfo(jvmtiEnv* jvmti_env, jclass c)
+      : jvmti_env_(jvmti_env), class_(c), name_(nullptr), generic_(nullptr) {}
+
+  ~ScopedClassInfo() {
+    if (class_ != nullptr) {
+      jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(name_));
+      jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(generic_));
+    }
+  }
+
+  bool Init() {
+    if (class_ == nullptr) {
+      name_ = const_cast<char*>("<NONE>");
+      generic_ = const_cast<char*>("<NONE>");
+      return true;
+    } else {
+      return jvmti_env_->GetClassSignature(class_, &name_, &generic_) == JVMTI_ERROR_NONE;
+    }
+  }
+
+  jclass GetClass() const {
+    return class_;
+  }
+  const char* GetName() const {
+    return name_;
+  }
+  // Generic type parameters, whatever is in the <> for a class
+  const char* GetGeneric() const {
+    return generic_;
+  }
+
+ private:
+  jvmtiEnv* jvmti_env_;
+  jclass class_;
+  char* name_;
+  char* generic_;
+};
+
+class ScopedMethodInfo {
+ public:
+  ScopedMethodInfo(jvmtiEnv* jvmti_env, JNIEnv* env, jmethodID method)
+      : jvmti_env_(jvmti_env),
+        env_(env),
+        method_(method),
+        declaring_class_(nullptr),
+        class_info_(nullptr),
+        name_(nullptr),
+        signature_(nullptr),
+        generic_(nullptr) {}
+
+  ~ScopedMethodInfo() {
+    env_->DeleteLocalRef(declaring_class_);
+    jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(name_));
+    jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(signature_));
+    jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(generic_));
+  }
+
+  bool Init() {
+    if (jvmti_env_->GetMethodDeclaringClass(method_, &declaring_class_) != JVMTI_ERROR_NONE) {
+      LOG(INFO) << "No decl";
+      return false;
+    }
+    class_info_.reset(new ScopedClassInfo(jvmti_env_, declaring_class_));
+    return class_info_->Init() &&
+           (jvmti_env_->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_;
+  }
+
+ private:
+  jvmtiEnv* jvmti_env_;
+  JNIEnv* env_;
+  jmethodID method_;
+  jclass declaring_class_;
+  std::unique_ptr<ScopedClassInfo> class_info_;
+  char* name_;
+  char* signature_;
+  char* generic_;
+
+  friend std::ostream& operator<<(std::ostream& os, ScopedMethodInfo const& method);
+};
+
+std::ostream& operator<<(std::ostream& os, const ScopedMethodInfo* method) {
+  return os << *method;
+}
+
+std::ostream& operator<<(std::ostream& os, ScopedMethodInfo const& method) {
+  return os << method.GetDeclaringClassInfo().GetName() << "->" << method.GetName()
+            << method.GetSignature();
+}
+
+void SimpleProfileData::DoDump(jvmtiEnv* jvmti,
+                               JNIEnv* jni,
+                               std::unordered_map<jmethodID, uint64_t> copy) {
+  std::ostringstream oss;
+  oss << "[";
+  bool is_first = true;
+  for (auto [meth, cnt] : copy) {
+    ScopedMethodInfo smi(jvmti, jni, meth);
+    if (!smi.Init()) {
+      continue;
+    }
+    if (!is_first) {
+      oss << "," << std::endl;
+    }
+    is_first = false;
+    oss << DataDefinition {
+      .class_name = smi.GetDeclaringClassInfo().GetName(),
+      .method_name = smi.GetName(),
+      .method_descriptor = smi.GetSignature(),
+      .count = cnt,
+    };
+  }
+  oss << "]";
+  CHECK_GE(TEMP_FAILURE_RETRY(write(out_fd_, oss.str().c_str(), oss.str().size())), 0)
+      << strerror(errno) << out_fd_ << " " << out_fd_name_;
+  fsync(out_fd_);
+}
+
+static void DataDumpCb(jvmtiEnv* jvmti_env) {
+  SimpleProfileData* data = SimpleProfileData::GetProfileData(jvmti_env);
+  data->Dump(jvmti_env);
+}
+
+static void MethodEntryCB(jvmtiEnv* jvmti_env,
+                          JNIEnv* env,
+                          jthread thread ATTRIBUTE_UNUSED,
+                          jmethodID method) {
+  SimpleProfileData* data = SimpleProfileData::GetProfileData(jvmti_env);
+  data->Enter(jvmti_env, env, method);
+}
+
+static void VMInitCB(jvmtiEnv* jvmti, JNIEnv* env, jthread thr) {
+  SimpleProfileData* data = SimpleProfileData::GetProfileData(jvmti);
+  data->FinishInitialization(jvmti, env, thr);
+}
+static void VMDeathCB(jvmtiEnv* jvmti, JNIEnv* env) {
+  SimpleProfileData* data = SimpleProfileData::GetProfileData(jvmti);
+  data->Shutdown(jvmti, env);
+}
+
+// Fills targets with the breakpoints to add.
+// Lname/of/Klass;->methodName(Lsig/of/Method)Lreturn/Type;@location,<...>
+static bool ParseArgs(const std::string& start_options,
+                      /*out*/ std::string* fd_name,
+                      /*out*/ int* fd,
+                      /*out*/ bool* dump_on_shutdown,
+                      /*out*/ bool* dump_on_main_stop) {
+  std::istringstream iss(start_options);
+  std::string item;
+  *dump_on_main_stop = false;
+  *dump_on_shutdown = false;
+  bool has_fd = false;
+  while (std::getline(iss, item, ',')) {
+    if (item == "dump_on_shutdown") {
+      *dump_on_shutdown = true;
+    } else if (item == "dump_on_main_stop") {
+      *dump_on_main_stop = true;
+    } else if (has_fd) {
+      LOG(ERROR) << "Too many args!";
+      return false;
+    } else {
+      has_fd = true;
+      *fd_name = item;
+      *fd = TEMP_FAILURE_RETRY(open(fd_name->c_str(), O_WRONLY | O_CLOEXEC | O_CREAT, 00666));
+      CHECK_GE(*fd, 0) << strerror(errno);
+    }
+  }
+  return has_fd;
+}
+
+enum class StartType {
+  OnAttach,
+  OnLoad,
+};
+
+static jint SetupJvmtiEnv(JavaVM* vm, jvmtiEnv** jvmti) {
+  jint res = 0;
+  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;
+}
+
+static jint AgentStart(StartType start,
+                       JavaVM* vm,
+                       const char* options,
+                       void* reserved ATTRIBUTE_UNUSED) {
+  if (options == nullptr) {
+    options = "";
+  }
+  jvmtiEnv* jvmti = nullptr;
+  jvmtiError error = JVMTI_ERROR_NONE;
+  {
+    jint res = 0;
+    res = SetupJvmtiEnv(vm, &jvmti);
+
+    if (res != JNI_OK || jvmti == nullptr) {
+      LOG(ERROR) << "Unable to access JVMTI, error code " << res;
+      return JNI_ERR;
+    }
+  }
+
+  int fd;
+  std::string fd_name;
+  bool dump_on_shutdown;
+  bool dump_on_main_stop;
+  if (!ParseArgs(options,
+                 /*out*/ &fd_name,
+                 /*out*/ &fd,
+                 /*out*/ &dump_on_shutdown,
+                 /*out*/ &dump_on_main_stop)) {
+    LOG(ERROR) << "failed to get output file from " << options << "!";
+    return JNI_ERR;
+  }
+
+  void* data_mem = nullptr;
+  error = jvmti->Allocate(sizeof(SimpleProfileData), reinterpret_cast<unsigned char**>(&data_mem));
+  if (error != JVMTI_ERROR_NONE) {
+    LOG(ERROR) << "Unable to alloc memory for breakpoint target data";
+    return JNI_ERR;
+  }
+
+  SimpleProfileData* data =
+      new (data_mem) SimpleProfileData(jvmti, fd_name, fd, dump_on_shutdown, dump_on_main_stop);
+  error = jvmti->SetEnvironmentLocalStorage(data);
+  if (error != JVMTI_ERROR_NONE) {
+    LOG(ERROR) << "Unable to set local storage";
+    return JNI_ERR;
+  }
+
+  jvmtiCapabilities caps {};
+  caps.can_generate_method_entry_events = JNI_TRUE;
+  caps.can_tag_objects = JNI_TRUE;
+  error = jvmti->AddCapabilities(&caps);
+  if (error != JVMTI_ERROR_NONE) {
+    LOG(ERROR) << "Unable to set caps";
+    return JNI_ERR;
+  }
+
+  jvmtiEventCallbacks callbacks {};
+  callbacks.MethodEntry = &MethodEntryCB;
+  callbacks.VMInit = &VMInitCB;
+  callbacks.DataDumpRequest = &DataDumpCb;
+  callbacks.VMDeath = &VMDeathCB;
+  callbacks.ThreadEnd = [](jvmtiEnv* env, JNIEnv* jni, jthread thr ATTRIBUTE_UNUSED) {
+    VMDeathCB(env, jni);
+  };
+
+  error = jvmti->SetEventCallbacks(&callbacks, static_cast<jint>(sizeof(callbacks)));
+
+  if (error != JVMTI_ERROR_NONE) {
+    LOG(ERROR) << "Unable to set event callbacks.";
+    return JNI_ERR;
+  }
+
+  if (start == StartType::OnAttach) {
+    JNIEnv* env = nullptr;
+    jint res = 0;
+    res = vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_2);
+    if (res != JNI_OK || env == nullptr) {
+      LOG(ERROR) << "Unable to get jnienv";
+      return JNI_ERR;
+    }
+    jthread temp;
+    ScopedLocalRef<jthread> cur(env, nullptr);
+    CHECK_JVMTI(jvmti->GetCurrentThread(&temp));
+    cur.reset(temp);
+    VMInitCB(jvmti, env, cur.get());
+  } else {
+    error = jvmti->SetEventNotificationMode(
+        JVMTI_ENABLE, JVMTI_EVENT_VM_INIT, nullptr /* all threads */);
+    if (error != JVMTI_ERROR_NONE) {
+      LOG(ERROR) << "Unable to set event vminit";
+      return JNI_ERR;
+    }
+  }
+  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(StartType::OnAttach, vm, options, reserved);
+}
+
+// Early attachment
+extern "C" JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* jvm, char* options, void* reserved) {
+  return AgentStart(StartType::OnLoad, jvm, options, reserved);
+}
+
+}  // namespace simple_profile