Add libfieldcount jvmti agent
This agent is useful for extracting information about the contents of
fields and measuring the memory impact of CLs based on the different
counts of various fields.
Test: manual
Test: ./test/run-test --host --dev --interpreter --runtime-option -Xopaque-jni-ids:true --64 --with-agent $ANDROID_HOST_OUT/lib64/libfieldcountd.so=Ljava/lang/Class\;.extData:Ldalvik/system/ClassExt\;,Ldalvik/system/ClassExt\;.jmethodIDs:Ljava/lang/Object\;,Ldalvik/system/ClassExt\;.staticJfieldIDs:Ljava/lang/Object\;,Ldalvik/system/ClassExt\;.instanceJfieldIDs:Ljava/lang/Object\; 001-HelloWorld
Bug: 134162467
Change-Id: I21cfded08f8d9f7db67a81e2ce88ef7567599d97
diff --git a/tools/jvmti-agents/field-counts/Android.bp b/tools/jvmti-agents/field-counts/Android.bp
new file mode 100644
index 0000000..0dd7509
--- /dev/null
+++ b/tools/jvmti-agents/field-counts/Android.bp
@@ -0,0 +1,88 @@
+//
+// 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: "fieldcount-base-defaults",
+ srcs: ["fieldcount.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",
+ // Annoyingly you aren't allowed to include even header-only non-ndk libs into an ndk build.
+ // Instead we put the directories this would bring in below in 'include_dirs'
+ // "libnativehelper_header_only",
+ ],
+ include_dirs: [
+ // NDK headers aren't available in platform NDK builds.
+ "libnativehelper/include_jni",
+ "libnativehelper/header_only_include",
+ ],
+ multilib: {
+ lib32: {
+ suffix: "32",
+ },
+ lib64: {
+ suffix: "64",
+ },
+ },
+ symlink_preferred_arch: true,
+}
+
+cc_defaults {
+ name: "fieldcount-defaults",
+ host_supported: true,
+ shared_libs: [
+ "libbase",
+ ],
+ defaults: ["fieldcount-base-defaults"],
+}
+
+cc_defaults {
+ name: "fieldcount-static-defaults",
+ host_supported: false,
+ defaults: ["fieldcount-base-defaults"],
+
+ shared_libs: [
+ "liblog",
+ ],
+ static_libs: [
+ "libbase_ndk",
+ ],
+ sdk_version: "current",
+ stl: "c++_static",
+}
+
+cc_library {
+ name: "libfieldcounts",
+ defaults: ["fieldcount-static-defaults"],
+}
+
+art_cc_library {
+ name: "libfieldcount",
+ defaults: ["fieldcount-defaults"],
+}
+
+art_cc_library {
+ name: "libfieldcountd",
+ defaults: [
+ "art_debug_defaults",
+ "fieldcount-defaults",
+ ],
+}
diff --git a/tools/jvmti-agents/field-counts/README.md b/tools/jvmti-agents/field-counts/README.md
new file mode 100644
index 0000000..b43881c
--- /dev/null
+++ b/tools/jvmti-agents/field-counts/README.md
@@ -0,0 +1,64 @@
+# fieldcount
+
+fieldcount is a JVMTI agent designed to investigate the types being held by specific fields and
+how large the objects referenced by these fields are.
+
+Note that just by using the agent some fields might be written (for example fields related to
+keeping track of jfieldIDs). Users should be aware of this.
+
+# Usage
+### Build
+> `make libfieldcount libfieldcounts`
+
+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:
+> `Lname/of/class;.nameOfField:Ltype/of/field;[,...]`
+
+#### ART
+```shell
+art -Xplugin:$ANDROID_HOST_OUT/lib64/libopenjdkjvmti.so '-agentpath:libfieldcount.so=Ljava/lang/Class;.extData:Ldalvik/system/ClassExt;,Ldalvik/system/ClassExt;.jmethodIDs:Ljava/lang/Object;' -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.
+
+```shell
+adb shell setenforce 0
+
+adb push $ANDROID_PRODUCT_OUT/system/lib64/libfieldcounts.so /data/local/tmp/
+
+adb shell am start-activity --attach-agent '/data/local/tmp/libfieldcounts.so=Ljava/lang/Class;.extData:Ldalvik/system/ClassExt;,Ldalvik/system/ClassExt;.jmethodIDs:Ljava/lang/Object;' some.debuggable.apps/.the.app.MainActivity
+```
+
+#### RI
+> `java '-agentpath:libfieldcount.so=Lname/of/class;.nameOfField:Ltype/of/field;' -cp tmp/helloworld/classes helloworld`
+
+### Printing the Results
+All statistics gathered during the trace are printed automatically when the
+program normally exits. In the case of Android applications, they are always
+killed, so we need to manually print the results.
+
+> `kill -SIGQUIT $(pid com.littleinc.orm_benchmark)`
+
+Will initiate a dump of the counts (to logcat).
+
+The dump will look something like this.
+
+```
+dalvikvm64 I 06-27 14:24:59 183155 183155 fieldcount.cc:60] listing field Ljava/lang/Class;.extData:Ldalvik/system/ClassExt;
+dalvikvm64 I 06-27 14:24:59 183155 183155 fieldcount.cc:60] listing field Ldalvik/system/ClassExt;.jmethodIDs:Ljava/lang/Object;
+Hello, world!
+dalvikvm64 I 06-27 14:24:59 183155 183155 fieldcount.cc:97] Dumping counts of fields.
+dalvikvm64 I 06-27 14:24:59 183155 183155 fieldcount.cc:98] Field name Type Count Total Size
+dalvikvm64 I 06-27 14:24:59 183155 183155 fieldcount.cc:155] Ljava/lang/Class;.extData:Ldalvik/system/ClassExt; <ALL TYPES> 2800 3024
+dalvikvm64 I 06-27 14:24:59 183155 183155 fieldcount.cc:161] Ljava/lang/Class;.extData:Ldalvik/system/ClassExt; Ldalvik/system/ClassExt; 64 3024
+dalvikvm64 I 06-27 14:24:59 183155 183155 fieldcount.cc:161] Ljava/lang/Class;.extData:Ldalvik/system/ClassExt; <null> 2738 0
+dalvikvm64 I 06-27 14:24:59 183155 183155 fieldcount.cc:155] Ldalvik/system/ClassExt;.jmethodIDs:Ljava/lang/Object; <ALL TYPES> 63 10008
+dalvikvm64 I 06-27 14:24:59 183155 183155 fieldcount.cc:161] Ldalvik/system/ClassExt;.jmethodIDs:Ljava/lang/Object; <null> 26 0
+dalvikvm64 I 06-27 14:24:59 183155 183155 fieldcount.cc:161] Ldalvik/system/ClassExt;.jmethodIDs:Ljava/lang/Object; [J 39 10008
+```
\ No newline at end of file
diff --git a/tools/jvmti-agents/field-counts/fieldcount.cc b/tools/jvmti-agents/field-counts/fieldcount.cc
new file mode 100644
index 0000000..e460104
--- /dev/null
+++ b/tools/jvmti-agents/field-counts/fieldcount.cc
@@ -0,0 +1,248 @@
+// 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 <nativehelper/scoped_local_ref.h>
+
+#include <atomic>
+#include <iomanip>
+#include <iostream>
+#include <istream>
+#include <jni.h>
+#include <jvmti.h>
+#include <memory>
+#include <sstream>
+#include <string.h>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+namespace fieldnull {
+
+#define CHECK_JVMTI(x) CHECK_EQ((x), JVMTI_ERROR_NONE)
+
+// 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;
+
+static JavaVM* java_vm = nullptr;
+
+// Field is "Lclass/name/here;.field_name:Lfield/type/here;"
+static std::pair<jclass, jfieldID> SplitField(JNIEnv* env, const std::string& field_id) {
+ CHECK_EQ(field_id[0], 'L');
+ env->PushLocalFrame(1);
+ std::istringstream is(field_id);
+ std::string class_name;
+ std::string field_name;
+ std::string field_type;
+
+ std::getline(is, class_name, '.');
+ std::getline(is, field_name, ':');
+ std::getline(is, field_type, '\0');
+
+ jclass klass = reinterpret_cast<jclass>(
+ env->NewGlobalRef(env->FindClass(class_name.substr(1, class_name.size() - 2).c_str())));
+ jfieldID field = env->GetFieldID(klass, field_name.c_str(), field_type.c_str());
+ CHECK(klass != nullptr);
+ CHECK(field != nullptr);
+ LOG(INFO) << "listing field " << field_id;
+ env->PopLocalFrame(nullptr);
+ return std::make_pair(klass, field);
+}
+
+static std::vector<std::pair<jclass, jfieldID>> GetRequestedFields(JNIEnv* env,
+ const std::string& args) {
+ std::vector<std::pair<jclass, jfieldID>> res;
+ std::stringstream args_stream(args);
+ std::string item;
+ while (std::getline(args_stream, item, ',')) {
+ if (item == "") {
+ continue;
+ }
+ res.push_back(SplitField(env, item));
+ }
+ return res;
+}
+
+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;
+}
+
+struct RequestList {
+ std::vector<std::pair<jclass, jfieldID>> fields_;
+};
+
+static void DataDumpRequestCb(jvmtiEnv* jvmti) {
+ JNIEnv* env = nullptr;
+ CHECK_EQ(java_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6), JNI_OK);
+ LOG(INFO) << "Dumping counts of fields.";
+ LOG(INFO) << "\t" << "Field name"
+ << "\t" << "Type"
+ << "\t" << "Count"
+ << "\t" << "Total Size";
+ RequestList* list;
+ CHECK_JVMTI(jvmti->GetEnvironmentLocalStorage(reinterpret_cast<void**>(&list)));
+ for (std::pair<jclass, jfieldID>& p : list->fields_) {
+ jclass klass = p.first;
+ jfieldID field = p.second;
+ // Make sure all instances of the class are tagged with the klass ptr value. Since this is a
+ // global ref it's guaranteed to be unique.
+ CHECK_JVMTI(jvmti->IterateOverInstancesOfClass(
+ p.first,
+ // We need to do this to all objects every time since we might be looking for multiple
+ // fields in classes that are subtypes of each other.
+ JVMTI_HEAP_OBJECT_EITHER,
+ /* class_tag, size, tag_ptr, user_data*/
+ [](jlong, jlong, jlong* tag_ptr, void* klass) -> jvmtiIterationControl {
+ *tag_ptr = static_cast<jlong>(reinterpret_cast<intptr_t>(klass));
+ return JVMTI_ITERATION_CONTINUE;
+ },
+ klass));
+ jobject* obj_list;
+ jint obj_len;
+ jlong tag = static_cast<jlong>(reinterpret_cast<intptr_t>(klass));
+ CHECK_JVMTI(jvmti->GetObjectsWithTags(1, &tag, &obj_len, &obj_list, nullptr));
+
+ std::unordered_map<std::string, size_t> class_sizes;
+ std::unordered_map<std::string, size_t> class_counts;
+ size_t total_size = 0;
+ for (jint i = 0; i < obj_len; i++) {
+ ScopedLocalRef<jobject> cur_thiz(env, obj_list[i]);
+ ScopedLocalRef<jobject> obj(env, env->GetObjectField(cur_thiz.get(), field));
+ std::string class_name("<null>");
+ jlong size = 0;
+ if (obj.get() != nullptr) {
+ char* class_name_tmp;
+ ScopedLocalRef<jclass> obj_klass(env, env->GetObjectClass(obj.get()));
+ CHECK_JVMTI(jvmti->GetClassSignature(obj_klass.get(), &class_name_tmp, nullptr));
+ CHECK_JVMTI(jvmti->GetObjectSize(obj.get(), &size));
+ class_name = class_name_tmp;
+ CHECK_JVMTI(jvmti->Deallocate(reinterpret_cast<unsigned char*>(class_name_tmp)));
+ }
+ total_size += static_cast<size_t>(size);
+ if (class_counts.find(class_name) == class_counts.end()) {
+ class_counts[class_name] = 1;
+ class_sizes[class_name] = 0;
+ }
+ class_sizes[class_name] += static_cast<size_t>(size);
+ class_counts[class_name]++;
+ }
+
+ char* field_name;
+ char* field_sig;
+ char* field_class_name;
+ CHECK_JVMTI(jvmti->GetFieldName(klass, field, &field_name, &field_sig, nullptr));
+ CHECK_JVMTI(jvmti->GetClassSignature(klass, &field_class_name, nullptr));
+ LOG(INFO) << "\t" << field_class_name << "." << field_name << ":" << field_sig
+ << "\t" << "<ALL TYPES>"
+ << "\t" << obj_len
+ << "\t" << total_size;
+ for (auto sz : class_sizes) {
+ size_t count = class_counts[sz.first];
+ LOG(INFO) << "\t" << field_class_name << "." << field_name << ":" << field_sig
+ << "\t" << sz.first
+ << "\t" << count
+ << "\t" << sz.second;
+ }
+ CHECK_JVMTI(jvmti->Deallocate(reinterpret_cast<unsigned char*>(field_name)));
+ CHECK_JVMTI(jvmti->Deallocate(reinterpret_cast<unsigned char*>(field_sig)));
+ CHECK_JVMTI(jvmti->Deallocate(reinterpret_cast<unsigned char*>(field_class_name)));
+ }
+}
+
+static void VMDeathCb(jvmtiEnv* jvmti, JNIEnv* env ATTRIBUTE_UNUSED) {
+ DataDumpRequestCb(jvmti);
+ RequestList* list = nullptr;
+ CHECK_JVMTI(jvmti->GetEnvironmentLocalStorage(reinterpret_cast<void**>(&list)));
+ delete list;
+}
+
+static void CreateFieldList(jvmtiEnv* jvmti, JNIEnv* env, const std::string& args) {
+ RequestList* list = nullptr;
+ CHECK_JVMTI(jvmti->Allocate(sizeof(*list), reinterpret_cast<unsigned char**>(&list)));
+ new (list) RequestList{
+ .fields_ = GetRequestedFields(env, args),
+ };
+ CHECK_JVMTI(jvmti->SetEnvironmentLocalStorage(list));
+}
+
+static void VMInitCb(jvmtiEnv* jvmti, JNIEnv* env, jobject thr ATTRIBUTE_UNUSED) {
+ char* args = nullptr;
+ CHECK_JVMTI(jvmti->GetEnvironmentLocalStorage(reinterpret_cast<void**>(&args)));
+ CHECK_JVMTI(jvmti->SetEnvironmentLocalStorage(nullptr));
+ CreateFieldList(jvmti, env, args);
+ CHECK_JVMTI(jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_DEATH, nullptr));
+ CHECK_JVMTI(
+ jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_DATA_DUMP_REQUEST, nullptr));
+ CHECK_JVMTI(jvmti->Deallocate(reinterpret_cast<unsigned char*>(args)));
+}
+
+static jint AgentStart(JavaVM* vm, char* options, bool is_onload) {
+ android::base::InitLogging(/* argv= */ nullptr);
+ java_vm = vm;
+ jvmtiEnv* jvmti = nullptr;
+ if (SetupJvmtiEnv(vm, &jvmti) != JNI_OK) {
+ LOG(ERROR) << "Could not get JVMTI env or ArtTiEnv!";
+ return JNI_ERR;
+ }
+ jvmtiCapabilities caps{
+ .can_tag_objects = 1,
+ };
+ CHECK_JVMTI(jvmti->AddCapabilities(&caps));
+ jvmtiEventCallbacks cb{
+ .VMInit = VMInitCb,
+ .DataDumpRequest = DataDumpRequestCb,
+ .VMDeath = VMDeathCb,
+ };
+ CHECK_JVMTI(jvmti->SetEventCallbacks(&cb, sizeof(cb)));
+ if (is_onload) {
+ unsigned char* ptr = nullptr;
+ CHECK_JVMTI(jvmti->Allocate(strlen(options) + 1, &ptr));
+ strcpy(reinterpret_cast<char*>(ptr), options);
+ CHECK_JVMTI(jvmti->SetEnvironmentLocalStorage(ptr));
+ CHECK_JVMTI(jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_INIT, nullptr));
+ } else {
+ JNIEnv* env = nullptr;
+ CHECK_EQ(vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6), JNI_OK);
+ CreateFieldList(jvmti, env, options);
+ CHECK_JVMTI(jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_DEATH, nullptr));
+ CHECK_JVMTI(
+ jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_DATA_DUMP_REQUEST, nullptr));
+ }
+ return JNI_OK;
+}
+
+// Late attachment (e.g. 'am attach-agent').
+extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm,
+ char* options,
+ void* reserved ATTRIBUTE_UNUSED) {
+ return AgentStart(vm, options, /*is_onload=*/false);
+}
+
+// Early attachment
+extern "C" JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* jvm,
+ char* options,
+ void* reserved ATTRIBUTE_UNUSED) {
+ return AgentStart(jvm, options, /*is_onload=*/true);
+}
+
+} // namespace fieldnull