/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: set ts=8 sts=2 et sw=2 tw=80:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "vm/GeckoProfiler-inl.h"

#include "mozilla/DebugOnly.h"
#include "mozilla/Sprintf.h"

#include "gc/GC.h"
#include "gc/PublicIterators.h"
#include "jit/BaselineJIT.h"
#include "jit/JitcodeMap.h"
#include "jit/JitRuntime.h"
#include "jit/JSJitFrameIter.h"
#include "jit/PerfSpewer.h"
#include "js/ProfilingStack.h"
#include "vm/FrameIter.h"  // js::OnlyJSJitFrameIter
#include "vm/JitActivation.h"
#include "vm/JSScript.h"

#include "gc/Marking-inl.h"
#include "jit/JSJitFrameIter-inl.h"

using namespace js;

GeckoProfilerThread::GeckoProfilerThread()
    : profilingStack_(nullptr), profilingStackIfEnabled_(nullptr) {}

GeckoProfilerRuntime::GeckoProfilerRuntime(JSRuntime* rt)
    : rt(rt),
      slowAssertions(false),
      enabled_(false),
      eventMarker_(nullptr),
      intervalMarker_(nullptr) {
  MOZ_ASSERT(rt != nullptr);
}

void GeckoProfilerThread::setProfilingStack(ProfilingStack* profilingStack,
                                            bool enabled) {
  profilingStack_ = profilingStack;
  profilingStackIfEnabled_ = enabled ? profilingStack : nullptr;
}

void GeckoProfilerRuntime::setEventMarker(void (*fn)(mozilla::MarkerCategory,
                                                     const char*,
                                                     const char*)) {
  eventMarker_ = fn;
}

void GeckoProfilerRuntime::setIntervalMarker(void (*fn)(
    mozilla::MarkerCategory, const char*, mozilla::TimeStamp, const char*)) {
  intervalMarker_ = fn;
}

// Get a pointer to the top-most profiling frame, given the exit frame pointer.
static jit::JitFrameLayout* GetTopProfilingJitFrame(jit::JitActivation* act) {
  // If there is no exit frame set, just return.
  if (!act->hasExitFP()) {
    return nullptr;
  }

  // Skip wasm frames that might be in the way.
  OnlyJSJitFrameIter iter(act);
  if (iter.done()) {
    return nullptr;
  }

  // Skip if the activation has no JS frames. This can happen if there's only a
  // TrampolineNative frame because these are skipped by the profiling frame
  // iterator.
  jit::JSJitProfilingFrameIterator jitIter(
      (jit::CommonFrameLayout*)iter.frame().fp());
  if (jitIter.done()) {
    return nullptr;
  }

  return jitIter.framePtr();
}

void GeckoProfilerRuntime::enable(bool enabled) {
  JSContext* cx = rt->mainContextFromAnyThread();
  MOZ_ASSERT(cx->geckoProfiler().infraInstalled());

  if (enabled_ == enabled) {
    return;
  }

  /*
   * Ensure all future generated code will be instrumented, or that all
   * currently instrumented code is discarded
   */
  ReleaseAllJITCode(rt->gcContext());

  // This function is called when the Gecko profiler makes a new Sampler
  // (and thus, a new circular buffer). Set all current entries in the
  // JitcodeGlobalTable as expired and reset the buffer range start.
  if (rt->hasJitRuntime() && rt->jitRuntime()->hasJitcodeGlobalTable()) {
    rt->jitRuntime()->getJitcodeGlobalTable()->setAllEntriesAsExpired();
  }
  rt->setProfilerSampleBufferRangeStart(0);

  // Ensure that lastProfilingFrame is null for the main thread.
  if (cx->jitActivation) {
    cx->jitActivation->setLastProfilingFrame(nullptr);
    cx->jitActivation->setLastProfilingCallSite(nullptr);
  }

  enabled_ = enabled;

  /* Toggle Gecko Profiler-related jumps on baseline jitcode.
   * The call to |ReleaseAllJITCode| above will release most baseline jitcode,
   * but not jitcode for scripts with active frames on the stack.  These scripts
   * need to have their profiler state toggled so they behave properly.
   */
  jit::ToggleBaselineProfiling(cx, enabled);

  // Update lastProfilingFrame to point to the top-most JS jit-frame currently
  // on stack.
  if (cx->jitActivation) {
    // Walk through all activations, and set their lastProfilingFrame
    // appropriately.
    if (enabled) {
      jit::JitActivation* jitActivation = cx->jitActivation;
      while (jitActivation) {
        auto* lastProfilingFrame = GetTopProfilingJitFrame(jitActivation);
        jitActivation->setLastProfilingFrame(lastProfilingFrame);
        jitActivation->setLastProfilingCallSite(nullptr);
        jitActivation = jitActivation->prevJitActivation();
      }
    } else {
      jit::JitActivation* jitActivation = cx->jitActivation;
      while (jitActivation) {
        jitActivation->setLastProfilingFrame(nullptr);
        jitActivation->setLastProfilingCallSite(nullptr);
        jitActivation = jitActivation->prevJitActivation();
      }
    }
  }

  // WebAssembly code does not need to be released, but profiling string
  // labels have to be generated so that they are available during async
  // profiling stack iteration.
  for (RealmsIter r(rt); !r.done(); r.next()) {
    r->wasm.ensureProfilingLabels(enabled);
  }

#ifdef JS_STRUCTURED_SPEW
  // Enable the structured spewer if the environment variable is set.
  if (enabled) {
    cx->spewer().enableSpewing();
  } else {
    cx->spewer().disableSpewing();
  }
#endif
}

