/* 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/. */

#ifndef CodeAddressService_h_
#define CodeAddressService_h_

#include <cstddef>
#include <cstdint>
#include <cstring>

#include "mozilla/AllocPolicy.h"
#include "mozilla/Assertions.h"
#include "mozilla/HashFunctions.h"
#include "mozilla/HashTable.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/StackWalk.h"

namespace mozilla {

namespace detail {

template <class AllocPolicy>
class CodeAddressServiceAllocPolicy : public AllocPolicy {
 public:
  char* strdup_(const char* aStr) {
    char* s = AllocPolicy::template pod_malloc<char>(strlen(aStr) + 1);
    if (!s) {
      MOZ_CRASH("CodeAddressService OOM");
    }
    strcpy(s, aStr);
    return s;
  }
};

// Default implementation of DescribeCodeAddressLock.
struct DefaultDescribeCodeAddressLock {
  static void Unlock() {}
  static void Lock() {}
  // Because CodeAddressService asserts that IsLocked() is true, returning true
  // here is a sensible default when there is no relevant lock.
  static bool IsLocked() { return true; }
};

}  // namespace detail

// This class is used to print details about code locations.
//
// |AllocPolicy_| must adhere to the description in mfbt/AllocPolicy.h.
//
// |DescribeCodeAddressLock| is needed when the callers may be holding a lock
// used by MozDescribeCodeAddress.  |DescribeCodeAddressLock| must implement
// static methods IsLocked(), Unlock() and Lock().
template <class AllocPolicy_ = MallocAllocPolicy,
          class DescribeCodeAddressLock =
              detail::DefaultDescribeCodeAddressLock>
class CodeAddressService
    : private detail::CodeAddressServiceAllocPolicy<AllocPolicy_> {
 protected:
  // GetLocation() is the key function in this class.  It's basically a wrapper
  // around MozDescribeCodeAddress.
  //
  // However, MozDescribeCodeAddress is very slow on some platforms, and we
  // have lots of repeated (i.e. same PC) calls to it.  So we do some caching
  // of results.  Each cached result includes two strings (|mFunction| and
  // |mLibrary|), so we also optimize them for space in the following ways.
  //
  // - The number of distinct library names is small, e.g. a few dozen.  There
  //   is lots of repetition, especially of libxul.  So we intern them in their
  //   own table, which saves space over duplicating them for each cache entry.
  //
  // - The number of distinct function names is much higher, so we duplicate
  //   them in each cache entry.  That's more space-efficient than interning
  //   because entries containing single-occurrence function names are quickly
  //   overwritten, and their copies released.  In addition, empty function
  //   names are common, so we use nullptr to represent them compactly.

  using AllocPolicy = detail::CodeAddressServiceAllocPolicy<AllocPolicy_>;
  using StringHashSet = HashSet<const char*, CStringHasher, AllocPolicy>;

  StringHashSet mLibraryStrings;

  struct Entry : private AllocPolicy {
    const void* mPc;
    char* mFunction;       // owned by the Entry;  may be null
    const char* mLibrary;  // owned by mLibraryStrings;  never null
                           //   in a non-empty entry is in use
    ptrdiff_t mLOffset;
    char* mFileName;  // owned by the Entry; may be null
    uint32_t mLineNo : 31;
    uint32_t mInUse : 1;  // is the entry used?

    Entry()
        : mPc(0),
          mFunction(nullptr),
          mLibrary(nullptr),
          mLOffset(0),
          mFileName(nullptr),
          mLineNo(0),
          mInUse(0) {}

    ~Entry() {
      // We don't free mLibrary because it's externally owned.
      AllocPolicy::free_(mFunction);
      AllocPolicy::free_(mFileName);
    }

    void Replace(const void* aPc, const char* aFunction, const char* aLibrary,
                 ptrdiff_t aLOffset, const char* aFileName,
                 unsigned long aLineNo) {
      mPc = aPc;

      // Convert "" to nullptr.  Otherwise, make a copy of the name.
      AllocPolicy::free_(mFunction);
      mFunction = !aFunction[0] ? nullptr : AllocPolicy::strdup_(aFunction);
      AllocPolicy::free_(mFileName);
      mFileName = !aFileName[0] ? nullptr : AllocPolicy::strdup_(aFileName);

      mLibrary = aLibrary;
      mLOffset = aLOffset;
      mLineNo = aLineNo;

      mInUse = 1;
    }

    size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
      // Don't measure mLibrary because it's externally owned.
      size_t n = 0;
      n += aMallocSizeOf(mFunction);
      n += aMallocSizeOf(mFileName);
      return n;
    }
  };

