// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/android/reached_code_profiler.h"

#include <signal.h>
#include <sys/time.h>
#include <ucontext.h>
#include <unistd.h>

#include <atomic>

#include "base/android/library_loader/anchor_functions.h"
#include "base/android/orderfile/orderfile_buildflags.h"
#include "base/android/reached_addresses_bitset.h"
#include "base/base_switches.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/important_file_writer.h"
#include "base/linux_util.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/path_service.h"
#include "base/scoped_generic.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/strings/stringprintf.h"
#include "base/synchronization/lock.h"
#include "base/task/single_thread_task_runner_forward.h"
#include "base/threading/thread.h"
#include "base/timer/timer.h"
#include "build/build_config.h"
#include "third_party/abseil-cpp/absl/types/optional.h"

#if !BUILDFLAG(SUPPORTS_CODE_ORDERING)
#error Code ordering support is required for the reached code profiler.
#endif

namespace base {
namespace android {

namespace {

#if !defined(NDEBUG) || defined(COMPONENT_BUILD)
// Always disabled for debug builds to avoid hitting a limit of signal
// interrupts that can get delivered into a single HANDLE_EINTR. Also
// debugging experience would be bad if there are a lot of signals flying
// around.
// Always disabled for component builds because in this case the code is not
// organized in one contiguous region which is required for the reached code
// profiler.
constexpr const bool kConfigurationSupported = false;
#else
constexpr const bool kConfigurationSupported = true;
#endif

constexpr const char kDumpToFileFlag[] = "reached-code-profiler-dump-to-file";

constexpr uint64_t kIterationsBeforeSkipping = 50;
constexpr uint64_t kIterationsBetweenUpdates = 100;
constexpr int kProfilerSignal = SIGWINCH;

constexpr base::TimeDelta kSamplingInterval = base::Milliseconds(10);
constexpr base::TimeDelta kDumpInterval = base::Seconds(30);

void HandleSignal(int signal, siginfo_t* info, void* context) {
  if (signal != kProfilerSignal)
    return;

  ucontext_t* ucontext = reinterpret_cast<ucontext_t*>(context);
#if defined(ARCH_CPU_ARM64)
  uintptr_t address = ucontext->uc_mcontext.pc;
#else
  uintptr_t address = ucontext->uc_mcontext.arm_pc;
#endif
  ReachedAddressesBitset::GetTextBitset()->RecordAddress(address);
}

struct ScopedTimerCloseTraits {
  static absl::optional<timer_t> InvalidValue() { return absl::nullopt; }

  static void Free(absl::optional<timer_t> x) { timer_delete(*x); }
};

// RAII object holding an interval timer.
using ScopedTimer =
    base::ScopedGeneric<absl::optional<timer_t>, ScopedTimerCloseTraits>;

void DumpToFile(const base::FilePath& path,
                scoped_refptr<base::SingleThreadTaskRunner> task_runner) {
  DCHECK(task_runner->BelongsToCurrentThread());

  auto dir_path = path.DirName();
  if (!base::DirectoryExists(dir_path) && !base::CreateDirectory(dir_path)) {
    PLOG(ERROR) << "Could not create " << dir_path;
    return;
  }

  std::vector<uint32_t> reached_offsets =
      ReachedAddressesBitset::GetTextBitset()->GetReachedOffsets();
  base::StringPiece contents(
      reinterpret_cast<const char*>(reached_offsets.data()),
      reached_offsets.size());
  if (!base::ImportantFileWriter::WriteFileAtomically(path, contents,
                                                      "ReachedDump")) {
    LOG(ERROR) << "Could not write reached dump into " << path;
  }

  task_runner->PostDelayedTask(
      FROM_HERE, base::BindOnce(&DumpToFile, path, task_runner), kDumpInterval);
}

class ReachedCodeProfiler {
 public:
  static ReachedCodeProfiler* GetInstance() {
    static base::NoDestructor<ReachedCodeProfiler> instance;
    return instance.get();
  }

  ReachedCodeProfiler(const ReachedCodeProfiler&) = delete;
  ReachedCodeProfiler& operator=(const ReachedCodeProfiler&) = delete;