/* Lookup the string for the function/script, creating one if necessary */
const char* GeckoProfilerRuntime::profileString(JSContext* cx,
                                                BaseScript* script) {
  ProfileStringMap::AddPtr s = strings().lookupForAdd(script);

  if (!s) {
    UniqueChars str = allocProfileString(cx, script);
    if (!str) {
      return nullptr;
    }
    MOZ_ASSERT(script->hasBytecode());
    if (!strings().add(s, script, std::move(str))) {
      ReportOutOfMemory(cx);
      return nullptr;
    }
  }

  return s->value().get();
}

void GeckoProfilerRuntime::onScriptFinalized(BaseScript* script) {
  /*
   * This function is called whenever a script is destroyed, regardless of
   * whether profiling has been turned on, so don't invoke a function on an
   * invalid hash set. Also, even if profiling was enabled but then turned
   * off, we still want to remove the string, so no check of enabled() is
   * done.
   */
  if (ProfileStringMap::Ptr entry = strings().lookup(script)) {
    strings().remove(entry);
  }
}

void GeckoProfilerRuntime::markEvent(const char* event, const char* details,
                                     JS::ProfilingCategoryPair jsPair) {
  MOZ_ASSERT(enabled());
  if (eventMarker_) {
    JS::AutoSuppressGCAnalysis nogc;
    mozilla::MarkerCategory category(
        static_cast<mozilla::baseprofiler::ProfilingCategoryPair>(jsPair));
    eventMarker_(category, event, details);
  }
}

void GeckoProfilerRuntime::markInterval(const char* event,
                                        mozilla::TimeStamp start,
                                        const char* details,
                                        JS::ProfilingCategoryPair jsPair) {
  MOZ_ASSERT(enabled());
  if (intervalMarker_) {
    JS::AutoSuppressGCAnalysis nogc;
    mozilla::MarkerCategory category(
        static_cast<mozilla::baseprofiler::ProfilingCategoryPair>(jsPair));
    intervalMarker_(category, event, start, details);
  }
}

bool GeckoProfilerThread::enter(JSContext* cx, JSScript* script) {
  const char* dynamicString =
      cx->runtime()->geckoProfiler().profileString(cx, script);
  if (dynamicString == nullptr) {
    return false;
  }

#ifdef DEBUG
  // In debug builds, assert the JS profiling stack frames already on the
  // stack have a non-null pc. Only look at the top frames to avoid quadratic
  // behavior.
  uint32_t sp = profilingStack_->stackPointer;
  if (sp > 0 && sp - 1 < profilingStack_->stackCapacity()) {
    size_t start = (sp > 4) ? sp - 4 : 0;
    for (size_t i = start; i < sp - 1; i++) {
      MOZ_ASSERT_IF(profilingStack_->frames[i].isJsFrame(),
                    profilingStack_->frames[i].pc());
    }
  }
#endif

  profilingStack_->pushJsFrame(
      "", dynamicString, script, script->code(),
      script->realm()->creationOptions().profilerRealmID());
  return true;
}

