/* 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 https://mozilla.org/MPL/2.0/. */

#include "BaseAlloc.h"

#include <cstring>

#include "mozilla/Saturate.h"

#include "Globals.h"
#include "FdPrintf.h"

using namespace mozilla;

// Change this to 1 to enable some BaseAlloc logging. Useful for debugging.
#define BASE_ALLOC_LOGGING 0

// Change this to 1 to enable expensive assertions beyond normal debug
// builds.
#define BASE_ALLOC_VALIDATION 0

#if BASE_ALLOC_VALIDATION
bool TreeContains(RedBlackTree<BaseAllocCell, BaseAllocCellRBTrait>& aTree,
                  BaseAllocCell* aCell) {
  BaseAllocCell* cur = aTree.SearchOrNext(aCell->Size());
  while (cur) {
    if (cur == aCell) {
      return true;
    }

    if (cur->Size() != aCell->Size()) {
      return false;
    }
    cur = aTree.Next(cur);
  }

  return false;
}
#endif

// By using a macro "Log" won't collide with PHC's Log function in unified
// builds.
#if BASE_ALLOC_LOGGING
#  define Log BaseLog
static void BaseLog(const char* fmt, ...);
#else
#  define Log(...)
#endif

constinit BaseAlloc sBaseAlloc;

uintptr_t BaseAllocCell::Align(uintptr_t aPtr) {
  // In addition to assuming that kBaseQuantum, the cache line size and page
  // size are all powers of two.  We also assume that the quantum, cache
  // line size, and page size are each greater than the previous one.
  // Together these assumptions imply that each is a multiple of the
  // previous one.
  static_assert(BaseAlloc::kBaseQuantum <= kCacheLineSize);
  MOZ_ASSERT(kCacheLineSize <= gPageSize);

  uintptr_t address =
      ALIGNMENT_CEILING(aPtr, uintptr_t(BaseAlloc::kBaseQuantum));

  uintptr_t cache_line = address & ~uintptr_t(kCacheLineMask);

  if (cache_line + BaseAlloc::kBaseQuantum < address) {
    // This address would result in cells that share a cache line, move it
    // forward to the next cache line.
    address = cache_line + kCacheLineSize;
  }

  MOZ_ASSERT(aPtr <= address);
  MOZ_ASSERT((address % alignof(BaseAllocCell)) == 0);

  return address;
}

// Initialize base allocation data structures.
void BaseAlloc::Init() MOZ_REQUIRES(gInitLock) { mMutex.Init(); }

base_alloc_size_t BaseAlloc::size_round_up(base_alloc_size_t aSize) {
  return ALIGNMENT_CEILING(aSize, kBaseQuantum);
}

unsigned BaseAlloc::get_list_index_for_size(base_alloc_size_t aSize) {
  if constexpr (kBaseQuantum * 2 >= kCacheLineSize) {
    return aSize / kBaseQuantum - 1;
  } else {
    // The lambda template prevents the C++ compiler from checking this
    // branch when it's not used. This is used to avoid a compiler warning
    // when kBaseQuantum * == kCacheLineSize.
    return []<typename T>(T aSize) -> unsigned {
      // The base allocator will allocate all objects on their own
      // cache line, but if kBaseQuantum is less than two times smaller than
      // kCacheLineSize, then some object sizes are impossible, they're
      // always rounded up to ensure the next object begins on a cache line
      // boundary.  Naively this would lead to 1-in-4 free lists being
      // wasted (on x86_64) because no object will be created that size.
      // Instead the following code calculates the list index for a given
      // size.
      //
      // For any cache line multiple there are 3 possible sizes they are:
      //  + cache_multiple,
      //  + cache_multiple - kBaseQuantum
      //  + cache_multiple - kBaseQuantum*2
      //
      // The code here will map them to indexes for the free list array.

      // The minimum possible size is kBaseMinimumSize.  So start by
      // enforcing that using a saturating subtraction so that the minimum
      // becomes 0.
      aSize = (SaturateUint32(aSize) - kBaseMinimumSize).value();

      // After that subtraction dividing by the cache line size gives us
      // the group of 3 this size is in.
      unsigned cache_line = aSize / kCacheLineSize;

      // Find the remainder,
      unsigned offset = (aSize % kCacheLineSize) / kBaseQuantum;

      // Remainders 0, 1 and 2 are valid.  But any other remainder won't map
      // to a valid size, round up to the valid size.
      //
      // With an exception for offset = 3, the expression in the return
      // statement below will produce the same result for offset=3 wheather
      // we enter this branch or not so we can skip it in that case.
      if (offset > 3) {
        cache_line++;
        offset = 0;
      }

      // Find the index into the free list array.
      return cache_line * 3 + offset;
    }(aSize);
  }
}

