/* -*- 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 "CombinedStacks.h"

#include "jsapi.h"
#include "js/Array.h"               // JS::NewArrayObject
#include "js/PropertyAndElement.h"  // JS_DefineElement, JS_DefineProperty
#include "js/String.h"

namespace mozilla::Telemetry {

// The maximum number of chrome hangs stacks that we're keeping.
const size_t kMaxChromeStacksKept = 50;

CombinedStacks::CombinedStacks() : CombinedStacks(kMaxChromeStacksKept) {}

CombinedStacks::CombinedStacks(size_t aMaxStacksCount)
    : mNextIndex(0), mMaxStacksCount(aMaxStacksCount) {}

size_t CombinedStacks::GetMaxStacksCount() const { return mMaxStacksCount; }
size_t CombinedStacks::GetModuleCount() const { return mModules.size(); }

const Telemetry::ProcessedStack::Module& CombinedStacks::GetModule(
    unsigned aIndex) const {
  return mModules[aIndex];
}

void CombinedStacks::AddFrame(
    size_t aStackIndex, const ProcessedStack::Frame& aFrame,
    const std::function<const ProcessedStack::Module&(int)>& aModuleGetter) {
  uint16_t modIndex;
  if (aFrame.mModIndex == std::numeric_limits<uint16_t>::max()) {
    modIndex = aFrame.mModIndex;
  } else {
    const ProcessedStack::Module& module = aModuleGetter(aFrame.mModIndex);
    auto modIterator = std::find(mModules.begin(), mModules.end(), module);
    if (modIterator == mModules.end()) {
      mModules.push_back(module);
      modIndex = mModules.size() - 1;
    } else {
      modIndex = modIterator - mModules.begin();
    }
  }
  mStacks[aStackIndex].push_back(
      ProcessedStack::Frame{aFrame.mOffset, modIndex});
}

size_t CombinedStacks::AddStack(const Telemetry::ProcessedStack& aStack) {
  size_t index = mNextIndex;
  // Advance the indices of the circular queue holding the stacks.
  mNextIndex = (mNextIndex + 1) % mMaxStacksCount;
  // Grow the vector up to the maximum size, if needed.
  if (mStacks.size() < mMaxStacksCount) {
    mStacks.resize(mStacks.size() + 1);
  }

  // Clear the old stack before set.
  mStacks[index].clear();

  size_t stackSize = aStack.GetStackSize();
  for (size_t i = 0; i < stackSize; ++i) {
    // Need to specify a return type in the following lambda,
    // otherwise it's incorrectly deduced to be a non-reference type.
    AddFrame(index, aStack.GetFrame(i),
             [&aStack](int aIdx) -> const ProcessedStack::Module& {
               return aStack.GetModule(aIdx);
             });
  }
  return index;
}

void CombinedStacks::AddStacks(const CombinedStacks& aStacks) {
  mStacks.resize(
      std::min(mStacks.size() + aStacks.GetStackCount(), mMaxStacksCount));

  for (const auto& stack : aStacks.mStacks) {
    size_t index = mNextIndex;
    // Advance the indices of the circular queue holding the stacks.
    mNextIndex = (mNextIndex + 1) % mMaxStacksCount;

    // Clear the old stack before set.
    mStacks[index].clear();

    for (const auto& frame : stack) {
      // Need to specify a return type in the following lambda,
      // otherwise it's incorrectly deduced to be a non-reference type.
      AddFrame(index, frame,
               [&aStacks](int aIdx) -> const ProcessedStack::Module& {
                 return aStacks.mModules[aIdx];
               });
    }
  }
}

const CombinedStacks::Stack& CombinedStacks::GetStack(unsigned aIndex) const {
  return mStacks[aIndex];
}

size_t CombinedStacks::GetStackCount() const { return mStacks.size(); }

size_t CombinedStacks::SizeOfExcludingThis() const {
  // This is a crude approximation. We would like to do something like
  // aMallocSizeOf(&mModules[0]), but on linux aMallocSizeOf will call
  // malloc_usable_size which is only safe on the pointers returned by malloc.
  // While it works on current libstdc++, it is better to be safe and not assume
  // that &vec[0] points to one. We could use a custom allocator, but
  // it doesn't seem worth it.
  size_t n = 0;
  n += mModules.capacity() * sizeof(Telemetry::ProcessedStack::Module);
  n += mStacks.capacity() * sizeof(Stack);
  for (const auto& s : mStacks) {
    n += s.capacity() * sizeof(Telemetry::ProcessedStack::Frame);
  }
  return n;
}

void CombinedStacks::RemoveStack(unsigned aIndex) {
  MOZ_ASSERT(aIndex < mStacks.size());

  mStacks.erase(mStacks.begin() + aIndex);

  if (aIndex < mNextIndex) {
    if (mNextIndex == 0) {
      mNextIndex = mStacks.size();
    } else {
      mNextIndex--;
    }
  }

  if (mNextIndex > mStacks.size()) {
    mNextIndex = mStacks.size();
  }
}

void CombinedStacks::Swap(CombinedStacks& aOther) {
  mModules.swap(aOther.mModules);
  mStacks.swap(aOther.mStacks);

  size_t nextIndex = aOther.mNextIndex;
  aOther.mNextIndex = mNextIndex;
  mNextIndex = nextIndex;

  size_t maxStacksCount = aOther.mMaxStacksCount;
  aOther.mMaxStacksCount = mMaxStacksCount;
  mMaxStacksCount = maxStacksCount;
}

void CombinedStacks::Clear() {
  mNextIndex = 0;
  mStacks.clear();
  mModules.clear();
}

JSObject* CreateJSStackObject(JSContext* cx, const CombinedStacks& stacks) {
  JS::Rooted<JSObject*> ret(cx, JS_NewPlainObject(cx));
  if (!ret) {
    return nullptr;
  }

  JS::Rooted<JSObject*> moduleArray(cx, JS::NewArrayObject(cx, 0));
  if (!moduleArray) {
    return nullptr;
  }
  bool ok =
      JS_DefineProperty(cx, ret, "memoryMap", moduleArray, JSPROP_ENUMERATE);
  if (!ok) {
    return nullptr;
  }

  const size_t moduleCount = stacks.GetModuleCount();
  for (size_t moduleIndex = 0; moduleIndex < moduleCount; ++moduleIndex) {
    // Current module
    const Telemetry::ProcessedStack::Module& module =
        stacks.GetModule(moduleIndex);

    JS::Rooted<JSObject*> moduleInfoArray(cx, JS::NewArrayObject(cx, 0));
    if (!moduleInfoArray) {
      return nullptr;
    }
    if (!JS_DefineElement(cx, moduleArray, moduleIndex, moduleInfoArray,
                          JSPROP_ENUMERATE)) {
      return nullptr;
    }

    unsigned index = 0;

    // Module name
    JS::Rooted<JSString*> str(cx, JS_NewUCStringCopyZ(cx, module.mName.get()));
    if (!str || !JS_DefineElement(cx, moduleInfoArray, index++, str,
                                  JSPROP_ENUMERATE)) {
      return nullptr;
    }

    // Module breakpad identifier
    JS::Rooted<JSString*> id(cx,
                             JS_NewStringCopyZ(cx, module.mBreakpadId.get()));
    if (!id ||
        !JS_DefineElement(cx, moduleInfoArray, index, id, JSPROP_ENUMERATE)) {
      return nullptr;
    }
  }

  JS::Rooted<JSObject*> reportArray(cx, JS::NewArrayObject(cx, 0));
  if (!reportArray) {
    return nullptr;
  }
  ok = JS_DefineProperty(cx, ret, "stacks", reportArray, JSPROP_ENUMERATE);
  if (!ok) {
    return nullptr;
  }

  const size_t length = stacks.GetStackCount();
  for (size_t i = 0; i < length; ++i) {
    // Represent call stack PCs as (module index, offset) pairs.
    JS::Rooted<JSObject*> pcArray(cx, JS::NewArrayObject(cx, 0));
    if (!pcArray) {
      return nullptr;
    }

    if (!JS_DefineElement(cx, reportArray, i, pcArray, JSPROP_ENUMERATE)) {
      return nullptr;
    }

    const CombinedStacks::Stack& stack = stacks.GetStack(i);
    const uint32_t pcCount = stack.size();
    for (size_t pcIndex = 0; pcIndex < pcCount; ++pcIndex) {
      const Telemetry::ProcessedStack::Frame& frame = stack[pcIndex];
      JS::Rooted<JSObject*> framePair(cx, JS::NewArrayObject(cx, 0));
      if (!framePair) {
        return nullptr;
      }
      int modIndex = (std::numeric_limits<uint16_t>::max() == frame.mModIndex)
                         ? -1
                         : frame.mModIndex;
      if (!JS_DefineElement(cx, framePair, 0, modIndex, JSPROP_ENUMERATE)) {
        return nullptr;
      }
      if (!JS_DefineElement(cx, framePair, 1,
                            static_cast<double>(frame.mOffset),
                            JSPROP_ENUMERATE)) {
        return nullptr;
      }
      if (!JS_DefineElement(cx, pcArray, pcIndex, framePair,
                            JSPROP_ENUMERATE)) {
        return nullptr;
      }
    }
  }

  return ret;
}

}  // namespace mozilla::Telemetry