void GeckoProfilerThread::exit(JSContext* cx, JSScript* script) {
  profilingStack_->pop();

#ifdef DEBUG
  /* Sanity check to make sure push/pop balanced */
  uint32_t sp = profilingStack_->stackPointer;
  if (sp < profilingStack_->stackCapacity()) {
    JSRuntime* rt = script->runtimeFromMainThread();
    const char* dynamicString = rt->geckoProfiler().profileString(cx, script);
    /* Can't fail lookup because we should already be in the set */
    MOZ_ASSERT(dynamicString);

    // Bug 822041
    if (!profilingStack_->frames[sp].isJsFrame()) {
      fprintf(stderr, "--- ABOUT TO FAIL ASSERTION ---\n");
      fprintf(stderr, " frames=%p size=%u/%u\n", (void*)profilingStack_->frames,
              uint32_t(profilingStack_->stackPointer),
              profilingStack_->stackCapacity());
      for (int32_t i = sp; i >= 0; i--) {
        ProfilingStackFrame& frame = profilingStack_->frames[i];
        if (frame.isJsFrame()) {
          fprintf(stderr, "  [%d] JS %s\n", i, frame.dynamicString());
        } else {
          fprintf(stderr, "  [%d] Label %s\n", i, frame.dynamicString());
        }
      }
    }

    ProfilingStackFrame& frame = profilingStack_->frames[sp];
    MOZ_ASSERT(frame.isJsFrame());
    MOZ_ASSERT(frame.script() == script);
    MOZ_ASSERT(strcmp((const char*)frame.dynamicString(), dynamicString) == 0);
  }
#endif
}

/*
 * Serializes the script/function pair into a "descriptive string" which is
 * allowed to fail. This function cannot trigger a GC because it could finalize
 * some scripts, resize the hash table of profile strings, and invalidate the
 * AddPtr held while invoking allocProfileString.
 */
/* static */
UniqueChars GeckoProfilerRuntime::allocProfileString(JSContext* cx,
                                                     BaseScript* script) {
  // Note: this profiler string is regexp-matched by
  // profiler code. Most recently at
  // https://github.com/firefox-devtools/profiler/blob/245b1a400c5c368ccc13641d0335398bafa0e870/src/profile-logic/process-profile.js#L520-L525

  // If the script has a function, try calculating its name.
  JSAtom* name = nullptr;
  size_t nameLength = 0;
  JSFunction* func = script->function();
  if (func && func->fullDisplayAtom()) {
    name = func->fullDisplayAtom();
    nameLength = JS::GetDeflatedUTF8StringLength(name);
  }

  // Calculate filename length. We cap this to a reasonable limit to avoid
  // performance impact of strlen/alloc/memcpy.
  constexpr size_t MaxFilenameLength = 200;
  const char* filenameStr = script->filename() ? script->filename() : "(null)";
  size_t filenameLength = js_strnlen(filenameStr, MaxFilenameLength);

  // Calculate line + column length.
  bool hasLineAndColumn = false;
  size_t lineAndColumnLength = 0;
  char lineAndColumnStr[30];
  if (name || script->isFunction() || script->isForEval()) {
    lineAndColumnLength =
        SprintfLiteral(lineAndColumnStr, "%u:%u", script->lineno(),
                       script->column().oneOriginValue());
    hasLineAndColumn = true;
  }

  // Full profile string for scripts with functions is:
  //      FuncName (FileName:Lineno:Column)
  // Full profile string for scripts without functions is:
  //      FileName:Lineno:Column
  // Full profile string for scripts without functions and without lines is:
  //      FileName

  // Calculate full string length.
  size_t fullLength = 0;
  if (name) {
    MOZ_ASSERT(hasLineAndColumn);
    fullLength = nameLength + 2 + filenameLength + 1 + lineAndColumnLength + 1;
  } else if (hasLineAndColumn) {
    fullLength = filenameLength + 1 + lineAndColumnLength;
  } else {
    fullLength = filenameLength;
  }

  // Allocate string.
  UniqueChars str(cx->pod_malloc<char>(fullLength + 1));
  if (!str) {
    return nullptr;
  }

  size_t cur = 0;

  // Fill string with function name if needed.
  if (name) {
    mozilla::DebugOnly<size_t> written = JS::DeflateStringToUTF8Buffer(
        name, mozilla::Span(str.get() + cur, nameLength));
    MOZ_ASSERT(written == nameLength);
    cur += nameLength;
    str[cur++] = ' ';
    str[cur++] = '(';
  }

  // Fill string with filename chars.
  memcpy(str.get() + cur, filenameStr, filenameLength);
  cur += filenameLength;

  // Fill line + column chars.
  if (hasLineAndColumn) {
    str[cur++] = ':';
    memcpy(str.get() + cur, lineAndColumnStr, lineAndColumnLength);
    cur += lineAndColumnLength;
  }

  // Terminal ')' if necessary.
  if (name) {
    str[cur++] = ')';
  }

  MOZ_ASSERT(cur == fullLength);
  str[cur] = 0;

  return str;
}

