blob: 1b359b8fb00766d000714a1b4c704ad0a76fbd5a [file] [log] [blame]
// 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,
[[maybe_unused]] jobject obj,
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, [[maybe_unused]] void* reserved) {
// 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