BaseAllocMetadata* BaseAllocCell::RightMetadata() {
  uintptr_t ptr = reinterpret_cast<uintptr_t>(this) + Size() +
                  BaseAlloc::kBaseQuantum - sizeof(BaseAllocMetadata);

  MOZ_ASSERT((ptr % alignof(BaseAllocMetadata)) == 0);
  return reinterpret_cast<BaseAllocMetadata*>(ptr);
}

void BaseAlloc::free(void* aPtr) MOZ_EXCLUDES(mMutex) {
  if (aPtr == nullptr) {
    return;
  }

  // base_chunk_dealloc must run outside mMutex: chunk_record allocates an
  // extent_node_t via BaseAlloc::alloc which would re-acquire it.
  void* chunkToDealloc = nullptr;
  size_t chunkSizeToDealloc = 0;

  {
    MutexAutoLock lock(mMutex);

    BaseAllocCell* cell = BaseAllocCell::GetCell(aPtr);

    // Zero the contents of the memory cell before we add it to a free list.
    // Otherwise the DoublyLinkedList code will hit an assertion because it
    // looks like it's already in a list.
    cell->ClearPayload();
    cell->SetFreed();

    Log("free(%p), size: %u\n", aPtr, cell->Size());

    // Attempt to merge backwards
    BaseAllocCell* left = cell->LeftCell();
    if (left && !left->Allocated() && left->Committed()) {
      Unlink(left);
      left->Merge(cell);
      cell = left;
    }
    // And forward
    BaseAllocCell* right = cell->RightCell();
    if (right && !right->Allocated() && right->Committed()) {
      Unlink(right);
      cell->Merge(right);
    }

    if (cell->Size() >= kChunkSize && !cell->RightCell() && !cell->LeftCell()) {
      // The cell covers a whole chunk and can be completely released.
      uintptr_t addr = reinterpret_cast<uintptr_t>(cell) & ~gRealPageSizeMask;
      size_t size = REAL_PAGE_CEILING(cell->Size());
      Log("Releasing entire chunk %p, size %d", addr, size);
      chunkToDealloc = reinterpret_cast<void*>(addr);
      chunkSizeToDealloc = size;
      mStats.mCommitted -= size;
      mStats.mMapped -= size;
    } else {
      Link(cell);
    }
  }

  if (chunkToDealloc) {
    base_chunk_dealloc(chunkToDealloc, chunkSizeToDealloc, UNKNOWN_CHUNK);
  }
}

void* BaseAlloc::alloc(size_t aSize) {
  aSize = size_round_up(aSize);

  // Allocations cannot exceed sizes greater than BASE_ALLOC_SIZE_MAX which
  // is required by BaseAlloc's heap structure.  We assert but also return
  // null for builds without assertions.
  MOZ_ASSERT(aSize <= BASE_ALLOC_SIZE_MAX);
  if (aSize > BASE_ALLOC_SIZE_MAX) {
    return nullptr;
  }

  MutexAutoLock lock(mMutex);

  BaseAllocCell* cell = alloc_cell(aSize);
  if (cell) {
    MOZ_ASSERT(cell->Size() >= aSize);
    cell->SetAllocated();
    return cell->Ptr();
  }

  return nullptr;
}

