// Copyright (C) 2017 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 "instruction_decoder.h"

#include <android-base/logging.h>
#include <atomic>
#include <jni.h>
#include <jvmti.h>
#include <map>
#include <memory>
#include <mutex>

// We could probably return a JNI_ERR here but lets crash instead if something fails.
#define CHECK_JVMTI_ERROR(jvmti, errnum) \
    CHECK_EQ(JVMTI_ERROR_NONE, (errnum)) << GetJvmtiErrorString((jvmti), (errnum)) << (" ")

namespace titrace {

static const char* GetJvmtiErrorString(jvmtiEnv* jvmti, jvmtiError errnum) {
  char* errnum_str = nullptr;
  jvmti->GetErrorName(errnum, /*out*/ &errnum_str);
  if (errnum_str == nullptr) {
    return "Unknown";
  }

  return errnum_str;
}

// Type-safe wrapper for JVMTI-allocated memory.
// Deallocates with jvmtiEnv::Deallocate.
template <typename T>
struct TiMemory {
  explicit TiMemory(jvmtiEnv* env, T* mem, size_t size) : env_(env), mem_(mem), size_(size) {
  }

  ~TiMemory() {
    if (mem_ != nullptr) {
      env_->Deallocate(static_cast<unsigned char*>(mem_));
    }
    mem_ = nullptr;
  }

  TiMemory(const TiMemory& other) = delete;
  TiMemory(TiMemory&& other) noexcept {
    env_ = other.env_;
    mem_ = other.mem_;
    size_ = other.size_;

    if (this != &other) {
      other.env_ = nullptr;
      other.mem_ = nullptr;
      other.size_ = 0u;
    }
  }

  TiMemory& operator=(TiMemory&& other) noexcept {
    if (mem_ != other.mem_) {
      TiMemory::~TiMemory();
    }
    new (this) TiMemory(std::move(other));
    return *this;
  }

  T* GetMemory() {
    return mem_;
  }

  size_t Size() {
    return size_ / sizeof(T);
  }

 private:
  jvmtiEnv* env_;
  T* mem_;
  size_t size_;
};

struct MethodBytecode {
  explicit MethodBytecode(jvmtiEnv* env, unsigned char* memory, jint size)
      : bytecode_(env, memory, static_cast<size_t>(size)) {
  }

  TiMemory<uint8_t> bytecode_;
};

struct TraceStatistics {
  static void Initialize(jvmtiEnv* jvmti) {
    TraceStatistics& stats = GetSingleton();

    bool is_ri = true;
    {
      jvmtiError error;
      char* value_ptr;
      error = jvmti->GetSystemProperty("java.vm.name", /*out*/ &value_ptr);
      CHECK_JVMTI_ERROR(jvmti, error) << "Failed to get property 'java.vm.name'";
      CHECK(value_ptr != nullptr) << "Returned property was null for 'java.vm.name'";

      if (strcmp("Dalvik", value_ptr) == 0) {
        is_ri = false;
      }
    }

    InstructionFileFormat format =
        is_ri ? InstructionFileFormat::kClass : InstructionFileFormat::kDex;
    stats.instruction_decoder_.reset(InstructionDecoder::NewInstance(format));

    CHECK_GE(arraysize(stats.instruction_counter_),
             stats.instruction_decoder_->GetMaximumOpcode());
  }

  static TraceStatistics& GetSingleton() {
    static TraceStatistics stats;
    return stats;
  }

  void Log() {
    LOG(INFO) << "================================================";
    LOG(INFO) << "              TI Trace // Summary               ";
    LOG(INFO) << "++++++++++++++++++++++++++++++++++++++++++++++++";
    LOG(INFO) << "  * Single step counter: " << single_step_counter_;
    LOG(INFO) << "+++++++++++    Instructions Count   ++++++++++++";

    size_t total = single_step_counter_;
    for (size_t i = 0; i < arraysize(instruction_counter_); ++i) {
      size_t inst_count = instruction_counter_[i];
      if (inst_count > 0) {
        const char* opcode_name = instruction_decoder_->GetName(i);
        LOG(INFO) << "  * " << opcode_name << "(op:" << i << "), count: " << inst_count
                  << ", % of total: " << (100.0 * inst_count / total);
      }
    }

    LOG(INFO) << "------------------------------------------------";
  }

  void OnSingleStep(jvmtiEnv* jvmti_env, jmethodID method, jlocation location) {
    // Counters do not need a happens-before.
    // Use the weakest memory order simply to avoid tearing.
    single_step_counter_.fetch_add(1u, std::memory_order_relaxed);

    MethodBytecode& bytecode = LookupBytecode(jvmti_env, method);

    // Decode jlocation value that depends on the bytecode format.
    size_t actual_location = instruction_decoder_->LocationToOffset(static_cast<size_t>(location));

    // Decode the exact instruction and increment its counter.
    CHECK_LE(actual_location, bytecode.bytecode_.Size());
    RecordInstruction(bytecode.bytecode_.GetMemory() + actual_location);
  }

 private:
  void RecordInstruction(const uint8_t* instruction) {
    uint8_t opcode = instruction[0];
    // Counters do not need a happens-before.
    // Use the weakest memory order simply to avoid tearing.
    instruction_counter_[opcode].fetch_add(1u, std::memory_order_relaxed);
  }

