Commit c4dd1705 authored by Doug Thayer's avatar Doug Thayer
Browse files

Bug 1651941 - Use shared buffer for Content Process StartupCache keys r=froydnj

Keys can take up a nontrivial chunk of memory. This change ensures that the
memory cost is not per-process. In order to do this we had to switch from
nsCStrings to MaybeOwnedCharPtrs pointing to null-terminated char sequences.
This meant fiddling around with the code that loads / stores to disk and
making it a little bit uglier, rather than adding another method to the
IOBuffers classes to support our weird maybe-shared string type.

Differential Revision: https://phabricator.services.mozilla.com/D83398
parent 7e6bdb77
Loading
Loading
Loading
Loading
+10 −10
Original line number Diff line number Diff line
@@ -121,11 +121,6 @@ class PreallocatedOutputBuffer {

  size_t remainingCapacity() { return data.length() - cursor_; }

  size_t cursor() const { return cursor_; }

  const uint8_t* Get() const { return data.begin().get(); }

 private:
  bool checkCapacity(size_t size) {
    if (size > remainingCapacity()) {
      error_ = true;
@@ -133,6 +128,11 @@ class PreallocatedOutputBuffer {
    return !error_;
  }

  size_t cursor() const { return cursor_; }

  const uint8_t* Get() const { return data.begin().get(); }

 private:
  bool error_ = false;

 public:
@@ -199,11 +199,6 @@ class InputBuffer {

  size_t remainingCapacity() { return data.length() - cursor_; }

  size_t cursor() const { return cursor_; }

  const uint8_t* Get() const { return data.begin().get(); }

 private:
  bool checkCapacity(size_t size) {
    if (size > remainingCapacity()) {
      error_ = true;
@@ -211,6 +206,11 @@ class InputBuffer {
    return !error_;
  }

  size_t cursor() const { return cursor_; }

  const uint8_t* Get() const { return data.begin().get(); }

 private:
  bool error_ = false;

 public:
+87 −24
Original line number Diff line number Diff line
@@ -528,12 +528,26 @@ Result<Ok, nsresult> StartupCache::LoadEntriesOffDisk() {
      uint32_t compressedSize = 0;
      uint32_t uncompressedSize = 0;
      uint8_t sharedToChild = 0;
      nsCString key;
      uint16_t keyLenWithNullTerm;
      buf.codeUint32(offset);
      buf.codeUint32(compressedSize);
      buf.codeUint32(uncompressedSize);
      buf.codeUint8(sharedToChild);
      buf.codeString(key);
      buf.codeUint16(keyLenWithNullTerm);
      if (!buf.checkCapacity(keyLenWithNullTerm)) {
        MOZ_ASSERT(false, "StartupCache file is corrupt.");
        return Err(NS_ERROR_UNEXPECTED);
      }

      const char* keyBuf =
          reinterpret_cast<const char*>(buf.read(keyLenWithNullTerm));
      if (keyBuf[keyLenWithNullTerm - 1] != 0) {
        MOZ_ASSERT(false, "StartupCache file is corrupt.");
        return Err(NS_ERROR_UNEXPECTED);
      }

      MaybeOwnedCharPtr key(MakeUnique<char[]>(keyLenWithNullTerm));
      memcpy(key.get(), keyBuf, keyLenWithNullTerm);

      if (offset + compressedSize > end - data) {
        if (XRE_IsParentProcess()) {
@@ -550,9 +564,10 @@ Result<Ok, nsresult> StartupCache::LoadEntriesOffDisk() {
      EnumSet<StartupCacheEntryFlags> entryFlags;
      if (sharedToChild) {
        // We encode the key as a u16 indicating the length, followed by the
        // contents of the string. We encode the uncompressed data similarly,
        // but with a u32 indicating the length rather than a u16.
        size_t sharedEntrySize = sizeof(uint16_t) + key.Length() +
        // contents of the string, plus a null terminator (to simplify things
        // by using existing c-string hashers). We encode the uncompressed data
        // as simply a u32 length, followed by the contents of the string.
        size_t sharedEntrySize = sizeof(uint16_t) + keyLenWithNullTerm +
                                 sizeof(uint32_t) + uncompressedSize;
        numSharedEntries++;
        mSharedDataSize += sharedEntrySize;
@@ -570,13 +585,13 @@ Result<Ok, nsresult> StartupCache::LoadEntriesOffDisk() {
      // We could use mTable.putNew if we knew the file we're loading weren't
      // corrupt. However, we don't know that, so check if the key already
      // exists. If it does, we know the file must be corrupt.
      decltype(mTable)::AddPtr p = mTable.lookupForAdd(key);
      decltype(mTable)::AddPtr p = mTable.lookupForAdd(key.get());
      if (p) {
        MOZ_ASSERT(false, "StartupCache file is corrupt.");
        return Err(NS_ERROR_UNEXPECTED);
      }

      if (!mTable.add(p, key,
      if (!mTable.add(p, std::move(key),
                      StartupCacheEntry(offset, compressedSize,
                                        uncompressedSize, entryFlags))) {
        MOZ_ASSERT(false, "StartupCache file is corrupt.");
@@ -619,7 +634,21 @@ Result<Ok, nsresult> StartupCache::LoadEntriesOffDisk() {
          continue;
        }

        sharedBuf.codeString(key);
        uint16_t keyLenWithNullTerm =
            strnlen(key.get(), kStartupCacheKeyLengthCap) + 1;
        if (keyLenWithNullTerm >= kStartupCacheKeyLengthCap) {
          MOZ_ASSERT(false, "StartupCache key too large or not terminated.");
          return Err(NS_ERROR_UNEXPECTED);
        }
        sharedBuf.codeUint16(keyLenWithNullTerm);

        if (!sharedBuf.checkCapacity(keyLenWithNullTerm)) {
          MOZ_ASSERT(false, "Exceeded shared StartupCache buffer size.");
          return Err(NS_ERROR_UNEXPECTED);
        }
        uint8_t* keyBuffer = sharedBuf.write(keyLenWithNullTerm);
        memcpy(keyBuffer, key.get(), keyLenWithNullTerm);

        sharedBuf.codeUint32(value.mUncompressedSize);

        if (value.mUncompressedSize > sharedBuf.remainingCapacity()) {
@@ -692,12 +721,24 @@ Result<Ok, nsresult> StartupCache::LoadEntriesFromSharedMemory() {
      return Err(NS_ERROR_UNEXPECTED);
    }

    nsCString key;
    uint16_t keyLenWithNullTerm = 0;
    buf.codeUint16(keyLenWithNullTerm);
    // NOTE: this will fail if the above codeUint16 call failed.
    if (!buf.checkCapacity(keyLenWithNullTerm)) {
      MOZ_ASSERT(false, "Corrupt StartupCache shared buffer.");
      return Err(NS_ERROR_UNEXPECTED);
    }
    MaybeOwnedCharPtr key(const_cast<char*>(
        reinterpret_cast<const char*>(buf.read(keyLenWithNullTerm))));
    if (key.get()[keyLenWithNullTerm - 1] != 0) {
      MOZ_ASSERT(false, "StartupCache key was not null-terminated");
      return Err(NS_ERROR_UNEXPECTED);
    }

    uint32_t uncompressedSize = 0;
    buf.codeString(key);
    buf.codeUint32(uncompressedSize);

    if (uncompressedSize > buf.remainingCapacity()) {
    if (!buf.checkCapacity(uncompressedSize)) {
      MOZ_ASSERT(false,
                 "StartupCache entry larger than remaining buffer size.");
      return Err(NS_ERROR_UNEXPECTED);
@@ -712,7 +753,7 @@ Result<Ok, nsresult> StartupCache::LoadEntriesFromSharedMemory() {
    // time guarantees.
    entry.mData = MaybeOwnedCharPtr(const_cast<char*>(data));

    if (!mTable.putNew(key, std::move(entry))) {
    if (!mTable.putNew(std::move(key), std::move(entry))) {
      return Err(NS_ERROR_UNEXPECTED);
    }
  }
@@ -778,7 +819,11 @@ bool StartupCache::HasEntry(const char* id) {
  AUTO_PROFILER_LABEL("StartupCache::HasEntry", OTHER);

  MutexAutoLock lock(mLock);
  return mTable.has(nsDependentCString(id));

  MOZ_ASSERT(
      strnlen(id, kStartupCacheKeyLengthCap) + 1 < kStartupCacheKeyLengthCap,
      "StartupCache key too large or not terminated.");
  return mTable.has(id);
}

nsresult StartupCache::GetBuffer(const char* id, const char** outbuf,
@@ -793,7 +838,10 @@ nsresult StartupCache::GetBuffer(const char* id, const char** outbuf,
    return NS_ERROR_NOT_AVAILABLE;
  }

  decltype(mTable)::Ptr p = mTable.lookup(nsDependentCString(id));
  MOZ_ASSERT(
      strnlen(id, kStartupCacheKeyLengthCap) + 1 < kStartupCacheKeyLengthCap,
      "StartupCache key too large or not terminated.");
  decltype(mTable)::Ptr p = mTable.lookup(id);
  if (!p) {
    return NS_ERROR_NOT_AVAILABLE;
  }
@@ -865,11 +913,15 @@ nsresult StartupCache::PutBuffer(const char* id, UniquePtr<char[]>&& inbuf,
    return NS_ERROR_NOT_AVAILABLE;
  }

  bool exists = mTable.has(nsDependentCString(id));
  uint32_t keyLenWithNullTerm = strnlen(id, kStartupCacheKeyLengthCap) + 1;
  if (keyLenWithNullTerm >= kStartupCacheKeyLengthCap) {
    MOZ_ASSERT(false, "StartupCache key too large or not terminated.");
    return NS_ERROR_UNEXPECTED;
  }
  decltype(mTable)::AddPtr p = mTable.lookupForAdd(id);

  if (exists) {
  if (p) {
    if (isFromChildProcess) {
      decltype(mTable)::Ptr p = mTable.lookup(nsDependentCString(id));
      auto& value = p->value();
      value.mFlags += StartupCacheEntryFlags::RequestedByChild;
      if (value.mRequestedOrder == kStartupCacheEntryNotRequested) {
@@ -898,11 +950,13 @@ nsresult StartupCache::PutBuffer(const char* id, UniquePtr<char[]>&& inbuf,
    flags += StartupCacheEntryFlags::RequestedByChild;
  }

  // putNew returns false on alloc failure - in the very unlikely event we hit
  UniquePtr<char[]> key = MakeUnique<char[]>(keyLenWithNullTerm);
  memcpy(key.get(), id, keyLenWithNullTerm);
  // add returns false on alloc failure - in the very unlikely event we hit
  // that and aren't going to crash elsewhere, there's no reason we need to
  // crash here.
  if (mTable.putNew(
          nsCString(id),
  if (mTable.add(
          p, MaybeOwnedCharPtr(std::move(key)),
          StartupCacheEntry(std::move(inbuf), len, ++mRequestedCount, flags))) {
    return ResetStartupWriteTimerImpl();
  }
@@ -923,7 +977,7 @@ size_t StartupCache::HeapSizeOfIncludingThis(
    if (iter.get().value().mData) {
      n += aMallocSizeOf(iter.get().value().mData.get());
    }
    n += iter.get().key().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
    n += iter.get().key().SizeOfExcludingThis(aMallocSizeOf);
  }

  return n;
@@ -961,7 +1015,7 @@ Result<Ok, nsresult> StartupCache::WriteToDisk() {
    MOZ_TRY(tmpFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
                                      0644, &fd.rwget()));

    nsTArray<std::pair<const nsCString*, StartupCacheEntry*>> entries;
    nsTArray<std::pair<const MaybeOwnedCharPtr*, StartupCacheEntry*>> entries;

    for (auto iter = mTable.iter(); !iter.done(); iter.next()) {
      entries.AppendElement(
@@ -975,7 +1029,7 @@ Result<Ok, nsresult> StartupCache::WriteToDisk() {
    entries.Sort(StartupCacheEntry::Comparator());
    loader::OutputBuffer buf;
    for (auto& e : entries) {
      const auto& key = e.first;
      const auto* key = e.first;
      auto* value = e.second;
      auto uncompressedSize = value->mUncompressedSize;
      uint8_t sharedToChild =
@@ -988,7 +1042,16 @@ Result<Ok, nsresult> StartupCache::WriteToDisk() {
      buf.codeUint32(0);
      buf.codeUint32(uncompressedSize);
      buf.codeUint8(sharedToChild);
      buf.codeString(*key);
      uint32_t keyLenWithNullTerm =
          strnlen(key->get(), kStartupCacheKeyLengthCap) + 1;
      if (keyLenWithNullTerm >= kStartupCacheKeyLengthCap) {
        MOZ_ASSERT(false, "StartupCache key too large or not terminated.");
        return Err(NS_ERROR_UNEXPECTED);
      }

      buf.codeUint16(keyLenWithNullTerm);
      char* keyBuf = reinterpret_cast<char*>(buf.write(keyLenWithNullTerm));
      memcpy(keyBuf, key->get(), keyLenWithNullTerm);
    }

    uint8_t headerSize[4];
+31 −6
Original line number Diff line number Diff line
@@ -11,7 +11,6 @@
#include "nsClassHashtable.h"
#include "nsComponentManagerUtils.h"
#include "nsTArray.h"
#include "nsTStringHasher.h"  // mozilla::DefaultHasher<nsCString>
#include "nsZipArchive.h"
#include "nsITimer.h"
#include "nsIMemoryReporter.h"
@@ -105,6 +104,11 @@ static const int kStartupCacheFd = 11;
static const int kStartupCacheEntryNotRequested = INT_MAX;
static const int kStartupcacheEntryNotInSharedData = -1;

// Keys must be of length `kStartupCacheKeyLengthCap - 1` or shorter, which
// will bring them to `kStartupCacheKeyLengthCap` or shorter with a null
// terminator.
static const int kStartupCacheKeyLengthCap = 1024;

// StartupCache entries can be backed by a buffer which they allocate as
// soon as they are requested, into which they decompress the contents out
// of the memory mapped file, *or* they can be backed by a contiguous buffer
@@ -131,8 +135,8 @@ class MaybeOwnedCharPtr {
    }
  }

  MaybeOwnedCharPtr(const MaybeOwnedCharPtr& other);
  MaybeOwnedCharPtr& operator=(const MaybeOwnedCharPtr& other);
  // MaybeOwnedCharPtr(const MaybeOwnedCharPtr& other);
  // MaybeOwnedCharPtr& operator=(const MaybeOwnedCharPtr& other);

  MaybeOwnedCharPtr(MaybeOwnedCharPtr&& other)
      : mPtr(std::exchange(other.mPtr, nullptr)),
@@ -150,9 +154,11 @@ class MaybeOwnedCharPtr {
    return *this;
  }

  operator char*() const { return mPtr; }

  explicit operator bool() const { return !!mPtr; }

  char* get() { return mPtr; }
  char* get() const { return mPtr; }

  explicit MaybeOwnedCharPtr(char* aBytes) : mPtr(aBytes), mOwned(false) {}

@@ -161,6 +167,24 @@ class MaybeOwnedCharPtr {

  explicit MaybeOwnedCharPtr(size_t size)
      : mPtr(new char[size]), mOwned(true) {}

  size_t SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const {
    if (!mOwned) {
      return 0;
    }
    return mallocSizeOf(mPtr);
  }
};

struct StartupCacheKeyHasher {
  using Key = MaybeOwnedCharPtr;
  using Lookup = const char*;

  static HashNumber hash(const Lookup& aLookup) { return HashString(aLookup); }

  static bool match(const Key& aKey, const Lookup& aLookup) {
    return strcmp(aKey.get(), aLookup) == 0;
  }
};

enum class StartupCacheEntryFlags {
@@ -204,7 +228,7 @@ struct StartupCacheEntry {
        mFlags(aFlags) {}

  struct Comparator {
    using Value = std::pair<const nsCString*, StartupCacheEntry*>;
    using Value = std::pair<const MaybeOwnedCharPtr*, StartupCacheEntry*>;

    bool Equals(const Value& a, const Value& b) const {
      // This is a bit ugly. Here and below, just note that we want entries
@@ -258,7 +282,8 @@ class StartupCache : public nsIMemoryReporter {
  friend class StartupCacheChild;

 public:
  using Table = HashMap<nsCString, StartupCacheEntry>;
  using Table =
      HashMap<MaybeOwnedCharPtr, StartupCacheEntry, StartupCacheKeyHasher>;

  NS_DECL_THREADSAFE_ISUPPORTS
  NS_DECL_NSIMEMORYREPORTER
+4 −1
Original line number Diff line number Diff line
@@ -41,7 +41,10 @@ void StartupCacheChild::SendEntriesAndFinalize(StartupCache::Table& entries) {

    auto data = dataArray.AppendElement();

    data->key() = key;
    MOZ_ASSERT(strnlen(key.get(), kStartupCacheKeyLengthCap) <
                   kStartupCacheKeyLengthCap,
               "StartupCache key over the size limit.");
    data->key() = nsCString(key.get());
    if (value.mFlags.contains(StartupCacheEntryFlags::AddedThisSession)) {
      data->data().AppendElements(
          reinterpret_cast<const uint8_t*>(value.mData.get()),