BaseAllocCell* BaseAlloc::alloc_cell(base_alloc_size_t aSize) {
  BaseAllocCell* cell = alloc_from_list(aSize);
  if (cell) {
    Log("alloc(%u) = %p (from free list)\n", aSize, cell);
    return cell;
  }

  cell = oversize_alloc(aSize);
  if (cell) {
    Log("alloc(%u) = %p (from oversize)\n", aSize, cell);
    return cell;
  }

  // Try to merge decommitted cells with their committed neighbours until a
  // cell of at least aSize is created.
  if (merge_decommitted_cells(aSize)) {
    cell = oversize_alloc(aSize);
    if (cell) {
      Log("alloc(%u) = %p (from oversize after merging decommitted cells)\n",
          aSize, cell);
      return cell;
    }
  }

  cell = decommitted_alloc(aSize);
  if (cell) {
    Log("alloc(%u) = %p (from decommitted)\n", aSize, cell);
    return cell;
  }

  cell = chunk_alloc(aSize);
  if (cell) {
    Log("alloc(%u) = %p (from new chunk)\n", aSize, cell);
    return cell;
  }

  Log("alloc(%u) failed\n", aSize);
  return nullptr;
}

BaseAllocCell* BaseAlloc::alloc_from_list(base_alloc_size_t aSize) {
  unsigned start_index = get_list_index_for_size(aSize);
  for (unsigned i = start_index; i < kNumFreeLists; i++) {
    if (!mFreeLists[i].isEmpty()) {
#if BASE_ALLOC_VALIDATION
      MOZ_ASSERT(mFreeLists[i].ListIsWellFormed());
#endif
      BaseAllocCell* cell = mFreeLists[i].popFront();
      MaybeTrim(cell, aSize);

      return cell;
    }
  }
  return nullptr;
}

BaseAllocCell* BaseAlloc::oversize_alloc(base_alloc_size_t aSize) {
  // Search for the best fit in the oversize tree.
  BaseAllocCell* cell = mFreeListOversize.SearchOrNext(aSize);
  if (cell) {
    mFreeListOversize.Remove(cell);

    MaybeTrim(cell, aSize);

    return cell;
  }

  return nullptr;
}

void BaseAlloc::Unlink(BaseAllocCell* cell) {
  MOZ_ASSERT(!cell->Allocated());

  if (cell->Committed()) {
    unsigned index = get_list_index_for_size(cell->Size());
    if (index < kNumFreeLists) {
#if BASE_ALLOC_VALIDATION
      MOZ_ASSERT(mFreeLists[index].ListIsWellFormed());
      MOZ_ASSERT(mFreeLists[index].contains(cell));
#endif
      mFreeLists[index].remove(cell);
#if BASE_ALLOC_VALIDATION
      MOZ_ASSERT(mFreeLists[index].ListIsWellFormed());
#endif
    } else {
#if BASE_ALLOC_VALIDATION
      MOZ_ASSERT(TreeContains(mFreeListOversize, cell));
#endif
      mFreeListOversize.Remove(cell);
    }
  } else {
#if BASE_ALLOC_VALIDATION
    MOZ_ASSERT(TreeContains(mFreeListDecommitted, cell));
#endif
    mFreeListDecommitted.Remove(cell);
  }
}

void BaseAlloc::Link(BaseAllocCell* cell) {
  MOZ_ASSERT(!cell->Allocated());

  // the size must conform to our classes/free lists.
  MOZ_ASSERT(cell->Size() == size_round_up(cell->Size()));

  if (cell->Committed()) {
    unsigned index = get_list_index_for_size(cell->Size());
    // If a larger size would not place this entry into a different list
    // then this size is "illegal".
    MOZ_ASSERT(get_list_index_for_size(cell->Size() + kBaseQuantum) ==
               index + 1);
    if (index < kNumFreeLists) {
#if BASE_ALLOC_VALIDATION
      MOZ_ASSERT(mFreeLists[index].ListIsWellFormed());
      MOZ_ASSERT(!mFreeLists[index].contains(cell));
      MOZ_ASSERT(cell->ProbablyNotInList());
#endif
      mFreeLists[index].pushFront(cell);
#if BASE_ALLOC_VALIDATION
      MOZ_ASSERT(mFreeLists[index].ListIsWellFormed());
#endif
    } else {
#if BASE_ALLOC_VALIDATION
      MOZ_ASSERT(!TreeContains(mFreeListOversize, cell));
      MOZ_ASSERT(cell->ProbablyNotInList());
#endif
      mFreeListOversize.Insert(cell);
    }
  } else {
#if BASE_ALLOC_VALIDATION
    MOZ_ASSERT(!TreeContains(mFreeListDecommitted, cell));
    MOZ_ASSERT(cell->ProbablyNotInList());
#endif
    mFreeListDecommitted.Insert(cell);
  }
}

