// Copyright (c) 2026 Anysphere, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.

#include "shell/renderer/oom_stack_trace.h"

#include <atomic>
#include <limits>
#include <string>
#include <string_view>

#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/no_destructor.h"
#include "base/threading/thread_local.h"
#include "electron/mas.h"
#include "gin/per_isolate_data.h"
#include "shell/common/v8_oom_diagnostics.h"
#include "third_party/abseil-cpp/absl/strings/str_format.h"
#include "v8/include/v8-exception.h"
#include "v8/include/v8-internal.h"
#include "v8/include/v8-isolate.h"
#include "v8/include/v8-local-handle.h"
#include "v8/include/v8-primitive.h"
#include "v8/include/v8-statistics.h"

namespace electron {

namespace {

// Forward-declare so OomState can reference it.
size_t NearHeapLimitCallback(void* data,
                             size_t current_heap_limit,
                             size_t initial_heap_limit);

struct OomState : public gin::PerIsolateData::DisposeObserver {
  raw_ptr<v8::Isolate> isolate;
  std::atomic<bool> is_in_oom{false};

  // gin::PerIsolateData::DisposeObserver:
  void OnBeforeDispose(v8::Isolate* disposing_isolate) override {
    if (!is_in_oom.load()) {
      disposing_isolate->RemoveNearHeapLimitCallback(NearHeapLimitCallback, 0);
    }
    isolate = nullptr;
  }
  void OnBeforeMicrotasksRunnerDispose(v8::Isolate* /*disposing*/) override {}
  void OnDisposed() override {}
};

base::ThreadLocalOwnedPointer<OomState>& GetOomState() {
  static base::NoDestructor<base::ThreadLocalOwnedPointer<OomState>> instance;
  return *instance;
}

std::string FormatStackTrace(v8::Isolate* isolate,
                             v8::Local<v8::StackTrace> stack) {
  std::string result;
  int frame_count = stack->GetFrameCount();
  for (int i = 0; i < frame_count; i++) {
    v8::Local<v8::StackFrame> frame = stack->GetFrame(isolate, i);

    v8::Local<v8::String> function_name = frame->GetFunctionName();
    v8::Local<v8::String> script_name = frame->GetScriptName();
    int line = frame->GetLineNumber();
    int col = frame->GetColumn();

    std::string func_str = "(anonymous)";
    if (!function_name.IsEmpty()) {
      v8::String::Utf8Value utf8(isolate, function_name);
      if (*utf8) {
        func_str = *utf8;
      }
    }

    std::string script_str = "<unknown>";
    if (!script_name.IsEmpty()) {
      v8::String::Utf8Value utf8(isolate, script_name);
      if (*utf8) {
        script_str = *utf8;
      }
    }

    absl::StrAppendFormat(&result, "    #%d %s (%s:%d:%d)\n", i, func_str,
                          script_str, line, col);
  }
  return result;
}

// Runs at the next V8 safe point after the heap limit was hit.
// At a safe point, all frames have deoptimization data available,
// so CurrentStackTrace won't FATAL on optimized frames.
void CaptureStackOnInterrupt(v8::Isolate* isolate, void* data) {
  if (!isolate->InContext()) {
    return;
  }

  v8::HeapStatistics stats;
  isolate->GetHeapStatistics(&stats);
  // CurrentStackTrace allocates StackTraceInfo + StackFrameInfo objects on the
  // V8 managed heap. If the 20 MB bump from NearHeapLimitCallback has been
  // partially consumed by the time this interrupt fires, these allocations
  // could trigger a secondary OOM. Check headroom before proceeding.
  // Note: use addition form to avoid unsigned underflow -- in OOM scenarios,
  // used_heap_size can transiently exceed heap_size_limit.
  constexpr size_t kMinHeadroom = 2 * 1024 * 1024;  // 2 MB
  if (stats.used_heap_size() + kMinHeadroom > stats.heap_size_limit()) {
    LOG(INFO) << "Skipping JS stack capture: insufficient heap headroom";
    return;
  }

  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::StackTrace> stack =
      v8::StackTrace::CurrentStackTrace(isolate, 10);
  if (stack.IsEmpty() || stack->GetFrameCount() == 0) {
    return;
  }

  std::string js_stack = FormatStackTrace(isolate, stack);
  if (!js_stack.empty()) {
    v8_oom::RecordJsStack(isolate, js_stack);
    LOG(ERROR) << "\n<--- JS stacktrace (captured at safe point) --->\n"
               << js_stack;
  }
}

// V8's pointer compression cage limits the heap to kPtrComprCageReservationSize
// (~4 GB). After this callback returns a new limit, V8 clamps it via
// Heap::AllocatorLimitOnMaxOldGenerationSize() to at most that cage size.
// If current_heap_limit is already near the ceiling the bump is effectively
// zero, the interrupt never gets enough headroom to fire, and we never capture
// a stack trace. When that happens we fall back to recording heap info only.
size_t NearHeapLimitCallback(void* data,
                             size_t current_heap_limit,
                             size_t initial_heap_limit) {
  auto* state = static_cast<OomState*>(data);
  v8::Isolate* isolate = state->isolate;

  if (state->is_in_oom.exchange(true)) {
    return current_heap_limit;
  }

  v8::HeapStatistics stats;
  isolate->GetHeapStatistics(&stats);
  std::string heap_info = absl::StrFormat("Heap: used=%.1fMB limit=%.1fMB",
                                          stats.used_heap_size() / 1048576.0,
                                          stats.heap_size_limit() / 1048576.0);
  LOG(ERROR) << "\n<--- Near heap limit --->\n" << heap_info;

  v8_oom::RecordJsStack(isolate, heap_info + " (stack pending)");
  v8_oom::RecordHeapDiagnostics(isolate);

  // Request an interrupt to capture the JS stack at the next safe point,
  // where optimized frames have deoptimization data available.
  // CurrentStackTrace is unsafe to call directly here because V8 may
  // FATAL on optimized frames missing deopt info during OOM.
  isolate->RequestInterrupt(CaptureStackOnInterrupt, state);

  // Remove ourselves and bump the limit to give V8 room to reach a safe
  // point where the interrupt can fire and capture the stack trace.
  isolate->RemoveNearHeapLimitCallback(NearHeapLimitCallback, 0);

  constexpr size_t kHeapBump = 20 * 1024 * 1024;
  size_t new_limit = current_heap_limit + kHeapBump;

#ifdef V8_COMPRESS_POINTERS
  constexpr size_t kCageLimit = v8::internal::kPtrComprCageReservationSize;
  static_assert(kCageLimit == size_t{1} << 32,
                "Cage size changed; review heap bump logic");
#else
  constexpr size_t kCageLimit = std::numeric_limits<size_t>::max();
#endif

  if (current_heap_limit >= kCageLimit - kHeapBump) {
    // The bump will be clamped by V8 to the cage ceiling, leaving no
    // headroom for the interrupt to fire. Record what we can now.
    v8_oom::RecordJsStack(isolate,
                          heap_info + " (at cage limit, stack unavailable)");
    LOG(INFO) << "Near V8 cage limit; stack trace capture may not succeed";
  }

  return new_limit;
}

}  // namespace

void RegisterOomStackTraceCallback(v8::Isolate* isolate) {
  auto& tls = GetOomState();
  if (tls.Get())
    return;
  auto state = std::make_unique<OomState>();
  state->isolate = isolate;
  OomState* raw = state.get();
  gin::PerIsolateData::From(isolate)->AddDisposeObserver(raw);
  tls.Set(std::move(state));
  isolate->AddNearHeapLimitCallback(NearHeapLimitCallback, raw);
}

void UnregisterOomStackTraceCallback(v8::Isolate* isolate) {
  auto& tls = GetOomState();
  OomState* state = tls.Get();
  if (!state || state->isolate != isolate)
    return;
  if (!state->is_in_oom.load()) {
    isolate->RemoveNearHeapLimitCallback(NearHeapLimitCallback, 0);
  }
  gin::PerIsolateData::From(isolate)->RemoveDisposeObserver(state);
  tls.Set(nullptr);
}

}  // namespace electron
