Plus actually checking the configuration file change in Differential Revision: https://phabricator.services.mozilla.com/D301774
908 lines
28 KiB
C++
908 lines
28 KiB
C++
/* 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
|