  MethodBytecode& LookupBytecode(jvmtiEnv* jvmti_env, jmethodID method) {
    jvmtiError error;
    std::lock_guard<std::mutex> lock(bytecode_cache_mutex_);

    auto it = bytecode_cache_.find(method);
    if (it == bytecode_cache_.end()) {
      jint bytecode_count_ptr = 0;
      unsigned char* bytecodes_ptr = nullptr;

      error = jvmti_env->GetBytecodes(method, &bytecode_count_ptr, &bytecodes_ptr);
      CHECK_JVMTI_ERROR(jvmti_env, error) << "Failed to get bytecodes for method " << method;
      CHECK(bytecodes_ptr != nullptr) << "Bytecode ptr was null for method " << method;
      CHECK_GE(bytecode_count_ptr, 0) << "Bytecode size too small for method " << method;

      // std::pair<iterator, bool inserted>
      auto&& pair = bytecode_cache_.insert(
          std::make_pair(method, MethodBytecode(jvmti_env, bytecodes_ptr, bytecode_count_ptr)));
      it = pair.first;
    }

    // Returning the address is safe. if map is resized, the contents will not move.
    return it->second;
  }

  std::unique_ptr<InstructionDecoder> instruction_decoder_;

  std::atomic<size_t> single_step_counter_{0u};
  std::atomic<size_t> instruction_counter_[256]{};

  // Cache the bytecode to avoid calling into JVMTI repeatedly.
  // TODO: invalidate if the bytecode was updated?
  std::map<jmethodID, MethodBytecode> bytecode_cache_;
  // bytecode cache is thread-safe.
  std::mutex bytecode_cache_mutex_;
};

struct EventCallbacks {
  static void SingleStep(jvmtiEnv* jvmti_env,
                         JNIEnv* jni_env ATTRIBUTE_UNUSED,
                         jthread thread ATTRIBUTE_UNUSED,
                         jmethodID method,
                         jlocation location) {
    TraceStatistics& stats = TraceStatistics::GetSingleton();
    stats.OnSingleStep(jvmti_env, method, location);
  }

  // Use "kill -SIGQUIT" to generate a data dump request.
  // Useful when running an android app since it doesn't go through
  // a normal Agent_OnUnload.
  static void DataDumpRequest(jvmtiEnv* jvmti_env ATTRIBUTE_UNUSED) {
    TraceStatistics& stats = TraceStatistics::GetSingleton();
    stats.Log();
  }
};

}  // namespace titrace

// Late attachment (e.g. 'am attach-agent').
JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char* options, void* reserved) {
  return Agent_OnLoad(vm, options, reserved);
}

// Early attachment (e.g. 'java -agent[lib|path]:filename.so').
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* jvm,
                                    char* /* options */,
                                    void* /* reserved */) {
  using namespace titrace;  // NOLINT [build/namespaces] [5]

  android::base::InitLogging(/* argv= */nullptr);

  jvmtiEnv* jvmti = nullptr;
  {
    jint res = 0;
    // Magic number that the agent can use to attach to non-debuggable apps on userdebug.
    constexpr jint kArtTiVersion = JVMTI_VERSION_1_2 | 0x40000000;
    res = jvm->GetEnv(reinterpret_cast<void**>(&jvmti), JVMTI_VERSION_1_1);

    if (res != JNI_OK || jvmti == nullptr) {
      res = jvm->GetEnv(reinterpret_cast<void**>(&jvmti), kArtTiVersion);
    }
    if (res != JNI_OK || jvmti == nullptr) {
      LOG(FATAL) << "Unable to access JVMTI, error code " << res;
    }
  }

  LOG(INFO) << "Agent_OnLoad: Hello World";

  {
    // Initialize our instruction file-format decoder.
    TraceStatistics::Initialize(jvmti);
  }

  jvmtiError error{};

  // Set capabilities.
  {
    jvmtiCapabilities caps = {};
    caps.can_generate_single_step_events = 1;
    caps.can_get_bytecodes = 1;

    error = jvmti->AddCapabilities(&caps);
    CHECK_JVMTI_ERROR(jvmti, error)
      << "Unable to get necessary JVMTI capabilities";
  }

  // Set callbacks.
  {
    jvmtiEventCallbacks callbacks = {};
    callbacks.SingleStep = &EventCallbacks::SingleStep;
    callbacks.DataDumpRequest = &EventCallbacks::DataDumpRequest;

    error = jvmti->SetEventCallbacks(&callbacks,
                                     static_cast<jint>(sizeof(callbacks)));
    CHECK_JVMTI_ERROR(jvmti, error) << "Unable to set event callbacks";
  }

  // Enable events notification.
  {
    error = jvmti->SetEventNotificationMode(JVMTI_ENABLE,
                                            JVMTI_EVENT_SINGLE_STEP,
                                            nullptr /* all threads */);
    CHECK_JVMTI_ERROR(jvmti, error)
      << "Failed to enable SINGLE_STEP notification";

    error = jvmti->SetEventNotificationMode(JVMTI_ENABLE,
                                            JVMTI_EVENT_DATA_DUMP_REQUEST,
                                            nullptr /* all threads */);
    CHECK_JVMTI_ERROR(jvmti, error)
      << "Failed to enable DATA_DUMP_REQUEST notification";
  }

  return JNI_OK;
}

// Note: This is not called for normal Android apps,
// use "kill -SIGQUIT" instead to generate a data dump request.
JNIEXPORT void JNICALL Agent_OnUnload(JavaVM* vm ATTRIBUTE_UNUSED) {
  using namespace titrace;  // NOLINT [build/namespaces] [5]
  LOG(INFO) << "Agent_OnUnload: Goodbye";

  TraceStatistics::GetSingleton().Log();
}