bool BaseAlloc::merge_decommitted_cells(base_alloc_size_t aSize) {
  // This might commit and merge multiple cells before creating one large
  // enough to satisfy the allocation.  Which may commit more memory than
  // necessary, but it's better than fragmentation.

  // The while loop and for loop are used together.  The for loop iterates
  // over the tree but if the code modifies the tree it needs to be
  // restarted, which is what the while loop is for - restarting that
  // iteration.
  //
  // After each item the code will either:
  //  * return true because it found a cell large enough,
  //  * possibly after
  //    merging)
  //  * return false because of an error committing memory.
  //  * Not be able to perform a merge with that cell and will go to the
  //    next cell in the tree.
  //  * Perform a merge, `break` and the while loop are used to restart the
  //    for loop.
  //  * return false because the entire tree was checked.
  bool restart;
  do {
    restart = false;
    // mFreeListDecommitted is sorted from smallest to largest so this will
    // attempt to merge smaller cells first.
    for (BaseAllocCell* cell : mFreeListDecommitted.iter()) {
      if (cell->Size() >= aSize) {
        // This cell is already large enough.  But this shouldn't happen
        // because oversize_alloc() failed before merge_decommitted_cells()
        // was called.
        return true;
      }

      BaseAllocCell* left = cell->LeftCell();
      if (left && !left->Allocated()) {
        // After unlink we can't use the iterator anymore, one way or
        // another code here must break the for loop.
        Unlink(cell);
        size_t change = cell->CommitAll();
        if (change == 0) {
          Link(cell);
          return false;
        }
        mStats.mCommitted += change;

        Unlink(left);
        if (!left->Committed()) {
          change = left->CommitAll();
          if (change == 0) {
            Link(left);
            return false;
          }
          mStats.mCommitted += change;
        }
        left->Merge(cell);
        Link(left);
        if (left->Size() >= aSize) {
          return true;
        }
        // Break the for loop restarting from the while loop.
        restart = true;
        break;
      }

      BaseAllocCell* right = cell->RightCell();
      if (right && !right->Allocated()) {
        Unlink(cell);
        size_t change = cell->CommitAll();
        if (change == 0) {
          Link(cell);
          return false;
        }
        mStats.mCommitted += change;

        Unlink(right);
        if (!right->Committed()) {
          change = right->CommitAll();
          if (change == 0) {
            Link(right);
            return false;
          }
          mStats.mCommitted += change;
        }
        cell->Merge(right);
        Link(cell);
        if (cell->Size() >= aSize) {
          return true;
        }
        restart = true;
        break;
      }
    }
  } while (restart);

  return false;
}

BaseAllocCell* BaseAlloc::chunk_alloc(base_alloc_size_t aSize)
    MOZ_REQUIRES(mMutex) {
  // aSize should be non-zero and aligned already.
  MOZ_ASSERT(aSize != 0);
  MOZ_ASSERT(aSize == size_round_up(aSize));

  // Make room for the metadata on either side of this cell and round up to
  // the chunk size.
  size_t csize = CHUNK_CEILING(kBaseQuantum * 2 + aSize);
  // Find the largest cell that fits within the chunk.
  base_alloc_size_t net_size = csize - kBaseQuantum * 2;
  MOZ_ASSERT(net_size >= aSize);

  void* base_pages = base_chunk_alloc(csize, kChunkSize);
  if (base_pages == nullptr) {
    return nullptr;
  }
  mStats.mCommitted += csize;
  mStats.mMapped += csize;

  BaseAllocCell* cell =
      new (reinterpret_cast<void*>(reinterpret_cast<uintptr_t>(base_pages) +
                                   kBaseQuantum)) BaseAllocCell(net_size);
  MaybeTrim(cell, aSize, true);

  return cell;
}