void GeckoProfilerThread::trace(JSTracer* trc) {
  if (profilingStack_) {
    size_t size = profilingStack_->stackSize();
    for (size_t i = 0; i < size; i++) {
      profilingStack_->frames[i].trace(trc);
    }
  }
}

void GeckoProfilerRuntime::fixupStringsMapAfterMovingGC() {
  for (ProfileStringMap::Enum e(strings()); !e.empty(); e.popFront()) {
    BaseScript* script = e.front().key();
    if (IsForwarded(script)) {
      script = Forwarded(script);
      e.rekeyFront(script);
    }
  }
}

#ifdef JSGC_HASH_TABLE_CHECKS
void GeckoProfilerRuntime::checkStringsMapAfterMovingGC() {
  CheckTableAfterMovingGC(strings(), [](const auto& entry) {
    BaseScript* script = entry.key();
    CheckGCThingAfterMovingGC(script);
    return script;
  });
}
#endif

void ProfilingStackFrame::trace(JSTracer* trc) {
  if (isJsFrame()) {
    JSScript* s = rawScript();
    TraceNullableRoot(trc, &s, "ProfilingStackFrame script");
    spOrScript = s;
  }
}

GeckoProfilerBaselineOSRMarker::GeckoProfilerBaselineOSRMarker(
    JSContext* cx, bool hasProfilerFrame)
    : profiler(&cx->geckoProfiler()) {
  if (!hasProfilerFrame || !cx->runtime()->geckoProfiler().enabled()) {
    profiler = nullptr;
    return;
  }

  uint32_t sp = profiler->profilingStack_->stackPointer;
  if (sp >= profiler->profilingStack_->stackCapacity()) {
    profiler = nullptr;
    return;
  }

  spBefore_ = sp;
  if (sp == 0) {
    return;
  }

  ProfilingStackFrame& frame = profiler->profilingStack_->frames[sp - 1];
  MOZ_ASSERT(!frame.isOSRFrame());
  frame.setIsOSRFrame(true);
}

GeckoProfilerBaselineOSRMarker::~GeckoProfilerBaselineOSRMarker() {
  if (profiler == nullptr) {
    return;
  }

  uint32_t sp = profiler->stackPointer();
  MOZ_ASSERT(spBefore_ == sp);
  if (sp == 0) {
    return;
  }

  ProfilingStackFrame& frame = profiler->stack()[sp - 1];
  MOZ_ASSERT(frame.isOSRFrame());
  frame.setIsOSRFrame(false);
}

JS_PUBLIC_API JSScript* ProfilingStackFrame::script() const {
  MOZ_ASSERT(isJsFrame());
  auto* script = reinterpret_cast<JSScript*>(spOrScript.operator void*());
  if (!script) {
    return nullptr;
  }

  // If profiling is supressed then we can't trust the script pointers to be
  // valid as they could be in the process of being moved by a compacting GC
  // (although it's still OK to get the runtime from them).
  JSContext* cx = script->runtimeFromAnyThread()->mainContextFromAnyThread();
  if (!cx->isProfilerSamplingEnabled()) {
    return nullptr;
  }

  MOZ_ASSERT(!IsForwarded(script));
  return script;
}

JS_PUBLIC_API JSFunction* ProfilingStackFrame::function() const {
  JSScript* script = this->script();
  return script ? script->function() : nullptr;
}

JS_PUBLIC_API jsbytecode* ProfilingStackFrame::pc() const {
  MOZ_ASSERT(isJsFrame());
  if (pcOffsetIfJS_ == NullPCOffset) {
    return nullptr;
  }

  JSScript* script = this->script();
  return script ? script->offsetToPC(pcOffsetIfJS_) : nullptr;
}

/* static */
int32_t ProfilingStackFrame::pcToOffset(JSScript* aScript, jsbytecode* aPc) {
  return aPc ? aScript->pcToOffset(aPc) : NullPCOffset;
}

void ProfilingStackFrame::setPC(jsbytecode* pc) {
  MOZ_ASSERT(isJsFrame());
  JSScript* script = this->script();
  MOZ_ASSERT(
      script);  // This should not be called while profiling is suppressed.
  pcOffsetIfJS_ = pcToOffset(script, pc);
}