  // Starts to periodically send |kProfilerSignal| to all threads.
  void Start(LibraryProcessType library_process_type,
             base::TimeDelta sampling_interval) {
    if (is_enabled_)
      return;

    // Set |kProfilerSignal| signal handler.
    // TODO(crbug.com/916263): consider restoring |old_handler| after the
    // profiler gets stopped.
    struct sigaction old_handler;
    struct sigaction sa;
    sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = &HandleSignal;
    sa.sa_flags = SA_RESTART | SA_SIGINFO;
    int ret = sigaction(kProfilerSignal, &sa, &old_handler);
    if (ret) {
      PLOG(ERROR) << "Error setting signal handler. The reached code profiler "
                     "is disabled";
      return;
    }

    // Create a new interval timer.
    struct sigevent sevp;
    memset(&sevp, 0, sizeof(sevp));
    sevp.sigev_notify = SIGEV_THREAD;
    sevp.sigev_notify_function = &OnTimerNotify;
    timer_t timerid;
    ret = timer_create(CLOCK_PROCESS_CPUTIME_ID, &sevp, &timerid);
    if (ret) {
      PLOG(ERROR)
          << "timer_create() failed. The reached code profiler is disabled";
      return;
    }

    timer_.reset(timerid);

    // Start the interval timer.
    struct itimerspec its;
    memset(&its, 0, sizeof(its));
    its.it_interval.tv_nsec = sampling_interval.InNanoseconds();
    its.it_value = its.it_interval;
    ret = timer_settime(timerid, 0, &its, nullptr);
    if (ret) {
      PLOG(ERROR)
          << "timer_settime() failed. The reached code profiler is disabled";
      return;
    }

    if (library_process_type == PROCESS_BROWSER)
      StartDumpingReachedCode();

    is_enabled_ = true;
  }

  // Stops profiling.
  void Stop() {
    timer_.reset();
    dumping_thread_.reset();
    is_enabled_ = false;
  }

  // Returns whether the profiler is currently enabled.
  bool IsEnabled() { return is_enabled_; }

 private:
  ReachedCodeProfiler()
      : current_pid_(getpid()), iteration_number_(0), is_enabled_(false) {}

  static void OnTimerNotify(sigval_t ignored) {
    ReachedCodeProfiler::GetInstance()->SendSignalToAllThreads();
  }

  void SendSignalToAllThreads() {
    // This code should be thread-safe.
    base::AutoLock scoped_lock(lock_);
    ++iteration_number_;

    if (iteration_number_ <= kIterationsBeforeSkipping ||
        iteration_number_ % kIterationsBetweenUpdates == 0) {
      tids_.clear();
      if (!base::GetThreadsForProcess(current_pid_, &tids_)) {
        LOG(WARNING) << "Failed to get a list of threads for process "
                     << current_pid_;
        return;
      }
    }

    pid_t current_tid = gettid();
    for (pid_t tid : tids_) {
      if (tid != current_tid)
        tgkill(current_pid_, tid, kProfilerSignal);
    }
  }

  void StartDumpingReachedCode() {
    const base::CommandLine* cmdline = base::CommandLine::ForCurrentProcess();
    if (!cmdline->HasSwitch(kDumpToFileFlag))
      return;

    base::FilePath dir_path(cmdline->GetSwitchValueASCII(kDumpToFileFlag));
    if (dir_path.empty()) {
      if (!base::PathService::Get(base::DIR_CACHE, &dir_path)) {
        LOG(WARNING) << "Failed to get cache dir path.";
        return;
      }
    }

    auto file_path =
        dir_path.Append(base::StringPrintf("reached-code-%d.txt", getpid()));

    dumping_thread_ =
        std::make_unique<base::Thread>("ReachedCodeProfilerDumpingThread");
    base::Thread::Options options;
    options.priority = base::ThreadPriority::BACKGROUND;
    dumping_thread_->StartWithOptions(std::move(options));
    dumping_thread_->task_runner()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(&DumpToFile, file_path, dumping_thread_->task_runner()),
        kDumpInterval);
  }

  base::Lock lock_;
  std::vector<pid_t> tids_;
  const pid_t current_pid_;
  uint64_t iteration_number_;
  ScopedTimer timer_;
  std::unique_ptr<base::Thread> dumping_thread_;

  bool is_enabled_;

  friend class NoDestructor<ReachedCodeProfiler>;
};

bool ShouldEnableReachedCodeProfiler() {
  if (!kConfigurationSupported)
    return false;

  const base::CommandLine* cmdline = base::CommandLine::ForCurrentProcess();
  return cmdline->HasSwitch(switches::kEnableReachedCodeProfiler);
}

}  // namespace

void InitReachedCodeProfilerAtStartup(LibraryProcessType library_process_type) {
  // The profiler shouldn't be run as part of webview.
  CHECK(library_process_type == PROCESS_BROWSER ||
        library_process_type == PROCESS_CHILD);

  if (!ShouldEnableReachedCodeProfiler())
    return;

  int interval_us = 0;
  base::TimeDelta sampling_interval = kSamplingInterval;
  if (base::StringToInt(
          base::CommandLine::ForCurrentProcess()->GetSwitchValueNative(
              switches::kReachedCodeSamplingIntervalUs),
          &interval_us) &&
      interval_us > 0) {
    sampling_interval = base::Microseconds(interval_us);
  }
  ReachedCodeProfiler::GetInstance()->Start(library_process_type,
                                            sampling_interval);
}

bool IsReachedCodeProfilerEnabled() {
  return ReachedCodeProfiler::GetInstance()->IsEnabled();
}

bool IsReachedCodeProfilerSupported() {
  return kConfigurationSupported;
}

}  // namespace android
}  // namespace base