BaseAllocCell* BaseAlloc::decommitted_alloc(base_alloc_size_t aSize) {
  BaseAllocCell* cell = mFreeListDecommitted.SearchOrNext(aSize);
  if (!cell) {
    return nullptr;
  }
  mFreeListDecommitted.Remove(cell);

  auto result = cell->Commit(aSize);
  if (!result) {
    mFreeListDecommitted.Insert(cell);
    return nullptr;
  }

  mStats.mCommitted += result->mChange;
  if (result->mNewCell1) {
    Link(result->mNewCell1);
  }
  if (result->mNewCell2) {
    Link(result->mNewCell2);
  }

  MaybeTrim(cell, aSize);

  return cell;
}

void* BaseAlloc::calloc(size_t aNumber, size_t aSize) {
  void* ret = alloc(aNumber * aSize);
  if (ret) {
    memset(ret, 0, aNumber * aSize);
  }
  return ret;
}

void* BaseAlloc::realloc(void* aPtr, size_t aNewSize) {
  if (aNewSize == 0) {
    free(aPtr);
    return nullptr;
  }

  if (aPtr == nullptr) {
    return alloc(aNewSize);
  }

  BaseAllocCell* cell = reinterpret_cast<BaseAllocCell*>(aPtr);
  size_t old_size = cell->Size();

  aNewSize = size_round_up(aNewSize);
  if (aNewSize < old_size) {
    // Shrinking
    MutexAutoLock lock(mMutex);

    MaybeTrim(cell, aNewSize);
    MOZ_ASSERT(cell->Size() >= aNewSize);
    Log("realloc %p (size %u) shrink to %u\n", cell, old_size, cell->Size());
    return cell->Ptr();
  } else if (aNewSize > old_size) {
    // Growing
    {
      MutexAutoLock lock(mMutex);

      BaseAllocCell* right = cell->RightCell();

      // See if this cell's neighour is free and large enough that we can
      // merge
      if (right && !right->Allocated() && right->Committed() &&
          (cell->Size() + kBaseQuantum + right->Size()) >= aNewSize) {
        Unlink(right);
        cell->Merge(right);

        // The new cell might be bigger than necessary.
        MaybeTrim(cell, aNewSize);
        MOZ_ASSERT(cell->Size() >= aNewSize);

        Log("realloc %p (size %u) grow in-place to %u\n", cell, old_size,
            cell->Size());
        MOZ_ASSERT(cell->Allocated());
        return cell->Ptr();
      }
    }  // Unlock mMutex

    // Moving realloc.
    Log("realloc beginning...\n");
    BaseAllocCell* new_cell = reinterpret_cast<BaseAllocCell*>(alloc(aNewSize));
    if (!new_cell) {
      return nullptr;
    }
    memcpy(new_cell->Ptr(), cell->Ptr(), old_size);
    free(cell);
    Log("...realloc %p (size %u) grow to %p (sizx %u)\n", cell, old_size,
        new_cell, new_cell->Size());
    return new_cell->Ptr();
  }

  // The cell stays the same size.
  MOZ_ASSERT(cell->Size() >= aNewSize);
  Log("realloc %p (size %u) no-op\n", cell, cell->Size());
  return cell->Ptr();
}

size_t BaseAlloc::usable_size(void* aPtr) {
  return reinterpret_cast<BaseAllocCell*>(aPtr)->Size();
}

void BaseAllocCell::SetSize(base_alloc_size_t aSize) {
  MOZ_ASSERT(aSize == BaseAlloc::size_round_up(aSize));

  // Set the left metadata's size first so it can be used to get the
  // right metadata's address.
  LeftMetadata()->mRightSize = aSize;

  // Now it's safe to set the right metadata's size.  Note that both the
  // old-right metadata, and the new metadata's right size are left untouched.
  RightMetadata()->mLeftSize = aSize;
}