JS_PUBLIC_API void js::SetContextProfilingStack(
    JSContext* cx, ProfilingStack* profilingStack) {
  cx->geckoProfiler().setProfilingStack(
      profilingStack, cx->runtime()->geckoProfiler().enabled());
}

JS_PUBLIC_API void js::EnableContextProfilingStack(JSContext* cx,
                                                   bool enabled) {
  cx->geckoProfiler().enable(enabled);
  cx->runtime()->geckoProfiler().enable(enabled);
}

JS_PUBLIC_API void js::RegisterContextProfilingEventMarker(
    JSContext* cx,
    void (*mark)(mozilla::MarkerCategory, const char*, const char*),
    void (*interval)(mozilla::MarkerCategory, const char*, mozilla::TimeStamp,
                     const char*)) {
  MOZ_ASSERT(cx->runtime()->geckoProfiler().enabled());
  cx->runtime()->geckoProfiler().setEventMarker(mark);
  cx->runtime()->geckoProfiler().setIntervalMarker(interval);
}

AutoSuppressProfilerSampling::AutoSuppressProfilerSampling(JSContext* cx)
    : cx_(cx), previouslyEnabled_(cx->isProfilerSamplingEnabled()) {
  if (previouslyEnabled_) {
    cx_->disableProfilerSampling();
  }
}

AutoSuppressProfilerSampling::~AutoSuppressProfilerSampling() {
  if (previouslyEnabled_) {
    cx_->enableProfilerSampling();
  }
}

namespace JS {

// clang-format off

// ProfilingSubcategory_X:
// One enum for each category X, listing that category's subcategories. This
// allows the sProfilingCategoryInfo macro construction below to look up a
// per-category index for a subcategory.
#define SUBCATEGORY_ENUMS_BEGIN_CATEGORY(name, labelAsString, color) \
  enum class ProfilingSubcategory_##name : uint32_t {
#define SUBCATEGORY_ENUMS_SUBCATEGORY(category, name, labelAsString) \
    name,
#define SUBCATEGORY_ENUMS_END_CATEGORY \
  };
MOZ_PROFILING_CATEGORY_LIST(SUBCATEGORY_ENUMS_BEGIN_CATEGORY,
                            SUBCATEGORY_ENUMS_SUBCATEGORY,
                            SUBCATEGORY_ENUMS_END_CATEGORY)
#undef SUBCATEGORY_ENUMS_BEGIN_CATEGORY
#undef SUBCATEGORY_ENUMS_SUBCATEGORY
#undef SUBCATEGORY_ENUMS_END_CATEGORY

// sProfilingCategoryPairInfo:
// A list of ProfilingCategoryPairInfos with the same order as
// ProfilingCategoryPair, which can be used to map a ProfilingCategoryPair to
// its information.
#define CATEGORY_INFO_BEGIN_CATEGORY(name, labelAsString, color)
#define CATEGORY_INFO_SUBCATEGORY(category, name, labelAsString) \
  {ProfilingCategory::category,                                  \
   uint32_t(ProfilingSubcategory_##category::name), labelAsString},
#define CATEGORY_INFO_END_CATEGORY
const ProfilingCategoryPairInfo sProfilingCategoryPairInfo[] = {
  MOZ_PROFILING_CATEGORY_LIST(CATEGORY_INFO_BEGIN_CATEGORY,
                              CATEGORY_INFO_SUBCATEGORY,
                              CATEGORY_INFO_END_CATEGORY)
};
#undef CATEGORY_INFO_BEGIN_CATEGORY
#undef CATEGORY_INFO_SUBCATEGORY
#undef CATEGORY_INFO_END_CATEGORY

// clang-format on

JS_PUBLIC_API const ProfilingCategoryPairInfo& GetProfilingCategoryPairInfo(
    ProfilingCategoryPair aCategoryPair) {
  static_assert(
      std::size(sProfilingCategoryPairInfo) ==
          uint32_t(ProfilingCategoryPair::COUNT),
      "sProfilingCategoryPairInfo and ProfilingCategory need to have the "
      "same order and the same length");

  uint32_t categoryPairIndex = uint32_t(aCategoryPair);
  MOZ_RELEASE_ASSERT(categoryPairIndex <=
                     uint32_t(ProfilingCategoryPair::LAST));
  return sProfilingCategoryPairInfo[categoryPairIndex];
}

}  // namespace JS
