You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
462 lines
14 KiB
462 lines
14 KiB
// 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 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;
|
|
|
|
class UniqueStringTable {
|
|
public:
|
|
UniqueStringTable() = default;
|
|
~UniqueStringTable() = default;
|
|
std::string Intern(const std::string& header, const std::string& key) {
|
|
if (map_.find(key) == map_.end()) {
|
|
map_[key] = next_index_;
|
|
// Emit definition line. E.g., =123,string
|
|
stream->Write(header + std::to_string(next_index_) + "," + key + "\n");
|
|
++next_index_;
|
|
}
|
|
return std::to_string(map_[key]);
|
|
}
|
|
private:
|
|
int32_t next_index_;
|
|
std::map<std::string, int32_t> map_;
|
|
};
|
|
|
|
static UniqueStringTable* string_table = nullptr;
|
|
|
|
// 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, JNIEnv* jni, jmethodID method_id) {
|
|
ScopedMethodInfo smi(jvmti, jni, method_id);
|
|
std::string method;
|
|
if (smi.Init(/*get_generic=*/false)) {
|
|
method = std::string(smi.GetDeclaringClassInfo().GetName()) +
|
|
"::" + smi.GetName() + smi.GetSignature();
|
|
} else {
|
|
method = "ERROR";
|
|
}
|
|
return string_table->Intern("+", method);
|
|
}
|
|
|
|
static int sampling_rate;
|
|
static int stack_depth_limit;
|
|
|
|
static void JNICALL logVMObjectAlloc(jvmtiEnv* jvmti,
|
|
JNIEnv* jni,
|
|
jthread thread,
|
|
jobject obj ATTRIBUTE_UNUSED,
|
|
jclass klass,
|
|
jlong size) {
|
|
// Sample only once out of sampling_rate tries, and prevent recursive allocation tracking,
|
|
static thread_local int sample_countdown = sampling_rate;
|
|
--sample_countdown;
|
|
if (sample_countdown != 0) {
|
|
return;
|
|
}
|
|
|
|
// Guard accesses to string table and emission.
|
|
static std::mutex mutex;
|
|
std::lock_guard<std::mutex> lg(mutex);
|
|
|
|
std::string record =
|
|
formatAllocation(jvmti,
|
|
jni,
|
|
jthreadContainer{.thread = thread},
|
|
klass,
|
|
jlongContainer{.val = size});
|
|
|
|
std::unique_ptr<jvmtiFrameInfo[]> stack_frames(new jvmtiFrameInfo[stack_depth_limit]);
|
|
jint stack_depth;
|
|
jvmtiError err = jvmti->GetStackTrace(thread,
|
|
0,
|
|
stack_depth_limit,
|
|
stack_frames.get(),
|
|
&stack_depth);
|
|
if (err == JVMTI_ERROR_NONE) {
|
|
// Emit stack frames in order from deepest in the stack to most recent.
|
|
// This simplifies post-collection processing.
|
|
for (int i = stack_depth - 1; i >= 0; --i) {
|
|
record += ";" + formatMethod(jvmti, jni, stack_frames[i].method);
|
|
}
|
|
}
|
|
stream->Write(string_table->Intern("=", record) + "\n");
|
|
|
|
sample_countdown = sampling_rate;
|
|
}
|
|
|
|
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 bool ProcessOptions(std::string options) {
|
|
std::string output_file_path;
|
|
if (options.empty()) {
|
|
static constexpr int kDefaultSamplingRate = 10;
|
|
static constexpr int kDefaultStackDepthLimit = 50;
|
|
static constexpr const char* kDefaultOutputFilePath = "/data/local/tmp/logstream.txt";
|
|
|
|
sampling_rate = kDefaultSamplingRate;
|
|
stack_depth_limit = kDefaultStackDepthLimit;
|
|
output_file_path = kDefaultOutputFilePath;
|
|
} else {
|
|
// options string should contain "sampling_rate,stack_depth_limit,output_file_path".
|
|
size_t comma_pos = options.find(',');
|
|
if (comma_pos == std::string::npos) {
|
|
return false;
|
|
}
|
|
sampling_rate = std::stoi(options.substr(0, comma_pos));
|
|
options = options.substr(comma_pos + 1);
|
|
comma_pos = options.find(',');
|
|
if (comma_pos == std::string::npos) {
|
|
return false;
|
|
}
|
|
stack_depth_limit = std::stoi(options.substr(0, comma_pos));
|
|
output_file_path = options.substr(comma_pos + 1);
|
|
}
|
|
LOG(INFO) << "Starting allocation tracing: sampling_rate=" << sampling_rate
|
|
<< ", stack_depth_limit=" << stack_depth_limit
|
|
<< ", output_file_path=" << output_file_path;
|
|
stream = new LockedStream(output_file_path);
|
|
|
|
return true;
|
|
}
|
|
|
|
static jint AgentStart(JavaVM* vm,
|
|
char* options,
|
|
void* reserved ATTRIBUTE_UNUSED) {
|
|
// Handle the sampling rate, depth limit, and output path, if set.
|
|
if (!ProcessOptions(options)) {
|
|
return JNI_ERR;
|
|
}
|
|
|
|
// 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();
|
|
|
|
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
|
|
|