void BaseAllocCell::ClearPayload() {
  memset(&mListElem, 0, sizeof(mListElem));
  mCommitted = true;
}

BaseAllocCell* BaseAllocCell::LeftCell() {
  base_alloc_size_t left_cell_size = LeftMetadata()->mLeftSize;
  if (!left_cell_size) {
    return nullptr;
  }

  BaseAllocCell* left = reinterpret_cast<BaseAllocCell*>(
      reinterpret_cast<uintptr_t>(this) - BaseAlloc::kBaseQuantum -
      left_cell_size);

  MOZ_ASSERT(left->RightMetadata() == LeftMetadata());

  return left;
}

BaseAllocCell* BaseAllocCell::RightCell() {
  base_alloc_size_t right_size = RightMetadata()->mRightSize;
  if (right_size == 0) {
    return nullptr;
  }

  BaseAllocCell* right = reinterpret_cast<BaseAllocCell*>(RightCellRaw());

  MOZ_ASSERT(RightMetadata() == right->LeftMetadata());

  return right;
}

uintptr_t BaseAllocCell::RightCellRaw() {
  return reinterpret_cast<uintptr_t>(this) + Size() + BaseAlloc::kBaseQuantum;
}

void BaseAllocCell::Merge(BaseAllocCell* aOther) {
  // aOther must be after this, we can check by comparing what they each
  // think their metadata is.
  MOZ_ASSERT(RightMetadata() == aOther->LeftMetadata());
  base_alloc_size_t new_size =
      Size() + aOther->Size() + BaseAlloc::kBaseQuantum;

  Log("Merge %p (size %u) with %p (size %u) -> size %u\n", this, Size(), aOther,
      aOther->Size(), new_size);

#ifdef MOZ_DEBUG
  BaseAllocMetadata* right_metadata = aOther->RightMetadata();
#endif
  // Check for overflow.
  MOZ_ASSERT(new_size > this->Size() && new_size > aOther->Size());

  BaseAllocMetadata* old_metadata = RightMetadata();
  SetSize(new_size);

  MOZ_ASSERT(RightMetadata() == right_metadata);

  // Clearing the old metadata may make debugging easier.
  old_metadata->Clear();
}

uintptr_t BaseAllocCell::CanSplit(base_alloc_size_t aSizeReq) {
  if (aSizeReq + BaseAlloc::kBaseQuantum + sizeof(BaseAllocCell) >= Size()) {
    // Insufficient size.
    return 0;
  }

  // Rather than use the requested size directly for the first cell, start
  // with the requested size then align the next cell and check if it still
  // leaves enough room after alignment.

  uintptr_t next_addr = Align(reinterpret_cast<uintptr_t>(this) + aSizeReq +
                              sizeof(BaseAllocMetadata));

  if (next_addr + BaseAlloc::kBaseMinimumSize >
      reinterpret_cast<uintptr_t>(RightMetadata())) {
    return 0;
  }

  return next_addr;
}

void BaseAlloc::MaybeTrim(BaseAllocCell* aCell, base_alloc_size_t aSizeRequest,
                          bool aDecommit) {
  uintptr_t new_addr = aCell->CanSplit(aSizeRequest);
  if (!new_addr) {
    return;
  }

  BaseAllocCell* next = aCell->Split(new_addr);
  MOZ_ASSERT(next);

  if (aDecommit && (next->Size() >= kDecommitThreshold)) {
    auto result = next->Decommit();
    mStats.mCommitted -= result.mChange;
    if (result.mNewCell1) {
      Link(result.mNewCell1);
    }
    if (result.mNewCell2) {
      Link(result.mNewCell2);
    }
  }

  Link(next);
}

bool BaseAllocCell::CanSplitHere(uintptr_t aNextAddr) {
  MOZ_ASSERT(Align(aNextAddr) == aNextAddr);

  if (Align(reinterpret_cast<uintptr_t>(this) + BaseAlloc::kBaseQuantum +
            sizeof(BaseAllocMetadata)) > aNextAddr) {
    // Not enough size for metadata before the beginning of the new cell.
    return false;
  }

  if (aNextAddr + BaseAlloc::kBaseQuantum >
      reinterpret_cast<uintptr_t>(this) + Size()) {
    // Not enough size in the new cell.
    return false;
  }

  return true;
}

