/*
 * 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 "perfetto/heap_profile.h"
#include "src/profiling/memory/heap_profile_internal.h"

#include <malloc.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include <atomic>
#include <cinttypes>
#include <memory>
#include <type_traits>

#include "perfetto/base/build_config.h"
#include "perfetto/base/logging.h"
#include "perfetto/ext/base/string_utils.h"
#include "perfetto/ext/base/unix_socket.h"
#include "perfetto/ext/base/utils.h"

#include "src/profiling/memory/client.h"
#include "src/profiling/memory/client_api_factory.h"
#include "src/profiling/memory/scoped_spinlock.h"
#include "src/profiling/memory/unhooked_allocator.h"
#include "src/profiling/memory/wire_protocol.h"

struct AHeapInfo {
  // Fields set by user.
  char heap_name[HEAPPROFD_HEAP_NAME_SZ];
  void (*enabled_callback)(void*, const AHeapProfileEnableCallbackInfo*);
  void (*disabled_callback)(void*, const AHeapProfileDisableCallbackInfo*);
  void* enabled_callback_data;
  void* disabled_callback_data;

  // Internal fields.
  perfetto::profiling::Sampler sampler;
  std::atomic<bool> ready;
  std::atomic<bool> enabled;
  std::atomic<uint64_t> adaptive_sampling_shmem_threshold;
  std::atomic<uint64_t> adaptive_sampling_max_sampling_interval_bytes;
};

struct AHeapProfileEnableCallbackInfo {
  uint64_t sampling_interval;
};

struct AHeapProfileDisableCallbackInfo {};

namespace {

using perfetto::profiling::ScopedSpinlock;
using perfetto::profiling::UnhookedAllocator;

#if defined(__GLIBC__)
const char* getprogname() {
  return program_invocation_short_name;
}
#elif !defined(__BIONIC__)
const char* getprogname() {
  return "";
}
#endif

// Holds the active profiling client. Is empty at the start, or after we've
// started shutting down a profiling session. Hook invocations take shared_ptr
// copies (ensuring that the client stays alive until no longer needed), and do
// nothing if this primary pointer is empty.
//
// This shared_ptr itself is protected by g_client_lock. Note that shared_ptr
// handles are not thread-safe by themselves:
// https://en.cppreference.com/w/cpp/memory/shared_ptr/atomic
//
// To avoid on-destruction re-entrancy issues, this shared_ptr needs to be
// constructed with an allocator that uses the unhooked malloc & free functions.
// See UnhookedAllocator.
//
// We initialize this storage the first time GetClientLocked is called. We
// cannot use a static initializer because that leads to ordering problems
// of the ELF's constructors.

alignas(std::shared_ptr<perfetto::profiling::Client>) char g_client_arr[sizeof(
    std::shared_ptr<perfetto::profiling::Client>)];

bool g_client_init;

std::shared_ptr<perfetto::profiling::Client>* GetClientLocked() {
  if (!g_client_init) {
    new (g_client_arr) std::shared_ptr<perfetto::profiling::Client>;
    g_client_init = true;
  }
  return reinterpret_cast<std::shared_ptr<perfetto::profiling::Client>*>(
      &g_client_arr);
}

constexpr auto kMinHeapId = 1;
constexpr auto kMaxNumHeaps = 256;

AHeapInfo g_heaps[kMaxNumHeaps] = {};

AHeapInfo& GetHeap(uint32_t id) {
  return g_heaps[id];
}

// Protects g_client, and serves as an external lock for sampling decisions (see
// perfetto::profiling::Sampler).
//
// We rely on this atomic's destuction being a nop, as it is possible for the
// hooks to attempt to acquire the spinlock after its destructor should have run
// (technically a use-after-destruct scenario).
static_assert(
    std::is_trivially_destructible<perfetto::profiling::Spinlock>::value,
    "lock must be trivially destructible.");
perfetto::profiling::Spinlock g_client_lock{};

std::atomic<uint32_t> g_next_heap_id{kMinHeapId};

// This can get called while holding the spinlock (in normal operation), or
// without holding the spinlock (from OnSpinlockTimeout).
void DisableAllHeaps() {
  bool disabled[kMaxNumHeaps] = {};
  uint32_t max_heap = g_next_heap_id.load();
  // This has to be done in two passes, in case the disabled_callback for one
  // enabled heap uses another. In that case, the callbacks for the other heap
  // would time out trying to acquire the spinlock, which we hold here.
  for (uint32_t i = kMinHeapId; i < max_heap; ++i) {
    AHeapInfo& info = GetHeap(i);
    if (!info.ready.load(std::memory_order_acquire))
      continue;
    disabled[i] = info.enabled.exchange(false, std::memory_order_acq_rel);
  }
  for (uint32_t i = kMinHeapId; i < max_heap; ++i) {
    if (!disabled[i]) {
      continue;
    }
    AHeapInfo& info = GetHeap(i);
    if (info.disabled_callback) {
      AHeapProfileDisableCallbackInfo disable_info;
      info.disabled_callback(info.disabled_callback_data, &disable_info);
    }
  }
}

#pragma GCC diagnostic push
#if PERFETTO_DCHECK_IS_ON()
#pragma GCC diagnostic ignored "-Wmissing-noreturn"
#endif

void OnSpinlockTimeout() {
  // Give up on profiling the process but leave it running.
  // The process enters into a poisoned state and will reject all
  // subsequent profiling requests.  The current session is kept
  // running but no samples are reported to it.
  PERFETTO_DFATAL_OR_ELOG(
      "Timed out on the spinlock - something is horribly wrong. "
      "Leaking heapprofd client.");
  DisableAllHeaps();
  perfetto::profiling::PoisonSpinlock(&g_client_lock);
}
#pragma GCC diagnostic pop

// Note: g_client can be reset by AHeapProfile_initSession without calling this
// function.
void ShutdownLazy(const std::shared_ptr<perfetto::profiling::Client>& client) {
  ScopedSpinlock s(&g_client_lock, ScopedSpinlock::Mode::Try);
  if (PERFETTO_UNLIKELY(!s.locked())) {
    OnSpinlockTimeout();
    return;
  }

  // other invocation already initiated shutdown
  if (*GetClientLocked() != client)
    return;

  DisableAllHeaps();
  // Clear primary shared pointer, such that later hook invocations become nops.
  GetClientLocked()->reset();
}

uint64_t MaybeToggleHeap(uint32_t heap_id,
                         perfetto::profiling::Client* client) {
  AHeapInfo& heap = GetHeap(heap_id);
  if (!heap.ready.load(std::memory_order_acquire))
    return 0;
  auto interval =
      GetHeapSamplingInterval(client->client_config(), heap.heap_name);
  // The callbacks must be called while NOT LOCKED. Because they run
  // arbitrary code, it would be very easy to build a deadlock.
  if (interval) {
    AHeapProfileEnableCallbackInfo session_info{interval};
    if (!heap.enabled.load(std::memory_order_acquire) &&
        heap.enabled_callback) {
      heap.enabled_callback(heap.enabled_callback_data, &session_info);
    }
    heap.adaptive_sampling_shmem_threshold.store(
        client->client_config().adaptive_sampling_shmem_threshold,
        std::memory_order_relaxed);
    heap.adaptive_sampling_max_sampling_interval_bytes.store(
        client->client_config().adaptive_sampling_max_sampling_interval_bytes,
        std::memory_order_relaxed);
    heap.enabled.store(true, std::memory_order_release);
    client->RecordHeapInfo(heap_id, &heap.heap_name[0], interval);
  } else if (heap.enabled.load(std::memory_order_acquire)) {
    heap.enabled.store(false, std::memory_order_release);
    if (heap.disabled_callback) {
      AHeapProfileDisableCallbackInfo info;
      heap.disabled_callback(heap.disabled_callback_data, &info);
    }
  }
  return interval;
}

// We're a library loaded into a potentially-multithreaded process, which might
// not be explicitly aware of this possiblity. Deadling with forks/clones is
// extremely complicated in such situations, but we attempt to handle certain
// cases.
//
// There are two classes of forking processes to consider:
//  * well-behaved processes that fork only when their threads (if any) are at a
//    safe point, and therefore not in the middle of our hooks/client.
//  * processes that fork with other threads in an arbitrary state. Though
//    technically buggy, such processes exist in practice.
//
// This atfork handler follows a crude lowest-common-denominator approach, where
// to handle the latter class of processes, we systematically leak any |Client|
// state (present only when actively profiling at the time of fork) in the
// postfork-child path.
//
// The alternative with acquiring all relevant locks in the prefork handler, and
// releasing the state postfork handlers, poses a separate class of edge cases,
// and is not deemed to be better as a result.
//
// Notes:
// * this atfork handler fires only for the |fork| libc entrypoint, *not*
//   |clone|. See client.cc's |IsPostFork| for some best-effort detection
//   mechanisms for clone/vfork.
// * it should be possible to start a new profiling session in this child
//   process, modulo the bionic's heapprofd-loading state machine being in the
//   right state.
// * we cannot avoid leaks in all cases anyway (e.g. during shutdown sequence,
//   when only individual straggler threads hold onto the Client).
void AtForkChild() {
  PERFETTO_LOG("heapprofd_client: handling atfork.");

  // A thread (that has now disappeared across the fork) could have been holding
  // the spinlock. We're now the only thread post-fork, so we can reset the
  // spinlock, though the state it protects (the |g_client| shared_ptr) might
  // not be in a consistent state.
  g_client_lock.locked.store(false);
  g_client_lock.poisoned.store(false);

  // We must not call the disabled callbacks here, because they might require
  // locks that are being held at the fork point.
  for (uint32_t i = kMinHeapId; i < g_next_heap_id.load(); ++i) {
    AHeapInfo& info = GetHeap(i);
    info.enabled.store(false);
  }
  // Leak the existing shared_ptr contents, including the profiling |Client| if
  // profiling was active at the time of the fork.
  // Note: this code assumes that the creation of the empty shared_ptr does not
  // allocate, which should be the case for all implementations as the
  // constructor has to be noexcept.
  new (g_client_arr) std::shared_ptr<perfetto::profiling::Client>();
}

}  // namespace

__attribute__((visibility("default"))) uint64_t
AHeapProfileEnableCallbackInfo_getSamplingInterval(
    const AHeapProfileEnableCallbackInfo* session_info) {
  return session_info->sampling_interval;
}

__attribute__((visibility("default"))) AHeapInfo* AHeapInfo_create(
    const char* heap_name) {
  size_t len = strlen(heap_name);
  if (len >= sizeof(AHeapInfo::heap_name)) {
    return nullptr;
  }

  uint32_t next_id = g_next_heap_id.fetch_add(1);
  if (next_id >= kMaxNumHeaps) {
    return nullptr;
  }

  if (next_id == kMinHeapId)
    perfetto::profiling::StartHeapprofdIfStatic();

  AHeapInfo& info = GetHeap(next_id);
  perfetto::base::StringCopy(info.heap_name, heap_name, sizeof(info.heap_name));
  return &info;
}

__attribute__((visibility("default"))) AHeapInfo* AHeapInfo_setEnabledCallback(
    AHeapInfo* info,
    void (*callback)(void*, const AHeapProfileEnableCallbackInfo*),
    void* data) {
  if (info == nullptr)
    return nullptr;
  if (info->ready.load(std::memory_order_relaxed)) {
    PERFETTO_ELOG(
        "AHeapInfo_setEnabledCallback called after heap was registered. "
        "This is always a bug.");
    return nullptr;
  }
  info->enabled_callback = callback;
  info->enabled_callback_data = data;
  return info;
}

__attribute__((visibility("default"))) AHeapInfo* AHeapInfo_setDisabledCallback(
    AHeapInfo* info,
    void (*callback)(void*, const AHeapProfileDisableCallbackInfo*),
    void* data) {
  if (info == nullptr)
    return nullptr;
  if (info->ready.load(std::memory_order_relaxed)) {
    PERFETTO_ELOG(
        "AHeapInfo_setDisabledCallback called after heap was registered. "
        "This is always a bug.");
    return nullptr;
  }
  info->disabled_callback = callback;
  info->disabled_callback_data = data;
  return info;
}

__attribute__((visibility("default"))) uint32_t AHeapProfile_registerHeap(
    AHeapInfo* info) {
  if (info == nullptr)
    return 0;
  info->ready.store(true, std::memory_order_release);
  auto heap_id = static_cast<uint32_t>(info - &g_heaps[0]);
  std::shared_ptr<perfetto::profiling::Client> client;
  {
    ScopedSpinlock s(&g_client_lock, ScopedSpinlock::Mode::Try);
    if (PERFETTO_UNLIKELY(!s.locked())) {
      OnSpinlockTimeout();
      return 0;
    }

    client = *GetClientLocked();
  }

  // Enable the heap immediately if there's a matching ongoing session.
  if (client) {
    uint64_t interval = MaybeToggleHeap(heap_id, client.get());
    if (interval) {
      ScopedSpinlock s(&g_client_lock, ScopedSpinlock::Mode::Try);
      if (PERFETTO_UNLIKELY(!s.locked())) {
        OnSpinlockTimeout();
        return 0;
      }
      info->sampler.SetSamplingInterval(interval);
    }
  }
  return heap_id;
}

__attribute__((visibility("default"))) bool
AHeapProfile_reportAllocation(uint32_t heap_id, uint64_t id, uint64_t size) {
  AHeapInfo& heap = GetHeap(heap_id);
  if (!heap.enabled.load(std::memory_order_acquire)) {
    return false;
  }
  size_t sampled_alloc_sz = 0;
  std::shared_ptr<perfetto::profiling::Client> client;
  {
    ScopedSpinlock s(&g_client_lock, ScopedSpinlock::Mode::Try);
    if (PERFETTO_UNLIKELY(!s.locked())) {
      OnSpinlockTimeout();
      return false;
    }

    auto* g_client_ptr = GetClientLocked();
    if (!*g_client_ptr)  // no active client (most likely shutting down)
      return false;
    auto& client_ptr = *g_client_ptr;

    if (s.blocked_us()) {
      client_ptr->AddClientSpinlockBlockedUs(s.blocked_us());
    }

    sampled_alloc_sz = heap.sampler.SampleSize(static_cast<size_t>(size));
    if (sampled_alloc_sz == 0)  // not sampling
      return false;
    if (client_ptr->write_avail() <
        client_ptr->adaptive_sampling_shmem_threshold()) {
      bool should_increment = true;
      if (client_ptr->adaptive_sampling_max_sampling_interval_bytes() != 0) {
        should_increment =
            heap.sampler.sampling_interval() <
            client_ptr->adaptive_sampling_max_sampling_interval_bytes();
      }
      if (should_increment) {
        uint64_t new_interval = 2 * heap.sampler.sampling_interval();
        heap.sampler.SetSamplingInterval(2 * heap.sampler.sampling_interval());
        client_ptr->RecordHeapInfo(heap_id, "", new_interval);
      }
    }

    client = client_ptr;  // owning copy
  }                       // unlock

  if (!client->RecordMalloc(heap_id, sampled_alloc_sz, size, id)) {
    ShutdownLazy(client);
    return false;
  }
  return true;
}

__attribute__((visibility("default"))) bool
AHeapProfile_reportSample(uint32_t heap_id, uint64_t id, uint64_t size) {
  const AHeapInfo& heap = GetHeap(heap_id);
  if (!heap.enabled.load(std::memory_order_acquire)) {
    return false;
  }
  std::shared_ptr<perfetto::profiling::Client> client;
  {
    ScopedSpinlock s(&g_client_lock, ScopedSpinlock::Mode::Try);
    if (PERFETTO_UNLIKELY(!s.locked())) {
      OnSpinlockTimeout();
      return false;
    }

    auto* g_client_ptr = GetClientLocked();
    if (!*g_client_ptr)  // no active client (most likely shutting down)
      return false;

    if (s.blocked_us()) {
      (*g_client_ptr)->AddClientSpinlockBlockedUs(s.blocked_us());
    }

    client = *g_client_ptr;  // owning copy
  }                          // unlock

  if (!client->RecordMalloc(heap_id, size, size, id)) {
    ShutdownLazy(client);
    return false;
  }
  return true;
}

__attribute__((visibility("default"))) void AHeapProfile_reportFree(
    uint32_t heap_id,
    uint64_t id) {
  const AHeapInfo& heap = GetHeap(heap_id);
  if (!heap.enabled.load(std::memory_order_acquire)) {
    return;
  }
  std::shared_ptr<perfetto::profiling::Client> client;
  {
    ScopedSpinlock s(&g_client_lock, ScopedSpinlock::Mode::Try);
    if (PERFETTO_UNLIKELY(!s.locked())) {
      OnSpinlockTimeout();
      return;
    }

    client = *GetClientLocked();  // owning copy (or empty)
    if (!client)
      return;

    if (s.blocked_us()) {
      client->AddClientSpinlockBlockedUs(s.blocked_us());
    }
  }

  if (!client->RecordFree(heap_id, id))
    ShutdownLazy(client);
}

__attribute__((visibility("default"))) bool AHeapProfile_initSession(
    void* (*malloc_fn)(size_t),
    void (*free_fn)(void*)) {
  static bool first_init = true;
  // Install an atfork handler to deal with *some* cases of the host forking.
  // The handler will be unpatched automatically if we're dlclosed.
  if (first_init && pthread_atfork(/*prepare=*/nullptr, /*parent=*/nullptr,
                                   &AtForkChild) != 0) {
    PERFETTO_PLOG("%s: pthread_atfork failed, not installing hooks.",
                  getprogname());
    return false;
  }
  first_init = false;

  // TODO(fmayer): Check other destructions of client and make a decision
  // whether we want to ban heap objects in the client or not.
  std::shared_ptr<perfetto::profiling::Client> old_client;
  {
    ScopedSpinlock s(&g_client_lock, ScopedSpinlock::Mode::Try);
    if (PERFETTO_UNLIKELY(!s.locked())) {
      OnSpinlockTimeout();
      return false;
    }

    auto* g_client_ptr = GetClientLocked();
    if (*g_client_ptr && (*g_client_ptr)->IsConnected()) {
      PERFETTO_LOG("%s: Rejecting concurrent profiling initialization.",
                   getprogname());
      return true;  // success as we're in a valid state
    }
    old_client = *g_client_ptr;
    g_client_ptr->reset();
  }

  old_client.reset();

  // The dispatch table never changes, so let the custom allocator retain the
  // function pointers directly.
  UnhookedAllocator<perfetto::profiling::Client> unhooked_allocator(malloc_fn,
                                                                    free_fn);

  // These factory functions use heap objects, so we need to run them without
  // the spinlock held.
  std::shared_ptr<perfetto::profiling::Client> client =
      perfetto::profiling::ConstructClient(unhooked_allocator);

  if (!client) {
    PERFETTO_LOG("%s: heapprofd_client not initialized, not installing hooks.",
                 getprogname());
    return false;
  }

  uint32_t max_heap = g_next_heap_id.load();
  bool heaps_enabled[kMaxNumHeaps] = {};

  PERFETTO_LOG("%s: heapprofd_client initialized.", getprogname());
  {
    ScopedSpinlock s(&g_client_lock, ScopedSpinlock::Mode::Try);
    if (PERFETTO_UNLIKELY(!s.locked())) {
      OnSpinlockTimeout();
      return false;
    }

    // This needs to happen under the lock for mutual exclusion regarding the
    // random engine.
    for (uint32_t i = kMinHeapId; i < max_heap; ++i) {
      AHeapInfo& heap = GetHeap(i);
      if (!heap.ready.load(std::memory_order_acquire)) {
        continue;
      }
      const uint64_t interval =
          GetHeapSamplingInterval(client->client_config(), heap.heap_name);
      if (interval) {
        heaps_enabled[i] = true;
        heap.sampler.SetSamplingInterval(interval);
      }
    }

    // This cannot have been set in the meantime. There are never two concurrent
    // calls to this function, as Bionic uses atomics to guard against that.
    PERFETTO_DCHECK(*GetClientLocked() == nullptr);
    *GetClientLocked() = client;
  }

  // We want to run MaybeToggleHeap last to make sure we never enable a heap
  // but subsequently return `false` from this function, which indicates to the
  // caller that we did not enable anything.
  //
  // For startup profiles, `false` is used by Bionic to signal it can unload
  // the library again.
  for (uint32_t i = kMinHeapId; i < max_heap; ++i) {
    if (!heaps_enabled[i]) {
      continue;
    }
    auto interval = MaybeToggleHeap(i, client.get());
    PERFETTO_DCHECK(interval > 0);
  }

  return true;
}