  const char* InternLibraryString(const char* aString) {
    auto p = mLibraryStrings.lookupForAdd(aString);
    if (p) {
      return *p;
    }

    const char* newString = AllocPolicy::strdup_(aString);
    if (!mLibraryStrings.add(p, newString)) {
      MOZ_CRASH("CodeAddressService OOM");
    }
    return newString;
  }

  Entry& GetEntry(const void* aPc) {
    MOZ_ASSERT(DescribeCodeAddressLock::IsLocked());

    uint32_t index = HashGeneric(aPc) & kMask;
    MOZ_ASSERT(index < kNumEntries);
    Entry& entry = mEntries[index];

    if (!entry.mInUse || entry.mPc != aPc) {
      mNumCacheMisses++;

      // MozDescribeCodeAddress can (on Linux) acquire a lock inside
      // the shared library loader.  Another thread might call malloc
      // while holding that lock (when loading a shared library).  So
      // we have to exit the lock around this call.  For details, see
      // https://bugzilla.mozilla.org/show_bug.cgi?id=363334#c3
      MozCodeAddressDetails details;
      {
        DescribeCodeAddressLock::Unlock();
        (void)MozDescribeCodeAddress(const_cast<void*>(aPc), &details);
        DescribeCodeAddressLock::Lock();
      }

      const char* library = InternLibraryString(details.library);
      entry.Replace(aPc, details.function, library, details.loffset,
                    details.filename, details.lineno);

    } else {
      mNumCacheHits++;
    }

    MOZ_ASSERT(entry.mPc == aPc);

    return entry;
  }

  // A direct-mapped cache.  When doing dmd::Analyze() just after starting
  // desktop Firefox (which is similar to analyzing after a longer-running
  // session, thanks to the limit on how many records we print), a cache with
  // 2^24 entries (which approximates an infinite-entry cache) has a ~91% hit
  // rate.  A cache with 2^12 entries has a ~83% hit rate, and takes up ~85 KiB
  // (on 32-bit platforms) or ~150 KiB (on 64-bit platforms).
  static const size_t kNumEntries = 1 << 12;
  static const size_t kMask = kNumEntries - 1;
  Entry mEntries[kNumEntries];

  size_t mNumCacheHits;
  size_t mNumCacheMisses;

 public:
  CodeAddressService()
      : mLibraryStrings(64), mEntries(), mNumCacheHits(0), mNumCacheMisses(0) {}

  ~CodeAddressService() {
    for (auto iter = mLibraryStrings.iter(); !iter.done(); iter.next()) {
      AllocPolicy::free_(const_cast<char*>(iter.get()));
    }
  }

  // Returns the minimum number of characters necessary to format the frame
  // information, without the terminating null. The buffer will be truncated
  // if the returned value is greater than aBufLen-1.
  int GetLocation(uint32_t aFrameNumber, const void* aPc, char* aBuf,
                  size_t aBufLen) {
    Entry& entry = GetEntry(aPc);
    return MozFormatCodeAddress(aBuf, aBufLen, aFrameNumber, entry.mPc,
                                entry.mFunction, entry.mLibrary, entry.mLOffset,
                                entry.mFileName, entry.mLineNo);
  }

  size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
    size_t n = aMallocSizeOf(this);
    for (uint32_t i = 0; i < kNumEntries; i++) {
      n += mEntries[i].SizeOfExcludingThis(aMallocSizeOf);
    }

    n += mLibraryStrings.shallowSizeOfExcludingThis(aMallocSizeOf);
    for (auto iter = mLibraryStrings.iter(); !iter.done(); iter.next()) {
      n += aMallocSizeOf(iter.get());
    }

    return n;
  }

  size_t CacheCapacity() const { return kNumEntries; }

  size_t CacheCount() const {
    size_t n = 0;
    for (size_t i = 0; i < kNumEntries; i++) {
      if (mEntries[i].mInUse) {
        n++;
      }
    }
    return n;
  }

  size_t NumCacheHits() const { return mNumCacheHits; }
  size_t NumCacheMisses() const { return mNumCacheMisses; }
};

}  // namespace mozilla

#endif  // CodeAddressService_h_