BaseAllocCell* BaseAllocCell::Split(uintptr_t aNewAddr) {
#ifdef MOZ_DEBUG
  BaseAllocMetadata* last_metadata = RightMetadata();
#endif
  base_alloc_size_t old_size = Size();
  base_alloc_size_t new_size =
      aNewAddr - BaseAlloc::kBaseQuantum - reinterpret_cast<uintptr_t>(this);
  SetSize(new_size);

  // This must use NextCellRaw and cast the result, using NextCell would run
  // assertions that would fail.
  BaseAllocCell* right = new (reinterpret_cast<BaseAllocCell*>(RightCellRaw()))
      BaseAllocCell(old_size - new_size - BaseAlloc::kBaseQuantum);

  Log("Split %p (size %u) -> (size %u) and %p (size %u)\n", this, old_size,
      Size(), right, right->Size());

  // Prove that the alignment code above is correct.
  MOZ_ASSERT(new_size == BaseAlloc::size_round_up(new_size));
  MOZ_ASSERT(right->Size() == BaseAlloc::size_round_up(right->Size()));
  MOZ_ASSERT(this->RightMetadata() == right->LeftMetadata());
  MOZ_ASSERT(right->RightMetadata() == last_metadata);

  return right;
}

BaseAllocCell::DeCommitResult BaseAllocCell::Decommit() {
  // Decommit pages within the "next" chunk.
  uintptr_t start = REAL_PAGE_CEILING(reinterpret_cast<uintptr_t>(this) +
                                      sizeof(BaseAllocCell));
  uintptr_t end = REAL_PAGE_FLOOR(reinterpret_cast<uintptr_t>(RightMetadata()));
  if (start >= end) {
    return DeCommitResult(0);
  }

  uintptr_t nbytes = end - start;

  // Try to split this cell so that more of the resident memory is usable.
  uintptr_t boundary = Align(end + BaseAlloc::kBaseQuantum);
  BaseAllocCell* end_cell = CanSplitHere(boundary) ? Split(boundary) : nullptr;

  boundary = Align(start - kCacheLineSize + BaseAlloc::kBaseQuantum);
  BaseAllocCell* cell = CanSplitHere(boundary) ? Split(boundary) : nullptr;

  if (cell) {
    cell->DoDecommit(start, nbytes);
  } else {
    DoDecommit(start, nbytes);
  }

  return DeCommitResult(nbytes, cell, end_cell);
}

void BaseAllocCell::DoDecommit(uintptr_t aFirstDecommit, uintptr_t aNBytes) {
  MOZ_ASSERT(reinterpret_cast<uintptr_t>(this) + sizeof(BaseAllocCell) <=
             aFirstDecommit);
  MOZ_ASSERT(aFirstDecommit + aNBytes <=
             reinterpret_cast<uintptr_t>(this) + Size());

  pages_decommit(reinterpret_cast<void*>(aFirstDecommit), aNBytes);
  mCommitted = false;

  Log("Decommitting in cell %p: %p - %p, %zu bytes\n", this, aFirstDecommit,
      aFirstDecommit + aNBytes, aNBytes);
}

Maybe<BaseAllocCell::DeCommitResult> BaseAllocCell::Commit(
    base_alloc_size_t aSizeReq) {
  MOZ_ASSERT(!mCommitted);
  MOZ_ASSERT(Size() >= aSizeReq);

  // The address after the last decommitted byte.
  uintptr_t last_decommitted =
      REAL_PAGE_FLOOR(reinterpret_cast<uintptr_t>(RightMetadata()));

  // The first currently-decommitted address.
  uintptr_t first_decommitted = REAL_PAGE_CEILING(
      reinterpret_cast<uintptr_t>(this) + sizeof(BaseAllocCell));

  MOZ_ASSERT(first_decommitted < last_decommitted);

  // A partly decommitted cell will require at least sizeof(BaseAllocCell)
  // bytes of its payload in committed memory.  But it also needs to be
  // properly aligned so that its payload isn't in the cache line of the
  // previous cell.  The minimum committed bytes of a decommitted cell is:
  base_alloc_size_t min_committed_bytes =
      std::max(base_alloc_size_t(kCacheLineSize) - BaseAlloc::kBaseQuantum,
               BaseAlloc::kBaseQuantum);

  // The end of the range that needs to be committed.
  // PAGE_CEILING(Align(this + aSizeReq + quantum)) is the lowest address
  // that may be decommitted and still satisfy an allocation on aSizeReq.
  // But because the cell to the right also needs to have the fields of
  // BaseAllocCell within committed memory then we need to add
  // min_committed_bytes before rounding up to the page boundary.
  uintptr_t new_first_decommitted =
      REAL_PAGE_CEILING(Align(reinterpret_cast<uintptr_t>(this) + aSizeReq +
                              BaseAlloc::kBaseQuantum) +
                        min_committed_bytes);

  // new_first_decommitted may be after last_decommitted when aSizeReq is large
  // enough that the committed memory at the end of the cell is also required to
  // satisfy the allocation.  But it will never be larger than the page
  // after the payload fo the next cell.
  MOZ_ASSERT(new_first_decommitted <=
             REAL_PAGE_CEILING(RightCellRaw() + sizeof(BaseAllocCell)));
  new_first_decommitted = std::min(new_first_decommitted, last_decommitted);

  MOZ_ASSERT(first_decommitted <= new_first_decommitted);

  if (first_decommitted == new_first_decommitted) {
    // Nothing needs committing to satisfy the allocation since it can be
    // satisfied from the first part of the cell.  This shouldn't happen
    // because the cell should have been split when it was decommitted.
    uintptr_t split_addr = CanSplit(aSizeReq);
    if (split_addr == 0) {
      return Nothing();
    }
    MOZ_ASSERT(split_addr < first_decommitted);
    BaseAllocCell* cell = Split(split_addr);
    mCommitted = true;
    cell->mCommitted = false;
    return Some(DeCommitResult(0, cell));
  }

  bool whole_cell = new_first_decommitted == last_decommitted;
  Log("Committing %s cell %p: %p - %p, %zu bytes\n",
      whole_cell ? "whole" : "part", this, first_decommitted,
      new_first_decommitted, new_first_decommitted - first_decommitted);

  // Do the commit before the split so that the new boundary is writable.
  if (!pages_commit(reinterpret_cast<void*>(first_decommitted),
                    new_first_decommitted - first_decommitted)) {
    return Nothing();
  }
  mCommitted = true;

  if (whole_cell) {
    return Some(DeCommitResult(new_first_decommitted - first_decommitted));
  }

  BaseAllocCell* cell = Split(new_first_decommitted - min_committed_bytes);
  cell->mCommitted = false;

  return Some(DeCommitResult(new_first_decommitted - first_decommitted, cell));
}

size_t BaseAllocCell::CommitAll() {
  Maybe<BaseAllocCell::DeCommitResult> commit_res = Commit(Size());
  if (!commit_res) {
    return 0;
  }
  MOZ_ASSERT(!commit_res->mNewCell1);
  MOZ_ASSERT(!commit_res->mNewCell2);
  return commit_res->mChange;
}

#if BASE_ALLOC_LOGGING
static size_t GetPid() { return size_t(getpid()); }

static void BaseLog(const char* fmt, ...) {
#  ifdef _WIN32
#    define LOG_STDERR GetStdHandle(STD_ERROR_HANDLE)
#  else
#    define LOG_STDERR 2
#  endif

  char buf[256];
  size_t pos = SNPrintf(buf, sizeof(buf), "BaseAlloc[%zu] ", GetPid());
  va_list vargs;
  va_start(vargs, fmt);
  pos += VSNPrintf(&buf[pos], sizeof(buf) - pos, fmt, vargs);
  MOZ_ASSERT(pos < sizeof(buf));
  va_end(vargs);

  FdPuts(LOG_STDERR, buf, pos);
}
#endif  // BASE_ALLOC_LOGGING

#undef Log
