/* 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 "gc/BufferAllocator-inl.h"
#include "mozilla/Likely.h"
#include "mozilla/ScopeExit.h"
#include <bit>
#ifdef XP_DARWIN
# include <mach/mach_init.h>
# include <mach/vm_map.h>
#endif
#include "gc/BufferAllocatorInternals.h"
#include "gc/GCInternals.h"
#include "gc/GCLock.h"
#include "gc/PublicIterators.h"
#include "gc/Tenuring.h"
#include "gc/Zone.h"
#include "js/HeapAPI.h"
#include "util/Poison.h"
#include "gc/Heap-inl.h"
#include "gc/Marking-inl.h"
using
namespace js;
using
namespace js::gc;
namespace js::gc {
AutoLockBufferAllocator::AutoLockBufferAllocator(
BufferAllocatorRuntime* runtime)
: LockGuard(runtime->lock) {}
static void CheckHighBitsOfPointer(
void* ptr) {
#ifdef JS_64BIT
// We require bit 48 and higher be clear.
MOZ_DIAGNOSTIC_ASSERT((uintptr_t(ptr) >>
47) ==
0);
#endif
}
BufferAllocator::FreeLists::FreeLists(FreeLists&& other) {
MOZ_ASSERT(this != &other);
assertEmpty();
std::swap(lists, other.lists);
std::swap(available, other.available);
other.assertEmpty();
}
BufferAllocator::FreeLists& BufferAllocator::FreeLists::
operator=(
FreeLists&& other) {
MOZ_ASSERT(this != &other);
assertEmpty();
std::swap(lists, other.lists);
std::swap(available, other.available);
other.assertEmpty();
return *this;
}
BufferAllocator::FreeLists::FreeListIter
BufferAllocator::FreeLists::freeListIter() {
return FreeListIter(*this);
}
BufferAllocator::FreeLists::FreeRegionIter
BufferAllocator::FreeLists::freeRegionIter() {
return FreeRegionIter(*this);
}
bool BufferAllocator::FreeLists::hasSizeClass(size_t sizeClass)
const {
MOZ_ASSERT(sizeClass <= MaxMediumAllocClass);
return available[sizeClass];
}
size_t BufferAllocator::FreeLists::getFirstAvailableSizeClass(
size_t minSizeClass, size_t maxSizeClass)
const {
MOZ_ASSERT(maxSizeClass <= MaxMediumAllocClass);
size_t result = available.FindNext(minSizeClass);
MOZ_ASSERT(result >= minSizeClass);
MOZ_ASSERT_IF(result != SIZE_MAX, !lists[result].isEmpty());
if (result > maxSizeClass) {
return SIZE_MAX;
}
return result;
}
size_t BufferAllocator::FreeLists::getLastAvailableSizeClass(
size_t minSizeClass, size_t maxSizeClass)
const {
MOZ_ASSERT(maxSizeClass <= MaxMediumAllocClass);
size_t result = available.FindPrev(maxSizeClass);
MOZ_ASSERT(result <= maxSizeClass || result == SIZE_MAX);
MOZ_ASSERT_IF(result != SIZE_MAX, !lists[result].isEmpty());
if (result < minSizeClass) {
return SIZE_MAX;
}
return result;
}
BufferAllocator::FreeRegion* BufferAllocator::FreeLists::getFirstRegion(
size_t sizeClass) {
MOZ_ASSERT(!lists[sizeClass].isEmpty());
return lists[sizeClass].getFirst();
}
void BufferAllocator::FreeLists::pushFront(size_t sizeClass,
FreeRegion* region) {
MOZ_ASSERT(sizeClass < AllocSizeClasses);
lists[sizeClass].pushFront(region);
available[sizeClass] = true;
}
void BufferAllocator::FreeLists::pushBack(size_t sizeClass,
FreeRegion* region) {
MOZ_ASSERT(sizeClass < AllocSizeClasses);
lists[sizeClass].pushBack(region);
available[sizeClass] = true;
}
void BufferAllocator::FreeLists::append(FreeLists&& other) {
for (size_t i =
0; i < AllocSizeClasses; i++) {
if (!other.lists[i].isEmpty()) {
lists[i].append(std::move(other.lists[i]));
available[i] = true;
}
}
other.available.ResetAll();
other.assertEmpty();
}
void BufferAllocator::FreeLists::remove(size_t sizeClass, FreeRegion* region) {
MOZ_ASSERT(sizeClass < AllocSizeClasses);
lists[sizeClass].remove(region);
available[sizeClass] = !lists[sizeClass].isEmpty();
}
void BufferAllocator::FreeLists::clear() {
for (
auto freeList = freeListIter(); !freeList.done(); freeList.next()) {
new (&freeList.get()) FreeList;
// clear() is less efficient.
}
available.ResetAll();
}
template <typename Func>
void BufferAllocator::FreeLists::forEachRegion(Func&& func) {
for (size_t i =
0; i <= MaxMediumAllocClass; i++) {
FreeList& freeList = lists[i];
FreeRegion* region = freeList.getFirst();
while (region) {
FreeRegion* next = region->getNext();
func(freeList, i, region);
region = next;
}
available[i] = !freeList.isEmpty();
}
}
inline void BufferAllocator::FreeLists::assertEmpty()
const {
#ifdef DEBUG
for (size_t i =
0; i < AllocSizeClasses; i++) {
MOZ_ASSERT(lists[i].isEmpty());
}
MOZ_ASSERT(available.IsEmpty());
#endif
}
inline void BufferAllocator::FreeLists::assertContains(
size_t sizeClass, FreeRegion* region)
const {
#ifdef DEBUG
MOZ_ASSERT(available[sizeClass]);
MOZ_ASSERT(lists[sizeClass].contains(region));
#endif
}
inline void BufferAllocator::FreeLists::checkAvailable()
const {
#ifdef DEBUG
for (size_t i =
0; i < AllocSizeClasses; i++) {
MOZ_ASSERT(available[i] == !lists[i].isEmpty());
}
#endif
}
BufferAllocator::ChunkLists::ChunkListIter
BufferAllocator::ChunkLists::chunkListIter() {
return ChunkListIter(*this);
}
BufferAllocator::ChunkLists::ChunkIter
BufferAllocator::ChunkLists::chunkIter() {
return ChunkIter(*this);
}
size_t BufferAllocator::ChunkLists::getFirstAvailableSizeClass(
size_t minSizeClass, size_t maxSizeClass)
const {
MOZ_ASSERT(maxSizeClass <= MaxMediumAllocClass);
size_t result = available.FindNext(minSizeClass);
MOZ_ASSERT(result >= minSizeClass);
MOZ_ASSERT_IF(result != SIZE_MAX, !lists[result].isEmpty());
if (result > maxSizeClass) {
return SIZE_MAX;
}
return result;
}
BufferChunk* BufferAllocator::ChunkLists::popFirstChunk(size_t sizeClass) {
MOZ_ASSERT(sizeClass < AllocSizeClasses);
MOZ_ASSERT(!lists[sizeClass].isEmpty());
BufferChunk* chunk = lists[sizeClass].popFirst();
if (lists[sizeClass].isEmpty()) {
available[sizeClass] =
false;
}
return chunk;
}
void BufferAllocator::ChunkLists::remove(size_t sizeClass, BufferChunk* chunk) {
MOZ_ASSERT(sizeClass <= AllocSizeClasses);
lists[sizeClass].remove(chunk);
available[sizeClass] = !lists[sizeClass].isEmpty();
}
void BufferAllocator::ChunkLists::pushFront(size_t sizeClass,
BufferChunk* chunk) {
MOZ_ASSERT(sizeClass <= AllocSizeClasses);
lists[sizeClass].pushFront(chunk);
available[sizeClass] = true;
}
void BufferAllocator::ChunkLists::pushBack(BufferChunk* chunk) {
MOZ_ASSERT(chunk->ownsFreeLists);
pushBack(chunk->sizeClassForAvailableLists(), chunk);
}
void BufferAllocator::ChunkLists::pushBack(size_t sizeClass,
BufferChunk* chunk) {
MOZ_ASSERT(sizeClass <= AllocSizeClasses);
MOZ_ASSERT(sizeClass == chunk->sizeClassForAvailableLists());
lists[sizeClass].pushBack(chunk);
available[sizeClass] = true;
}
BufferAllocator::BufferChunkList
BufferAllocator::ChunkLists::extractAllChunks() {
// Extract all chunks to a new list ordered so that chunks with the largest
// free regions are last.
BufferChunkList result = std::move(lists[FullChunkSizeClass]);
for (
auto list = chunkListIter(); !list.done(); list.next()) {
result.append(std::move(list.get()));
}
available.ResetAll();
return result;
}
inline bool BufferAllocator::ChunkLists::isEmpty()
const {
checkAvailable();
return available.IsEmpty();
}
inline void BufferAllocator::ChunkLists::checkAvailable()
const {
#ifdef DEBUG
for (size_t i =
0; i < AllocSizeClasses; i++) {
MOZ_ASSERT(available[i] == !lists[i].isEmpty());
}
#endif
}
}
// namespace js::gc
MOZ_ALWAYS_INLINE
void PoisonAlloc(
void* alloc, uint8_t value, size_t bytes,
MemCheckKind kind) {
#ifndef EARLY_BETA_OR_EARLIER
// Limit poisoning in release builds.
bytes = std::min(bytes, size_t(
256));
#endif
AlwaysPoison(alloc, value, bytes, kind);
}
template <typename D, size_t S, size_t G>
void AllocSpace<D, S, G>::setAllocated(
void* alloc, size_t bytes,
bool allocated) {
MOZ_ASSERT(bytes !=
0);
MOZ_ASSERT(bytes % GranularityBytes ==
0);
size_t startBit = ptrToIndex(alloc);
size_t endBit = endBitIndex(startBit, bytes);
MOZ_ASSERT(allocStartBitmap.ref()[startBit] != allocated);
MOZ_ASSERT(allocStartBitmap.ref()[startBit] == allocEndBitmap.ref()[endBit]);
MOZ_ASSERT(findEndBit(startBit) >= endBit);
allocStartBitmap.ref()[startBit] = allocated;
allocEndBitmap.ref()[endBit] = allocated;
}
template <typename D, size_t S, size_t G>
void AllocSpace<D, S, G>::setDeallocated(
void* alloc, size_t bytes) {
MOZ_ASSERT(allocBytes(alloc) == bytes);
setNurseryOwned(alloc,
false);
setAllocated(alloc, bytes,
false);
}
template <typename D, size_t S, size_t G>
void AllocSpace<D, S, G>::updateEndOffset(
void* alloc, size_t oldBytes,
size_t newBytes) {
MOZ_ASSERT(isAllocated(alloc));
MOZ_ASSERT(newBytes != oldBytes);
size_t startBit = ptrToIndex(alloc);
size_t oldEndBit = endBitIndex(startBit, oldBytes);
MOZ_ASSERT(allocEndBitmap.ref()[oldEndBit]);
allocEndBitmap.ref()[oldEndBit] =
false;
size_t newEndBit = endBitIndex(startBit, newBytes);
MOZ_ASSERT(allocStartBitmap.ref().FindNext(startBit +
1) > newEndBit);
MOZ_ASSERT(findEndBit(startBit) > newEndBit);
allocEndBitmap.ref()[newEndBit] = true;
}
template <typename D, size_t S, size_t G>
size_t AllocSpace<D, S, G>::allocBytes(
const void* alloc)
const {
MOZ_ASSERT(isAllocated(alloc));
size_t startBit = ptrToIndex(alloc);
size_t endBit = findEndBit(startBit);
MOZ_ASSERT(endBit >= startBit);
MOZ_ASSERT(endBit < MaxAllocCount);
return (endBit - startBit +
1) * GranularityBytes;
}
template <typename D, size_t S, size_t G>
bool AllocSpace<D, S, G>::setMarked(
void* alloc) {
MOZ_ASSERT(isAllocated(alloc));
size_t bit = ptrToIndex(alloc);
// This is thread safe but can return false positives if another thread also
// marked the same allocation at the same time;
if (markBits.ref().getBit(bit)) {
return false;
}
markBits.ref().setBit(bit, true);
return true;
}
template <typename D, size_t S, size_t G>
size_t AllocSpace<D, S, G>::findNextAllocated(uintptr_t offset)
const {
size_t bit = offsetToIndex(offset);
size_t next = allocStartBitmap.ref().FindNext(bit);
if (next == SIZE_MAX) {
return SizeBytes;
}
return next * GranularityBytes;
}
template <typename D, size_t S, size_t G>
size_t AllocSpace<D, S, G>::findPrevAllocated(uintptr_t offset)
const {
size_t bit = offsetToIndex(offset);
size_t prev = allocStartBitmap.ref().FindPrev(bit);
if (prev == SIZE_MAX) {
return SizeBytes;
}
return prev * GranularityBytes;
}
template <typename D, size_t S, size_t G>
BufferAllocator::FreeRegion* AllocSpace<D, S, G>::findFollowingFreeRegion(
uintptr_t startAddr) {
// Find the free region that starts at |startAddr|, which is not allocated and
// not at the end of the chunk. Always returns a region.
uintptr_t offset = uintptr_t(startAddr) & AddressMask;
MOZ_ASSERT(isValidOffset(offset));
MOZ_ASSERT((offset % GranularityBytes) ==
0);
MOZ_ASSERT(!isAllocated(offset));
// Already marked as not allocated.
offset = findNextAllocated(offset);
MOZ_ASSERT(offset <= SizeBytes);
auto* region = FreeRegion::fromEndAddr(startAddress() + offset);
MOZ_ASSERT(region->startAddr == startAddr);
return region;
}
template <typename D, size_t S, size_t G>
BufferAllocator::FreeRegion* AllocSpace<D, S, G>::findPrecedingFreeRegion(
uintptr_t endAddr) {
// Find the free region, if any, that ends at |endAddr|, which may be
// allocated or at the start of the chunk.
uintptr_t offset = uintptr_t(endAddr) & AddressMask;
MOZ_ASSERT(isValidOffset(offset));
MOZ_ASSERT((offset % GranularityBytes) ==
0);
if (offset == firstAllocOffset()) {
return nullptr;
// Already at start of chunk.
}
MOZ_ASSERT(!isAllocated(offset));
offset = findPrevAllocated(offset);
if (offset != SizeBytes) {
// Found a preceding allocation.
const void* alloc = ptrFromOffset(offset);
size_t bytes = allocBytes(alloc);
MOZ_ASSERT(uintptr_t(alloc) + bytes <= endAddr);
if (uintptr_t(alloc) + bytes == endAddr) {
// No free space between preceding allocation and |endAddr|.
return nullptr;
}
}
auto* region = FreeRegion::fromEndAddr(endAddr);
#ifdef DEBUG
region->check();
if (offset != SizeBytes) {
const void* alloc = ptrFromOffset(offset);
size_t bytes = allocBytes(alloc);
MOZ_ASSERT(region->startAddr == uintptr_t(alloc) + bytes);
}
else {
MOZ_ASSERT(region->startAddr == startAddress() + firstAllocOffset());
}
#endif
return region;
}
BufferChunk::BufferChunk(Zone* zone)
: ChunkBase(zone->runtimeFromAnyThread(), ChunkKind::Buffers), zone(zone) {
MOZ_ASSERT(decommittedPages.ref().IsEmpty());
MOZ_ASSERT(!allocatedDuringCollection);
MOZ_ASSERT(!stolenFromSweepList);
}
BufferChunk::~BufferChunk() {
#ifdef DEBUG
MOZ_ASSERT(allocStartBitmap.ref().IsEmpty());
MOZ_ASSERT(allocEndBitmap.ref().IsEmpty());
MOZ_ASSERT(nurseryOwnedBitmap.ref().IsEmpty());
#endif
}
void BufferChunk::setSmallBufferRegion(
void* alloc,
bool smallAlloc) {
MOZ_ASSERT(isAllocated(alloc));
size_t bit = ptrToIndex<SmallRegionSize, SmallRegionSize>(alloc);
smallRegionBitmap.ref().setBit(bit, smallAlloc);
}
bool BufferChunk::isSmallBufferRegion(
const void* alloc)
const {
// Allow any valid small alloc pointer within the region.
size_t bit = ptrToIndex<SmallRegionSize, SmallAllocGranularity>(alloc);
return smallRegionBitmap.ref().getBit(bit);
}
size_t BufferChunk::sizeClassForAvailableLists()
const {
MOZ_ASSERT(ownsFreeLists);
// To quickly find an available chunk we bin them by the size of their largest
// free region. This allows us to select a chunk we know will be able to
// satisfy a request.
//
// This prioritises allocating into chunks with large free regions first. It
// might be better for memory use to allocate into chunks with less free space
// first instead.
size_t sizeClass =
freeLists.ref().getLastAvailableSizeClass(
0, MaxMediumAllocClass);
// Use a special size class for completely full chunks.
if (sizeClass == SIZE_MAX) {
return BufferAllocator::FullChunkSizeClass;
}
return sizeClass;
}
void SmallBufferRegion::setHasNurseryOwnedAllocs(
bool value) {
hasNurseryOwnedAllocs_ = value;
}
bool SmallBufferRegion::hasNurseryOwnedAllocs()
const {
return hasNurseryOwnedAllocs_.ref();
}
BufferAllocatorRuntime::BufferAllocatorRuntime()
: lock(mutexid::BufferAllocator) {}
void BufferAllocatorRuntime::incSweepCount() { allocatorSweepCount++; }
void BufferAllocatorRuntime::decSweepCount() {
MOZ_ALWAYS_TRUE(allocatorSweepCount-- !=
0);
}
bool BufferAllocatorRuntime::needLockToAccessBufferMap()
const {
return allocatorSweepCount !=
0;
}
LargeBuffer* BufferAllocatorRuntime::lookupLargeBuffer(
void* alloc) {
MaybeLock lock;
return lookupLargeBuffer(alloc, lock);
}
LargeBuffer* BufferAllocatorRuntime::lookupLargeBuffer(
void* alloc,
MaybeLock& lock) {
MOZ_ASSERT(lock.isNothing());
if (needLockToAccessBufferMap()) {
lock.emplace(this);
}
auto ptr = largeAllocMap.ref().readonlyThreadsafeLookup(alloc);
MOZ_ASSERT(ptr);
LargeBuffer* buffer = ptr->value();
MOZ_ASSERT(buffer->data() == alloc);
return buffer;
}
void BufferAllocatorRuntime::checkGCStateNotInUse() {
MOZ_ASSERT(allocatorSweepCount ==
0);
}
BufferAllocator::BufferAllocator(GCRuntime* gc, Zone* zone)
: gc(gc),
zone(zone),
sweptMixedChunks(gc->bufferRuntime().lock),
sweptTenuredChunks(gc->bufferRuntime().lock),
sweptLargeTenuredAllocs(gc->bufferRuntime().lock),
minorState(State::NotCollecting),
majorState(State::NotCollecting),
minorSweepingFinished(gc->bufferRuntime().lock),
majorSweepingFinished(gc->bufferRuntime().lock) {}
BufferAllocator::~BufferAllocator() {
#ifdef DEBUG
checkGCStateNotInUse();
MOZ_ASSERT(mixedChunks.ref().isEmpty());
MOZ_ASSERT(tenuredChunks.ref().isEmpty());
freeLists.ref().assertEmpty();
MOZ_ASSERT(availableMixedChunks.ref().isEmpty());
MOZ_ASSERT(availableTenuredChunks.ref().isEmpty());
MOZ_ASSERT(largeNurseryAllocs.ref().isEmpty());
MOZ_ASSERT(largeTenuredAllocs.ref().isEmpty());
#endif
}
bool BufferAllocator::isEmpty()
const {
checkMainThread();
MOZ_ASSERT(!zone->wasGCStarted() || zone->isGCFinished());
MOZ_ASSERT(minorState == State::NotCollecting);
MOZ_ASSERT(majorState == State::NotCollecting);
return mixedChunks.ref().isEmpty() && availableMixedChunks.ref().isEmpty() &&
tenuredChunks.ref().isEmpty() &&
availableTenuredChunks.ref().isEmpty() &&
largeNurseryAllocs.ref().isEmpty() &&
largeTenuredAllocs.ref().isEmpty();
}
void BufferAllocator::setMultiThreadedUse(Mutex* mutex) {
MOZ_ASSERT(CurrentThreadCanAccessZone(zone));
MOZ_ASSERT(!multiThreadedMutex);
MOZ_ASSERT(majorState != State::Sweeping);
multiThreadedMutex = mutex;
}
void BufferAllocator::clearMultiThreadedUse() {
MOZ_ASSERT(CurrentThreadCanAccessZone(zone));
MOZ_ASSERT(multiThreadedMutex);
MOZ_ASSERT(majorState != State::Sweeping);
multiThreadedMutex = nullptr;
}
void BufferAllocator::checkAccess()
const {
#ifdef DEBUG
if (multiThreadedMutex) {
MOZ_ASSERT(multiThreadedMutex->isOwnedByCurrentThread());
}
else {
MOZ_ASSERT(CurrentThreadCanAccessZone(zone));
}
#endif
}
void BufferAllocator::checkMainThread()
const {
MOZ_ASSERT(!multiThreadedMutex);
MOZ_ASSERT(CurrentThreadCanAccessZone(zone));
}
bool BufferAllocator::isUsedByMainThread()
const {
return !multiThreadedMutex; }
BufferAllocatorRuntime* BufferAllocator::runtime()
const {
return &gc->bufferRuntime();
}
void* BufferAllocator::alloc(size_t bytes,
bool nurseryOwned) {
MOZ_ASSERT_IF(zone->isGCMarkingOrSweeping(), majorState == State::Marking);
checkAccess();
if (IsLargeAllocSize(bytes)) {
return allocLarge(bytes, nurseryOwned,
false);
}
if (IsSmallAllocSize(bytes)) {
return allocSmall(bytes, nurseryOwned,
false);
}
return allocMedium(bytes, nurseryOwned,
false);
}
void* BufferAllocator::allocInGC(size_t bytes,
bool nurseryOwned) {
// Currently this is used during tenuring only.
MOZ_ASSERT(minorState == State::Marking);
MOZ_ASSERT_IF(zone->isGCMarkingOrSweeping(), majorState == State::Marking);
checkAccess();
void* result;
if (IsLargeAllocSize(bytes)) {
result = allocLarge(bytes, nurseryOwned, true);
}
else if (IsSmallAllocSize(bytes)) {
result = allocSmall(bytes, nurseryOwned, true);
}
else {
result = allocMedium(bytes, nurseryOwned, true);
}
if (!result) {
return nullptr;
}
// Barrier to mark nursery-owned allocations that happen during collection. We
// don't need to do this for tenured-owned allocations because we don't sweep
// tenured-owned allocations that happened after the start of a major
// collection.
if (nurseryOwned) {
markNurseryOwnedAlloc(result, true);
}
return result;
}
inline Zone* LargeBuffer::zone() {
Zone* zone = zoneFromAnyThread();
MOZ_ASSERT(CurrentThreadCanAccessZone(zone));
return zone;
}
inline Zone* LargeBuffer::zoneFromAnyThread() {
return BufferChunk::from(this)->zone;
}
#ifdef XP_DARWIN
static inline void VirtualCopyPages(
void* dst,
const void* src, size_t bytes) {
MOZ_ASSERT((uintptr_t(dst) & PageMask) ==
0);
MOZ_ASSERT((uintptr_t(src) & PageMask) ==
0);
MOZ_ASSERT(bytes >= ChunkSize);
kern_return_t r = vm_copy(mach_task_self(), vm_address_t(src),
vm_size_t(bytes), vm_address_t(dst));
if (r != KERN_SUCCESS) {
MOZ_CRASH(
"vm_copy() failed");
}
}
#endif
void* BufferAllocator::realloc(
void* alloc, size_t bytes,
bool nurseryOwned) {
// Reallocate a buffer. This has the same semantics as standard libarary
// realloc: if |ptr| is null it creates a new allocation, and if it fails it
// returns |nullptr| and the original |ptr| is still valid.
checkAccess();
if (!alloc) {
return this->alloc(bytes, nurseryOwned);
}
MOZ_ASSERT(isNurseryOwned(alloc) == nurseryOwned);
MOZ_ASSERT_IF(zone->isGCMarkingOrSweeping(), majorState == State::Marking);
bytes = GetGoodAllocSize(bytes);
size_t currentBytes;
if (IsLargeAlloc(alloc)) {
LargeBuffer* buffer = lookupLargeBuffer(alloc);
currentBytes = buffer->allocBytes();
// We can shrink large allocations (on some platforms).
if (bytes < buffer->allocBytes() && IsLargeAllocSize(bytes)) {
if (shrinkLarge(buffer, bytes)) {
return alloc;
}
}
}
else if (IsMediumAlloc(alloc)) {
BufferChunk* chunk = BufferChunk::from(alloc);
MOZ_ASSERT(!chunk->isSmallBufferRegion(alloc));
currentBytes = chunk->allocBytes(alloc);
// We can grow or shrink medium allocations.
if (bytes < currentBytes && !IsSmallAllocSize(bytes)) {
if (shrinkMedium(alloc, bytes)) {
return alloc;
}
}
if (bytes > currentBytes && !IsLargeAllocSize(bytes)) {
if (growMedium(alloc, bytes)) {
return alloc;
}
}
}
else {
// TODO: Grow and shrink small allocations.
auto* region = SmallBufferRegion::from(alloc);
currentBytes = region->allocBytes(alloc);
}
if (bytes == currentBytes) {
return alloc;
}
void* newAlloc = this->alloc(bytes, nurseryOwned);
if (!newAlloc) {
return nullptr;
}
auto freeGuard = mozilla::MakeScopeExit([&]() { free(alloc); });
size_t bytesToCopy = std::min(bytes, currentBytes);
#ifdef XP_DARWIN
if (bytesToCopy >= ChunkSize) {
MOZ_ASSERT(IsLargeAlloc(alloc));
MOZ_ASSERT(IsLargeAlloc(newAlloc));
VirtualCopyPages(newAlloc, alloc, bytesToCopy);
return newAlloc;
}
#endif
memcpy(newAlloc, alloc, bytesToCopy);
return newAlloc;
}
void BufferAllocator::free(
void* alloc) {
MOZ_ASSERT(alloc);
checkAccess();
if (IsLargeAlloc(alloc)) {
freeLarge(alloc);
return;
}
if (IsMediumAlloc(alloc)) {
freeMedium(alloc);
return;
}
// Can't free small allocations.
}
/* static */
bool BufferAllocator::IsBufferAlloc(
void* alloc) {
// Precondition: |alloc| is a pointer to a buffer allocation, a GC thing or a
// direct nursery allocation returned by Nursery::allocateBuffer.
if (IsLargeAlloc(alloc)) {
return true;
}
ChunkBase* chunk = detail::GetGCAddressChunkBase(alloc);
return chunk->getKind() == ChunkKind::Buffers;
}
#ifdef DEBUG
bool BufferAllocator::hasAlloc(
void* alloc) {
MOZ_ASSERT(IsBufferAlloc(alloc));
if (IsLargeAlloc(alloc)) {
MaybeLock lock;
if (needLockToAccessBufferMap()) {
lock.emplace(runtime());
}
auto ptr = runtime()->largeAllocMap.ref().readonlyThreadsafeLookup(alloc);
return ptr.found();
}
BufferChunk* chunk = BufferChunk::from(alloc);
return chunk->zone == zone;
}
#endif
size_t BufferAllocator::getAllocSize(
void* alloc) {
checkAccess();
if (!alloc) {
return 0;
}
if (IsLargeAlloc(alloc)) {
LargeBuffer* buffer = lookupLargeBuffer(alloc);
return buffer->allocBytes();
}
if (IsSmallAlloc(alloc)) {
auto* region = SmallBufferRegion::from(alloc);
return region->allocBytes(alloc);
}
MOZ_ASSERT(IsMediumAlloc(alloc));
BufferChunk* chunk = BufferChunk::from(alloc);
return chunk->allocBytes(alloc);
}
bool BufferAllocator::isNurseryOwned(
void* alloc) {
if (IsLargeAlloc(alloc)) {
LargeBuffer* buffer = lookupLargeBuffer(alloc);
return buffer->isNurseryOwned;
}
if (IsSmallAlloc(alloc)) {
auto* region = SmallBufferRegion::from(alloc);
return region->isNurseryOwned(alloc);
}
BufferChunk* chunk = BufferChunk::from(alloc);
return chunk->isNurseryOwned(alloc);
}
void BufferAllocator::markNurseryOwnedAlloc(
void* alloc,
bool nurseryOwned) {
MOZ_ASSERT(alloc);
MOZ_ASSERT(isNurseryOwned(alloc));
MOZ_ASSERT(minorState == State::Marking);
if (IsLargeAlloc(alloc)) {
LargeBuffer* buffer = lookupLargeBuffer(alloc);
MOZ_ASSERT(buffer->zone() == zone);
markLargeNurseryOwnedBuffer(buffer, nurseryOwned);
return;
}
if (IsSmallAlloc(alloc)) {
markSmallNurseryOwnedBuffer(alloc, nurseryOwned);
return;
}
MOZ_ASSERT(IsMediumAlloc(alloc));
markMediumNurseryOwnedBuffer(alloc, nurseryOwned);
}
void BufferAllocator::markSmallNurseryOwnedBuffer(
void* alloc,
bool nurseryOwned) {
#ifdef DEBUG
BufferChunk* chunk = BufferChunk::from(alloc);
MOZ_ASSERT(chunk->zone == zone);
MOZ_ASSERT(chunk->hasNurseryOwnedAllocs);
#endif
auto* region = SmallBufferRegion::from(alloc);
MOZ_ASSERT(region->hasNurseryOwnedAllocs());
MOZ_ASSERT(region->isNurseryOwned(alloc));
if (region->isMarked(alloc)) {
MOZ_ASSERT(nurseryOwned);
return;
}
if (!nurseryOwned) {
region->setNurseryOwned(alloc,
false);
// If all nursery owned allocations in the region were tenured then
// chunk->isNurseryOwned(region) will now be stale. It will be updated when
// the region is swept.
return;
}
region->setMarked(alloc);
}
void BufferAllocator::markMediumNurseryOwnedBuffer(
void* alloc,
bool nurseryOwned) {
BufferChunk* chunk = BufferChunk::from(alloc);
MOZ_ASSERT(chunk->zone == zone);
MOZ_ASSERT(chunk->hasNurseryOwnedAllocs);
MOZ_ASSERT(chunk->isAllocated(alloc));
MOZ_ASSERT(chunk->isNurseryOwned(alloc));
if (chunk->isMarked(alloc)) {
MOZ_ASSERT(nurseryOwned);
return;
}
size_t size = chunk->allocBytes(alloc);
increaseHeapSize(size, nurseryOwned,
false,
false);
if (!nurseryOwned) {
// Change the allocation to a tenured owned one. This prevents sweeping in a
// minor collection.
chunk->setNurseryOwned(alloc,
false);
return;
}
chunk->setMarked(alloc);
}
void BufferAllocator::markLargeNurseryOwnedBuffer(LargeBuffer* buffer,
bool nurseryOwned) {
MOZ_ASSERT(buffer->isNurseryOwned);
// The buffer metadata is held in a small buffer. Check whether it has already
// been marked.
auto* region = SmallBufferRegion::from(buffer);
MOZ_ASSERT(region->isNurseryOwned(buffer));
if (region->isMarked(buffer)) {
MOZ_ASSERT(nurseryOwned);
return;
}
markSmallNurseryOwnedBuffer(buffer, nurseryOwned);
largeNurseryAllocsToSweep.ref().remove(buffer);
size_t usableSize = buffer->allocBytes();
increaseHeapSize(usableSize, nurseryOwned,
false,
false);
if (!nurseryOwned) {
buffer->isNurseryOwned =
false;
buffer->allocatedDuringCollection = majorState != State::NotCollecting;
largeTenuredAllocs.ref().pushBack(buffer);
return;
}
largeNurseryAllocs.ref().pushBack(buffer);
}
bool BufferAllocator::isMarkedBlack(
void* alloc) {
checkMainThread();
if (IsLargeAlloc(alloc)) {
// The buffer metadata is held in a small buffer.
alloc = lookupLargeBuffer(alloc);
}
else if (!IsSmallAlloc(alloc)) {
MOZ_ASSERT(IsMediumAlloc(alloc));
BufferChunk* chunk = BufferChunk::from(alloc);
return chunk->isMarked(alloc);
}
auto* region = SmallBufferRegion::from(alloc);
return region->isMarked(alloc);
}
/* static */
void* BufferAllocator::TraceEdge(JSTracer* trc,
void** bufferp,
const char* name) {
// Buffers are conceptually part of the owning cell and are not reported to
// the tracer.
// TODO: This should be unified with the rest of the tracing system.
MOZ_ASSERT(bufferp);
void* buffer = *bufferp;
#ifdef JS_GC_CONCURRENT_MARKING
// Conservatively perform an atomic load even when marking is not concurrent.
buffer = __atomic_load_n(bufferp, __ATOMIC_RELAXED);
#else
buffer = *bufferp;
#endif
if (!buffer) {
return nullptr;
}
if (!IsLargeAlloc(buffer) &&
js::gc::detail::GetGCAddressChunkBase(buffer)->isNurseryChunk()) {
// JSObject slots and elements can be allocated in the nursery and this is
// handled separately.
return buffer;
}
MOZ_ASSERT(IsBufferAlloc(buffer));
if (MOZ_UNLIKELY(IsLargeAlloc(buffer))) {
TraceLargeAlloc(trc, bufferp, name);
return buffer;
}
BufferChunk* chunk = BufferChunk::from(buffer);
BufferAllocator& allocator = chunk->zone->bufferAllocator;
if (IsSmallAlloc(buffer)) {
allocator.traceSmallAlloc(trc, bufferp, name);
return buffer;
}
allocator.traceMediumAlloc(trc, bufferp, name);
return buffer;
}
void BufferAllocator::traceSmallAlloc(JSTracer* trc,
void** allocp,
const char* name) {
void* alloc = *allocp;
auto* region = SmallBufferRegion::from(alloc);
if (trc->isTenuringTracer()) {
if (region->isNurseryOwned(alloc)) {
bool nurseryOwned = TenuringTracer::From(trc)->sourceIsInNursery.value();
markSmallNurseryOwnedBuffer(alloc, nurseryOwned);
}
return;
}
if (trc->isMarkingTracer()) {
if (zone->isGCMarking() && !region->isNurseryOwned(alloc)) {
markSmallTenuredAlloc(alloc);
}
return;
}
}
void BufferAllocator::traceMediumAlloc(JSTracer* trc,
void** allocp,
const char* name) {
void* alloc = *allocp;
BufferChunk* chunk = BufferChunk::from(alloc);
if (trc->isTenuringTracer()) {
if (chunk->isNurseryOwned(alloc)) {
bool nurseryOwned = TenuringTracer::From(trc)->sourceIsInNursery.value();
markMediumNurseryOwnedBuffer(alloc, nurseryOwned);
}
return;
}
if (trc->isMarkingTracer()) {
if (zone->isGCMarking() && !chunk->isNurseryOwned(alloc)) {
markMediumTenuredAlloc(alloc);
}
return;
}
}
/* static */
void BufferAllocator::TraceLargeAlloc(JSTracer* trc,
void** allocp,
const char* name) {
void* alloc = *allocp;
BufferAllocatorRuntime* runtime = &trc->runtime()->gc.bufferRuntime();
LargeBuffer* buffer = runtime->lookupLargeBuffer(alloc);
Zone* zone = buffer->zoneFromAnyThread();
// May be parallel marking here.
zone->bufferAllocator.traceLargeBuffer(trc, buffer, name);
}
void BufferAllocator::traceLargeBuffer(JSTracer* trc, LargeBuffer* buffer,
const char* name) {
if (trc->isTenuringTracer()) {
if (buffer->isNurseryOwned) {
bool nurseryOwned = TenuringTracer::From(trc)->sourceIsInNursery.value();
markLargeNurseryOwnedBuffer(buffer, nurseryOwned);
}
return;
}
if (trc->isMarkingTracer()) {
if (zone->isGCMarking() && !buffer->isNurseryOwned) {
markLargeTenuredBuffer(buffer);
}
return;
}
}
bool BufferAllocator::markTenuredAlloc(
void* alloc) {
MOZ_ASSERT(alloc);
MOZ_ASSERT(!isNurseryOwned(alloc));
if (IsLargeAlloc(alloc)) {
LargeBuffer* buffer = lookupLargeBuffer(alloc);
return markLargeTenuredBuffer(buffer);
}
if (IsSmallAlloc(alloc)) {
return markSmallTenuredAlloc(alloc);
}
return markMediumTenuredAlloc(alloc);
}
bool BufferAllocator::markSmallTenuredAlloc(
void* alloc) {
auto* chunk = BufferChunk::from(alloc);
if (chunk->allocatedDuringCollection) {
// Will not be swept, already counted as marked.
return false;
}
auto* region = SmallBufferRegion::from(alloc);
MOZ_ASSERT(region->isAllocated(alloc));
return region->setMarked(alloc);
}
bool BufferAllocator::markMediumTenuredAlloc(
void* alloc) {
BufferChunk* chunk = BufferChunk::from(alloc);
MOZ_ASSERT(chunk->isAllocated(alloc));
if (chunk->allocatedDuringCollection) {
// Will not be swept, already counted as marked.
return false;
}
return chunk->setMarked(alloc);
}
void BufferAllocator::startMinorCollection(MaybeLock& lock) {
checkMainThread();
maybeMergeSweptData(lock);
#ifdef DEBUG
MOZ_ASSERT(minorState == State::NotCollecting);
if (majorState == State::NotCollecting) {
if (gc->hasZealMode(ZealMode::CheckHeapBeforeMinorGC)) {
// This is too expensive to run on every minor GC.
checkGCStateNotInUse(lock);
}
}
#endif
// Large allocations that are marked when tracing the nursery will be moved
// back to the main list.
MOZ_ASSERT(largeNurseryAllocsToSweep.ref().isEmpty());
std::swap(largeNurseryAllocs.ref(), largeNurseryAllocsToSweep.ref());
minorState = State::Marking;
}
bool BufferAllocator::startMinorSweeping() {
// Called during minor GC. Operates on the active allocs/chunks lists. The 'to
// sweep' lists do not contain nursery owned allocations.
#ifdef DEBUG
checkMainThread();
MOZ_ASSERT(minorState == State::Marking);
{
AutoLock lock(runtime());
MOZ_ASSERT(!minorSweepingFinished);
MOZ_ASSERT(sweptMixedChunks.ref().isEmpty());
}
for (LargeBuffer* buffer : largeNurseryAllocs.ref()) {
MOZ_ASSERT(buffer->isNurseryOwned);
}
for (LargeBuffer* buffer : largeNurseryAllocsToSweep.ref()) {
MOZ_ASSERT(buffer->isNurseryOwned);
}
#endif
// Check whether there are any medium chunks containing nursery owned
// allocations that need to be swept.
if (mixedChunks.ref().isEmpty() && availableMixedChunks.ref().isEmpty() &&
largeNurseryAllocsToSweep.ref().isEmpty()) {
// Nothing more to do. Don't transition to sweeping state.
minorState = State::NotCollecting;
return false;
}
#ifdef DEBUG
for (BufferChunk* chunk : mixedChunks.ref()) {
MOZ_ASSERT(!chunk->ownsFreeLists);
chunk->freeLists.ref().assertEmpty();
}
#endif
// Move free regions in |tenuredChunks| out of |freeLists| and into their
// respective chunk header. Discard free regions in |mixedChunks| which will
// be rebuilt by sweeping.
//
// This is done for |tenuredChunks| too in order to reduce the number of free
// regions we need to process here on the next minor GC.
//
// Some possibilities to make this more efficient are:
// - have separate free lists for nursery/tenured chunks
// - keep free regions at different ends of the free list depending on chunk
// kind
freeLists.ref().forEachRegion(
[](FreeList& list, size_t sizeClass, FreeRegion* region) {
BufferChunk* chunk = BufferChunk::from(region);
if (!chunk->hasNurseryOwnedAllocs) {
list.remove(region);
chunk->freeLists.ref().pushBack(sizeClass, region);
}
});
freeLists.ref().clear();
// Set the flag to indicate all tenured chunks now own their free regions.
for (BufferChunk* chunk : tenuredChunks.ref()) {
MOZ_ASSERT(!chunk->hasNurseryOwnedAllocs);
chunk->ownsFreeLists = true;
}
// Move all mixed chunks to the list of chunks to sweep.
mixedChunksToSweep.ref() = std::move(mixedChunks.ref());
mixedChunksToSweep.ref().append(
availableMixedChunks.ref().extractAllChunks());
// Move all tenured chunks to |availableTenuredChunks|.
while (BufferChunk* chunk = tenuredChunks.ref().popFirst()) {
availableTenuredChunks.ref().pushBack(chunk);
}
minorState = State::Sweeping;
runtime()->incSweepCount();
return true;
}
struct LargeAllocToFree {
size_t bytes;
LargeAllocToFree* next = nullptr;
explicit LargeAllocToFree(size_t bytes) : bytes(bytes) {}
};
static void PushLargeAllocToFree(LargeAllocToFree** listHead,
LargeBuffer* buffer) {
auto* alloc =
new (buffer->data()) LargeAllocToFree(buffer->bytes);
alloc->next = *listHead;
*listHead = alloc;
}
static void FreeLargeAllocs(LargeAllocToFree* listHead) {
while (listHead) {
LargeAllocToFree* alloc = listHead;
LargeAllocToFree* next = alloc->next;
UnmapPages(alloc, alloc->bytes);
listHead = next;
}
}
void BufferAllocator::sweepForMinorCollection() {
// Called on a background thread.
MOZ_ASSERT(minorState.refNoCheck() == State::Sweeping);
{
AutoLock lock(runtime());
MOZ_ASSERT(sweptMixedChunks.ref().isEmpty());
}
// Bug 1961749: Freeing large buffers can be slow so it might be worth
// splitting sweeping into two phases so that all zones get their medium
// buffers swept and made available for allocation before any large buffers
// are freed.
// Freeing large buffers may be slow, so leave that till the end. However
// large buffer metadata is stored in small buffers so form a list of large
// buffers to free before sweeping small buffers.
LargeAllocToFree* largeAllocsToFree = nullptr;
while (!largeNurseryAllocsToSweep.ref().isEmpty()) {
LargeBuffer* buffer = largeNurseryAllocsToSweep.ref().popFirst();
PushLargeAllocToFree(&largeAllocsToFree, buffer);
MaybeLock lock(std::in_place, runtime());
unregisterLarge(buffer, true, lock);
}
while (!mixedChunksToSweep.ref().isEmpty()) {
BufferChunk* chunk = mixedChunksToSweep.ref().popFirst();
if (sweepChunk(chunk, SweepKind::Nursery,
false)) {
{
AutoLock lock(runtime());
sweptMixedChunks.ref().pushBack(chunk);
}
// Signal to the main thread that swept data is available by setting this
// relaxed atomic flag.
hasSweepDataToMerge = true;
}
}
// Unmap large buffers.
FreeLargeAllocs(largeAllocsToFree);
// Signal to main thread to update minorState.
{
AutoLock lock(runtime());
MOZ_ASSERT(!minorSweepingFinished);
minorSweepingFinished = true;
hasSweepDataToMerge = true;
}
}
void BufferAllocator::startMajorCollection(MaybeLock& lock) {
checkMainThread();
maybeMergeSweptData(lock);
#ifdef DEBUG
MOZ_ASSERT(majorState == State::NotCollecting);
checkGCStateNotInUse(lock);
// Everything is tenured since we just evicted the nursery, or will be by the
// time minor sweeping finishes.
MOZ_ASSERT(mixedChunks.ref().isEmpty());
MOZ_ASSERT(availableMixedChunks.ref().isEmpty());
MOZ_ASSERT(largeNurseryAllocs.ref().isEmpty());
#endif
#ifdef DEBUG
for (BufferChunk* chunk : tenuredChunks.ref()) {
MOZ_ASSERT(!chunk->ownsFreeLists);
chunk->freeLists.ref().assertEmpty();
}
#endif
largeTenuredAllocsToSweep.ref() = std::move(largeTenuredAllocs.ref());
// Move free regions that need to be swept to the free lists in their
// respective chunks.
freeLists.ref().forEachRegion(
[](FreeList& list, size_t sizeClass, FreeRegion* region) {
BufferChunk* chunk = BufferChunk::from(region);
MOZ_ASSERT(!chunk->hasNurseryOwnedAllocs);
list.remove(region);
chunk->freeLists.ref().pushBack(sizeClass, region);
});
// Move all tenured chunks to |availableTenuredChunks|.
while (BufferChunk* chunk = tenuredChunks.ref().popFirst()) {
MOZ_ASSERT(!chunk->hasNurseryOwnedAllocs);
chunk->ownsFreeLists = true;
availableTenuredChunks.ref().pushBack(chunk);
}
// Move all available tenured chunks to the sweep list.
tenuredChunksToSweep.ref() = availableTenuredChunks.ref().extractAllChunks();
if (minorState == State::Sweeping) {
// Ensure swept nursery chunks are moved to the tenuredChunks lists in
// mergeSweptData.
majorStartedWhileMinorSweeping = true;
}
#ifdef DEBUG
MOZ_ASSERT(tenuredChunks.ref().isEmpty());
MOZ_ASSERT(availableTenuredChunks.ref().isEmpty());
freeLists.ref().assertEmpty();
MOZ_ASSERT(largeTenuredAllocs.ref().isEmpty());
for (BufferChunk* chunk : tenuredChunksToSweep.ref()) {
MOZ_ASSERT(chunk->ownsFreeLists);
}
#endif
majorState = State::Marking;
}
void BufferAllocator::startMajorSweeping(MaybeLock& lock) {
// Called when a zone transitions from marking to sweeping.
#ifdef DEBUG
checkMainThread();
MOZ_ASSERT(majorState == State::Marking);
MOZ_ASSERT(zone->isGCFinished());
MOZ_ASSERT(!majorSweepingFinished.refNoCheck());
#endif
maybeMergeSweptData(lock);
MOZ_ASSERT(!majorStartedWhileMinorSweeping);
clearMarkBitsInStolenChunks();
if (minorState == State::Sweeping) {
majorSweepingStartedWhileMinorSweeping = true;
}
majorState = State::Sweeping;
runtime()->incSweepCount();
}
void BufferAllocator::sweepForMajorCollection(
bool shouldDecommit) {
// Called on a background thread.
MOZ_ASSERT(majorState.refNoCheck() == State::Sweeping);
// Sweep large allocs first since they rely on the mark bits of their
// corresponding LargeBuffer structures which are stored small buffers.
//
// It's tempting to try and optimize this by moving the allocations between
// lists when they are marked, in the same way as for nursery sweeping. This
// would require synchronizing the list modification when marking in parallel,
// so is probably not worth it.
LargeAllocList sweptLargeAllocs;
LargeAllocToFree* largeAllocsToFree = nullptr;
while (!largeTenuredAllocsToSweep.ref().isEmpty()) {
LargeBuffer* buffer = largeTenuredAllocsToSweep.ref().popFirst();
if (isLargeTenuredMarked(buffer)) {
buffer->isMarked =
false;
sweptLargeAllocs.pushBack(buffer);
}
else {
PushLargeAllocToFree(&largeAllocsToFree, buffer);
MaybeLock lock(std::in_place, runtime());
unregisterLarge(buffer, true, lock);
}
}
while (!tenuredChunksToSweep.ref().isEmpty()) {
BufferChunk* chunk = tenuredChunksToSweep.ref().popFirst();
if (sweepChunk(chunk, SweepKind::Tenured, shouldDecommit)) {
{
AutoLock lock(runtime());
sweptTenuredChunks.ref().pushBack(chunk);
}
// Signal to the main thread that swept data is available by setting this
// relaxed atomic flag.
hasSweepDataToMerge = true;
}
}
// Unmap large buffers.
//
// Bug 1961749: This could possibly run after signalling sweeping is finished
// or concurrently with other sweeping.
FreeLargeAllocs(largeAllocsToFree);
AutoLock lock(runtime());
sweptLargeTenuredAllocs.ref() = std::move(sweptLargeAllocs);
// Signal to main thread to update majorState.
MOZ_ASSERT(!majorSweepingFinished);
majorSweepingFinished = true;
}
void BufferAllocator::finishMajorCollection(
const AutoLock& lock) {
// This can be called in any state:
//
// - NotCollecting: after major sweeping has finished and the state has been
// reset to NotCollecting in mergeSweptData.
//
// - Marking: if collection was aborted and startMajorSweeping was not
// called.
//
// - Sweeping: if sweeping has finished and mergeSweptData has not been
// called yet.
checkMainThread();
MOZ_ASSERT_IF(majorState == State::Sweeping, majorSweepingFinished);
if (minorState == State::Sweeping || majorState == State::Sweeping) {
mergeSweptData(lock);
}
if (majorState == State::Marking) {
abortMajorSweeping(lock);
}
#ifdef DEBUG
checkGCStateNotInUse(lock);
#endif
}
void BufferAllocator::abortMajorSweeping(
const AutoLock& lock) {
// We have aborted collection without sweeping this zone. Restore or rebuild
// the original state.
#ifdef DEBUG
checkMainThread();
MOZ_ASSERT(majorState == State::Marking);
MOZ_ASSERT(sweptTenuredChunks.ref().isEmpty());
for (
auto chunk = availableTenuredChunks.ref().chunkIter(); !chunk.done();
chunk.next()) {
MOZ_ASSERT(chunk->allocatedDuringCollection);
}
#endif
clearMarkBitsInStolenChunks();
clearAllocatedDuringCollectionState(lock);
if (minorState == State::Sweeping) {
// If we are minor sweeping then chunks with allocatedDuringCollection set
// may be present in |mixedChunksToSweep|. Set a flag so these are cleared
// when they are merged later.
majorFinishedWhileMinorSweeping = true;
}
// Clear mark bits for chunks we didn't end up sweeping.
while (BufferChunk* chunk = tenuredChunksToSweep.ref().popFirst()) {
MOZ_ASSERT(chunk->ownsFreeLists);
clearChunkMarkBits(chunk);
availableTenuredChunks.ref().pushBack(chunk);
}
// Clear mark bits for large allocations we didn't end up sweeping.
while (LargeBuffer* buffer = largeTenuredAllocsToSweep.ref().popLast()) {
buffer->isMarked =
false;
largeTenuredAllocs.ref().pushFront(buffer);
}
majorState = State::NotCollecting;
}
void BufferAllocator::clearMarkBitsInStolenChunks() {
// Clear mark bits for any chunks stolen from the sweep list. These are in the
// current or available lists and have stolenFromSweepList == true. There
// shouldn't be too many of these as everything got moved to the sweep list at
// the start of marking.
//
// We also need to do the same for chunks coming back from minor sweeping when
// majorSweepingStartedWhileMinorSweeping is set.
for (BufferChunkList* list : {&mixedChunks.ref(), &tenuredChunks.ref()}) {
for (BufferChunk* chunk : *list) {
chunk->clearMarkBitsIfStolenChunk();
}
}
for (ChunkLists* lists :
{&availableMixedChunks.ref(), &availableTenuredChunks.ref()}) {
for (
auto chunk = lists->chunkIter(); !chunk.done(); chunk.next()) {
chunk->clearMarkBitsIfStolenChunk();
}
}
}
void BufferChunk::clearMarkBitsIfStolenChunk() {
if (stolenFromSweepList) {
clearMarkBits();
stolenFromSweepList =
false;
}
}
void BufferAllocator::clearAllocatedDuringCollectionState(
const AutoLock& lock) {
#ifdef DEBUG
// This flag is not set for large nursery-owned allocations.
for (LargeBuffer* buffer : largeNurseryAllocs.ref()) {
MOZ_ASSERT(!buffer->allocatedDuringCollection);
}
#endif
ClearAllocatedDuringCollection(mixedChunks.ref());
ClearAllocatedDuringCollection(availableMixedChunks.ref());
ClearAllocatedDuringCollection(tenuredChunks.ref());
ClearAllocatedDuringCollection(availableTenuredChunks.ref());
ClearAllocatedDuringCollection(largeTenuredAllocs.ref());
}
/* static */
void BufferAllocator::ClearAllocatedDuringCollection(ChunkLists& chunks) {
for (
auto chunk = chunks.chunkIter(); !chunk.done(); chunk.next()) {
chunk->allocatedDuringCollection =
false;
}
}
/* static */
void BufferAllocator::ClearAllocatedDuringCollection(BufferChunkList& list) {
for (
auto* chunk : list) {
chunk->allocatedDuringCollection =
false;
}
}
/* static */
void BufferAllocator::ClearAllocatedDuringCollection(LargeAllocList& list) {
for (
auto* element : list) {
element->allocatedDuringCollection =
false;
}
}
void BufferAllocator::maybeMergeSweptData() {
if (minorState == State::Sweeping || majorState == State::Sweeping) {
mergeSweptData();
}
}
void BufferAllocator::mergeSweptData() {
AutoLock lock(runtime());
mergeSweptData(lock);
}
void BufferAllocator::maybeMergeSweptData(MaybeLock& lock) {
if (minorState == State::Sweeping || majorState == State::Sweeping) {
if (lock.isNothing()) {
lock.emplace(runtime());
}
mergeSweptData(lock.ref());
}
}
void BufferAllocator::mergeSweptData(
const AutoLock& lock) {
checkAccess();
MOZ_ASSERT(minorState == State::Sweeping || majorState == State::Sweeping);
if (majorSweepingFinished) {
clearAllocatedDuringCollectionState(lock);
if (minorState == State::Sweeping) {
majorFinishedWhileMinorSweeping = true;
}
}
// Merge swept chunks that previously contained nursery owned allocations. If
// semispace nursery collection is in use then these chunks may contain both
// nursery and tenured-owned allocations, otherwise all allocations will be
// tenured-owned.
while (!sweptMixedChunks.ref().isEmpty()) {
BufferChunk* chunk = sweptMixedChunks.ref().popLast();
MOZ_ASSERT(chunk->ownsFreeLists);
MOZ_ASSERT(chunk->hasNurseryOwnedAllocs);
chunk->hasNurseryOwnedAllocs = chunk->hasNurseryOwnedAllocsAfterSweep;
MOZ_ASSERT_IF(
majorState == State::NotCollecting && !majorFinishedWhileMinorSweeping,
!chunk->allocatedDuringCollection);
if (majorSweepingStartedWhileMinorSweeping) {
if (chunk->stolenFromSweepList) {
clearChunkMarkBits(chunk);
chunk->stolenFromSweepList =
false;
}
}
if (majorFinishedWhileMinorSweeping) {
chunk->allocatedDuringCollection =
false;
}
size_t sizeClass = chunk->sizeClassForAvailableLists();
if (chunk->hasNurseryOwnedAllocs) {
availableMixedChunks.ref().pushFront(sizeClass, chunk);
}
else if (majorStartedWhileMinorSweeping) {
tenuredChunksToSweep.ref().pushFront(chunk);
}
else {
availableTenuredChunks.ref().pushFront(sizeClass, chunk);
}
}
// Merge swept chunks that did not contain nursery owned allocations.
#ifdef DEBUG
for (BufferChunk* chunk : sweptTenuredChunks.ref()) {
MOZ_ASSERT(!chunk->hasNurseryOwnedAllocs);
MOZ_ASSERT(!chunk->hasNurseryOwnedAllocsAfterSweep);
MOZ_ASSERT(!chunk->allocatedDuringCollection);
}
#endif
while (BufferChunk* chunk = sweptTenuredChunks.ref().popFirst()) {
size_t sizeClass = chunk->sizeClassForAvailableLists();
availableTenuredChunks.ref().pushFront(sizeClass, chunk);
}
largeTenuredAllocs.ref().prepend(std::move(sweptLargeTenuredAllocs.ref()));
hasSweepDataToMerge =
false;
if (minorSweepingFinished) {
MOZ_ASSERT(minorState == State::Sweeping);
runtime()->decSweepCount();
minorState = State::NotCollecting;
minorSweepingFinished =
false;
majorStartedWhileMinorSweeping =
false;
majorSweepingStartedWhileMinorSweeping =
false;
majorFinishedWhileMinorSweeping =
false;
#ifdef DEBUG
for (BufferChunk* chunk : mixedChunks.ref()) {
verifyChunk(chunk, true);
}
for (BufferChunk* chunk : tenuredChunks.ref()) {
verifyChunk(chunk,
false);
}
#endif
}
if (majorSweepingFinished) {
MOZ_ASSERT(majorState == State::Sweeping);
runtime()->decSweepCount();
majorState = State::NotCollecting;
majorSweepingFinished =
false;
MOZ_ASSERT(tenuredChunksToSweep.ref().isEmpty());
}
}
void BufferAllocator::clearMarkStateAfterBarrierVerification() {
checkMainThread();
MOZ_ASSERT(!zone->wasGCStarted());
maybeMergeSweptData();
MOZ_ASSERT(minorState == State::NotCollecting);
MOZ_ASSERT(majorState == State::NotCollecting);
for (
auto* chunks : {&mixedChunks.ref(), &tenuredChunks.ref()}) {
for (
auto* chunk : *chunks) {
clearChunkMarkBits(chunk);
}
}
for (
auto* chunks :
{&availableMixedChunks.ref(), &availableTenuredChunks.ref()}) {
for (
auto chunk = chunks->chunkIter(); !chunk.done(); chunk.next()) {
clearChunkMarkBits(chunk);
}
}
#ifdef DEBUG
checkGCStateNotInUse();
#endif
}
void BufferAllocator::clearChunkMarkBits(BufferChunk* chunk) {
checkAccess();
chunk->clearMarkBits();
}
void BufferChunk::clearMarkBits() {
markBits.ref().clear();
for (
auto iter = smallRegionIter(); !iter.done(); iter.next()) {
SmallBufferRegion* region = iter.get();
region->markBits.ref().clear();
}
}
bool BufferAllocator::isPointerWithinBuffer(
void* ptr) {
checkMainThread();
maybeMergeSweptData();
MOZ_ASSERT(mixedChunksToSweep.ref().isEmpty());
MOZ_ASSERT_IF(majorState != State::Marking,
tenuredChunksToSweep.ref().isEmpty());
for (
const auto* chunks : {&mixedChunks.ref(), &tenuredChunks.ref(),
&tenuredChunksToSweep.ref()}) {
for (
auto* chunk : *chunks) {
if (chunk->isPointerWithinAllocation(ptr)) {
return true;
}
}
}
for (
auto* chunks :
{&availableMixedChunks.ref(), &availableTenuredChunks.ref()}) {
for (
auto chunk = chunks->chunkIter(); !chunk.done(); chunk.next()) {
if (chunk->isPointerWithinAllocation(ptr)) {
return true;
}
}
}
// Note we cannot safely access data that is being swept on another thread.
for (
const auto* allocs :
{&largeNurseryAllocs.ref(), &largeTenuredAllocs.ref()}) {
for (
auto* alloc : *allocs) {
if (alloc->isPointerWithinAllocation(ptr)) {
return true;
}
}
}
return false;
}
bool BufferChunk::isPointerWithinAllocation(
void* ptr)
const {
uintptr_t offset = uintptr_t(ptr) - uintptr_t(this);
if (offset >= ChunkSize || offset < FirstMediumAllocOffset) {
return false;
}
if (smallRegionBitmap.ref().getBit(offset / SmallRegionSize)) {
auto* region = SmallBufferRegion::from(ptr);
return region->isPointerWithinAllocation(ptr);
}
uintptr_t allocOffset =
findPrevAllocated(RoundDown(offset, MinMediumAllocSize));
MOZ_ASSERT(allocOffset <= ChunkSize);
if (allocOffset == ChunkSize) {
return false;
}
const void* alloc = ptrFromOffset(allocOffset);
size_t size = allocBytes(alloc);
return offset < allocOffset + size;
}
bool SmallBufferRegion::isPointerWithinAllocation(
void* ptr)
const {
uintptr_t offset = uintptr_t(ptr) - uintptr_t(this);
MOZ_ASSERT(offset < SmallRegionSize);
uintptr_t allocOffset =
findPrevAllocated(RoundDown(offset, SmallAllocGranularity));
MOZ_ASSERT(allocOffset <= SmallRegionSize);
if (allocOffset == SmallRegionSize) {
return false;
}
const void* alloc = ptrFromOffset(allocOffset);
size_t size = allocBytes(alloc);
return offset < allocOffset + size;
}
bool LargeBuffer::isPointerWithinAllocation(
void* ptr)
const {
return uintptr_t(ptr) - uintptr_t(alloc) < bytes;
}
#ifdef DEBUG
void BufferAllocator::checkGCStateNotInUse() {
maybeMergeSweptData();
AutoLock lock(runtime());
// Some fields are protected by this lock.
checkGCStateNotInUse(lock);
}
void BufferAllocator::checkGCStateNotInUse(MaybeLock& maybeLock) {
if (maybeLock.isNothing()) {
// Some fields are protected by this lock.
maybeLock.emplace(runtime());
}
checkGCStateNotInUse(maybeLock.ref());
}
void BufferAllocator::checkGCStateNotInUse(
const AutoLock& lock) {
MOZ_ASSERT(majorState == State::NotCollecting);
bool isNurserySweeping = minorState == State::Sweeping;
checkChunkListGCStateNotInUse(mixedChunks.ref(), true,
false,
false);
checkChunkListGCStateNotInUse(tenuredChunks.ref(),
false,
false,
false);
checkChunkListsGCStateNotInUse(availableMixedChunks.ref(), true,
false);
checkChunkListsGCStateNotInUse(availableTenuredChunks.ref(),
false,
false);
if (isNurserySweeping) {
checkChunkListGCStateNotInUse(sweptMixedChunks.ref(), true,
majorFinishedWhileMinorSweeping, true);
checkChunkListGCStateNotInUse(sweptTenuredChunks.ref(),
false,
false, true);
}
else {
MOZ_ASSERT(mixedChunksToSweep.ref().isEmpty());
MOZ_ASSERT(largeNurseryAllocsToSweep.ref().isEmpty());
MOZ_ASSERT(sweptMixedChunks.ref().isEmpty());
MOZ_ASSERT(sweptTenuredChunks.ref().isEmpty());
MOZ_ASSERT(!majorStartedWhileMinorSweeping);
MOZ_ASSERT(!majorSweepingStartedWhileMinorSweeping);
MOZ_ASSERT(!majorFinishedWhileMinorSweeping);
MOZ_ASSERT(!hasSweepDataToMerge);
MOZ_ASSERT(!minorSweepingFinished);
MOZ_ASSERT(!majorSweepingFinished);
}
MOZ_ASSERT(tenuredChunksToSweep.ref().isEmpty());
checkAllocListGCStateNotInUse(largeNurseryAllocs.ref(), true);
checkAllocListGCStateNotInUse(largeTenuredAllocs.ref(),
false);
MOZ_ASSERT(largeTenuredAllocsToSweep.ref().isEmpty());
MOZ_ASSERT(sweptLargeTenuredAllocs.ref().isEmpty());
}
void BufferAllocator::checkChunkListsGCStateNotInUse(
ChunkLists& chunkLists,
bool hasNurseryOwnedAllocs,
bool allowAllocatedDuringCollection) {
for (
auto chunk = chunkLists.chunkIter(); !chunk.done(); chunk.next()) {
checkChunkGCStateNotInUse(chunk, allowAllocatedDuringCollection, true);
verifyChunk(chunk, hasNurseryOwnedAllocs);
MOZ_ASSERT(chunk->ownsFreeLists);
size_t sizeClass = chunk.getSizeClass();
MOZ_ASSERT(chunk->sizeClassForAvailableLists() == sizeClass);
MOZ_ASSERT_IF(sizeClass != FullChunkSizeClass,
chunk->freeLists.ref().hasSizeClass(sizeClass));
}
}
void BufferAllocator::checkChunkListGCStateNotInUse(
BufferChunkList& chunks,
bool hasNurseryOwnedAllocs,
bool allowAllocatedDuringCollection,
bool allowFreeLists) {
for (BufferChunk* chunk : chunks) {
checkChunkGCStateNotInUse(chunk, allowAllocatedDuringCollection,
allowFreeLists);
verifyChunk(chunk, hasNurseryOwnedAllocs);
}
}
void BufferAllocator::checkChunkGCStateNotInUse(
BufferChunk* chunk,
bool allowAllocatedDuringCollection,
bool allowFreeLists) {
MOZ_ASSERT_IF(!allowAllocatedDuringCollection,
!chunk->allocatedDuringCollection);
MOZ_ASSERT(!chunk->stolenFromSweepList);
MOZ_ASSERT(chunk->markBits.ref().isEmpty());
for (
auto iter = chunk->smallRegionIter(); !iter.done(); iter.next()) {
SmallBufferRegion* region = iter.get();
MOZ_ASSERT(region->markBits.ref().isEmpty());
}
MOZ_ASSERT(allowFreeLists == chunk->ownsFreeLists);
if (!chunk->ownsFreeLists) {
chunk->freeLists.ref().assertEmpty();
}
}
void BufferAllocator::verifyChunk(BufferChunk* chunk,
bool hasNurseryOwnedAllocs) {
MOZ_ASSERT(chunk->hasNurseryOwnedAllocs == hasNurseryOwnedAllocs);
static constexpr size_t StepBytes = MediumAllocGranularity;
size_t freeOffset = FirstMediumAllocOffset;
size_t freeListsFreeRegionCount =
0;
if (chunk->ownsFreeLists) {
chunk->freeLists.ref().checkAvailable();
for (
auto region = chunk->freeLists.ref().freeRegionIter(); !region.done();
region.next()) {
MOZ_ASSERT(BufferChunk::from(region) == chunk);
freeListsFreeRegionCount++;
}
}
else {
MOZ_ASSERT(chunk->freeLists.ref().isEmpty());
}
size_t chunkFreeRegionCount =
0;
for (
auto iter = chunk->allocIter(); !iter.done(); iter.next()) {
// Check any free region preceding this allocation.
size_t offset = iter.getOffset();
MOZ_ASSERT(offset >= FirstMediumAllocOffset);
if (offset > freeOffset) {
verifyFreeRegion(chunk, offset, offset - freeOffset,
chunkFreeRegionCount);
}
// Check this allocation.
void* alloc = iter.get();
MOZ_ASSERT_IF(chunk->isNurseryOwned(alloc), hasNurseryOwnedAllocs);
size_t bytes = chunk->allocBytes(alloc);
uintptr_t endOffset = offset + bytes;
MOZ_ASSERT(endOffset <= ChunkSize);
for (size_t i = offset + StepBytes; i < endOffset; i += StepBytes) {
MOZ_ASSERT(!chunk->isAllocated(i));
}
if (chunk->isSmallBufferRegion(alloc)) {
auto* region = SmallBufferRegion::from(alloc);
MOZ_ASSERT_IF(region->hasNurseryOwnedAllocs(), hasNurseryOwnedAllocs);
verifySmallBufferRegion(region, chunkFreeRegionCount);
}
freeOffset = endOffset;
}
// Check any free region following the last allocation.
if (freeOffset < ChunkSize) {
verifyFreeRegion(chunk, ChunkSize, ChunkSize - freeOffset,
chunkFreeRegionCount);
}
MOZ_ASSERT_IF(chunk->ownsFreeLists,
freeListsFreeRegionCount == chunkFreeRegionCount);
}
void BufferAllocator::verifyFreeRegion(BufferChunk* chunk, uintptr_t endOffset,
size_t expectedSize,
size_t& freeRegionCount) {
MOZ_ASSERT(expectedSize >= MinFreeRegionSize);
auto* freeRegion = FreeRegion::fromEndOffset(chunk, endOffset);
MOZ_ASSERT(freeRegion->isInList());
MOZ_ASSERT(freeRegion->size() == expectedSize);
freeRegionCount++;
}
void BufferAllocator::verifySmallBufferRegion(SmallBufferRegion* region,
size_t& freeRegionCount) {
bool foundNurseryOwnedAllocs =
false;
static constexpr size_t StepBytes = SmallAllocGranularity;
size_t freeOffset = FirstSmallAllocOffset;
for (
auto iter = region->allocIter(); !iter.done(); iter.next()) {
// Check any free region preceding this allocation.
size_t offset = iter.getOffset();
MOZ_ASSERT(offset >= FirstSmallAllocOffset);
if (offset > freeOffset) {
verifyFreeRegion(region, offset, offset - freeOffset, freeRegionCount);
}
// Check this allocation.
void* alloc = iter.get();
MOZ_ASSERT_IF(region->isNurseryOwned(alloc),
region->hasNurseryOwnedAllocs());
size_t bytes = region->allocBytes(alloc);
uintptr_t endOffset = offset + bytes;
MOZ_ASSERT(endOffset <= SmallRegionSize);
for (size_t i = offset + StepBytes; i < endOffset; i += StepBytes) {
MOZ_ASSERT(!region->isAllocated(i));
}
if (region->isNurseryOwned(alloc)) {
foundNurseryOwnedAllocs = true;
}
freeOffset = endOffset;
}
MOZ_ASSERT(foundNurseryOwnedAllocs == region->hasNurseryOwnedAllocs());
// Check any free region following the last allocation.
if (freeOffset < SmallRegionSize) {
verifyFreeRegion(region, SmallRegionSize, SmallRegionSize - freeOffset,
freeRegionCount);
}
}
void BufferAllocator::verifyFreeRegion(SmallBufferRegion* region,
uintptr_t endOffset, size_t expectedSize,
size_t& freeRegionCount) {
if (expectedSize < MinFreeRegionSize) {
return;
}
auto* freeRegion = FreeRegion::fromEndOffset(region, endOffset);
MOZ_ASSERT(freeRegion->isInList());
MOZ_ASSERT(freeRegion->size() == expectedSize);
freeRegionCount++;
}
void BufferAllocator::checkAllocListGCStateNotInUse(LargeAllocList& list,
bool isNurseryOwned) {
for (LargeBuffer* buffer : list) {
MOZ_ASSERT(buffer->isNurseryOwned == isNurseryOwned);
MOZ_ASSERT_IF(!isNurseryOwned, !buffer->allocatedDuringCollection);
}
}
#endif
void* BufferAllocator::allocSmall(size_t bytes,
bool nurseryOwned,
bool inGC) {
MOZ_ASSERT(IsSmallAllocSize(bytes));
// Round up to next available size.
bytes = RoundUp(std::max(bytes, MinSmallAllocSize), SmallAllocGranularity);
MOZ_ASSERT(bytes <= MaxSmallAllocSize);
// Get size class from |bytes|.
size_t sizeClass = SizeClassForSmallAlloc(bytes);
void* alloc = bumpAlloc(bytes, sizeClass, MaxSmallAllocClass);
if (MOZ_UNLIKELY(!alloc)) {
alloc = retrySmallAlloc(bytes, sizeClass, inGC);
if (!alloc) {
return nullptr;
}
}
SmallBufferRegion* region = SmallBufferRegion::from(alloc);
region->setAllocated(alloc, bytes, true);
MOZ_ASSERT(region->allocBytes(alloc) == bytes);
MOZ_ASSERT(!region->isNurseryOwned(alloc));
region->setNurseryOwned(alloc, nurseryOwned);
auto* chunk = BufferChunk::from(alloc);
if (nurseryOwned && !region->hasNurseryOwnedAllocs()) {
region->setHasNurseryOwnedAllocs(true);
setChunkHasNurseryAllocs(chunk);
}
// Heap size updates are done for the small buffer region as a whole, not
// individual allocations within it.
MOZ_ASSERT(!region->isMarked(alloc));
MOZ_ASSERT(IsSmallAlloc(alloc));
return alloc;
}
MOZ_NEVER_INLINE
void* BufferAllocator::retrySmallAlloc(size_t bytes,
size_t sizeClass,
bool inGC) {
auto alloc = [&]() {
return bumpAlloc(bytes, sizeClass, MaxSmallAllocClass);
};
auto growHeap = [&]() {
return allocNewSmallRegion(inGC); };
return refillFreeListsAndRetryAlloc(sizeClass, MaxSmallAllocClass, alloc,
growHeap);
}
bool BufferAllocator::allocNewSmallRegion(
bool inGC) {
void* ptr = allocMediumAligned(SmallRegionSize, inGC);
if (!ptr) {
return false;
}
auto* region =
new (ptr) SmallBufferRegion;
BufferChunk* chunk = BufferChunk::from(region);
chunk->setSmallBufferRegion(region, true);
uintptr_t freeStart = uintptr_t(region) + FirstSmallAllocOffset;
uintptr_t freeEnd = uintptr_t(region) + SmallRegionSize;
size_t sizeClass =
SizeClassForFreeRegion(freeEnd - freeStart, SizeKind::Small);
ptr = reinterpret_cast<
void*>(freeEnd -
sizeof(FreeRegion));
FreeRegion* freeRegion =
new (ptr) FreeRegion(freeStart);
MOZ_ASSERT(freeRegion->getEnd() == freeEnd);
freeLists.ref().pushFront(sizeClass, freeRegion);
return true;
}
/* static */
bool BufferAllocator::IsSmallAlloc(
void* alloc) {
MOZ_ASSERT(IsBufferAlloc(alloc));
// Test for large buffers before calling this so we can assume |alloc| is
// inside a chunk.
MOZ_ASSERT(!IsLargeAlloc(alloc));
BufferChunk* chunk = BufferChunk::from(alloc);
return chunk->isSmallBufferRegion(alloc);
}
void* BufferAllocator::allocMedium(size_t bytes,
bool nurseryOwned,
bool inGC) {
MOZ_ASSERT(!IsSmallAllocSize(bytes));
MOZ_ASSERT(!IsLargeAllocSize(bytes));
// Round up to next allowed size.
bytes = RoundUp(bytes, MediumAllocGranularity);
MOZ_ASSERT(bytes <= MaxMediumAllocSize);
// Get size class from |bytes|.
size_t sizeClass = SizeClassForMediumAlloc(bytes);
void* alloc = bumpAlloc(bytes, sizeClass, MaxMediumAllocClass);
if (MOZ_UNLIKELY(!alloc)) {
alloc = retryMediumAlloc(bytes, sizeClass, inGC);
if (!alloc) {
return nullptr;
}
}
setAllocated(alloc, bytes, nurseryOwned, inGC);
return alloc;
}
MOZ_NEVER_INLINE
void* BufferAllocator::retryMediumAlloc(size_t bytes,
size_t sizeClass,
bool inGC) {
auto alloc = [&]() {
return bumpAlloc(bytes, sizeClass, MaxMediumAllocClass);
};
auto growHeap = [&]() {
return stealOrAllocNewChunk(sizeClass, inGC); };
return refillFreeListsAndRetryAlloc(sizeClass, MaxMediumAllocClass, alloc,
growHeap);
}
template <typename Alloc, typename GrowHeap>
void* BufferAllocator::refillFreeListsAndRetryAlloc(size_t sizeClass,
size_t maxSizeClass,
Alloc&& alloc,
GrowHeap&& growHeap) {
RefillResult r;
do {
r = refillFreeLists(sizeClass, maxSizeClass, growHeap);
if (r == RefillResult::Fail) {
return nullptr;
}
}
while (r == RefillResult::Retry);
void* ptr = alloc();
MOZ_ASSERT(ptr);
return ptr;
}
template <typename GrowHeap>
BufferAllocator::RefillResult BufferAllocator::refillFreeLists(
size_t sizeClass, size_t maxSizeClass, GrowHeap&& growHeap) {
MOZ_ASSERT(sizeClass <= maxSizeClass);
// Take chunks from the available lists and add their free regions to the
// free lists.
if (useAvailableChunk(sizeClass, maxSizeClass)) {
return RefillResult::Success;
}
// If that fails try to merge swept data and retry, avoiding taking the lock
// unless we know there is data to merge. This reduces context switches.
if (hasSweepDataToMerge) {
mergeSweptData();
return RefillResult::Retry;
}
// Try to grow the heap.
if (MOZ_LIKELY(growHeap())) {
return RefillResult::Success;
}
// If all else fails (and we're on the main thread), try waiting for any
// background GC activity to finish.
if (isUsedByMainThread()) {
if (gc->waitForBackgroundTasksOnAllocFailure()) {
return RefillResult::Retry;
}
}
return RefillResult::Fail;
}
bool BufferAllocator::useAvailableChunk(size_t sizeClass, size_t maxSizeClass) {
return useAvailableChunk(sizeClass, maxSizeClass, availableMixedChunks.ref(),
mixedChunks.ref()) ||
useAvailableChunk(sizeClass, maxSizeClass,
availableTenuredChunks.ref(), tenuredChunks.ref());
}
bool BufferAllocator::useAvailableChunk(size_t sizeClass, size_t maxSizeClass,
ChunkLists& src, BufferChunkList& dst) {
// Move available chunks from available list |src| to current list |dst| (and
// put their free regions into the |freeLists|) for size classes less than or
// equal to |sizeClass| that are not currently represented in the free lists
// and for which we have chunks in |src|.
//
// Chunks are moved from the available list to the free lists as needed to
// limit the number of regions in the free lists, as these need to be iterated
// on minor GC.
//
// This restriction on only moving regions less than or equal to the required
// size class is to encourage filling up more used chunks before using less
// used chunks, in the hope that less used chunks will become completely empty
// and can be reclaimed.
MOZ_ASSERT(freeLists.ref().getFirstAvailableSizeClass(
sizeClass, maxSizeClass) == SIZE_MAX);
SizeClassBitSet sizeClasses = getChunkSizeClassesToMove(maxSizeClass, src);
for (
auto i = BitSetIter(sizeClasses); !i.done(); i.next()) {
MOZ_ASSERT(i <= maxSizeClass);
MOZ_ASSERT(!freeLists.ref().hasSizeClass(i));
BufferChunk* chunk = src.popFirstChunk(i);
MOZ_ASSERT(chunk->ownsFreeLists);
MOZ_ASSERT(chunk->freeLists.ref().hasSizeClass(i));
dst.pushBack(chunk);
freeLists.ref().append(std::move(chunk->freeLists.ref()));
chunk->ownsFreeLists =
false;
chunk->freeLists.ref().assertEmpty();
if (i >= sizeClass) {
// We should now be able to allocate a block of the required size as we've
// added free regions of size class |i| where |i => sizeClass|.
MOZ_ASSERT(freeLists.ref().getFirstAvailableSizeClass(
sizeClass, maxSizeClass) != SIZE_MAX);
return true;
}
}
MOZ_ASSERT(freeLists.ref().getFirstAvailableSizeClass(
sizeClass, maxSizeClass) == SIZE_MAX);
return false;
}
BufferAllocator::SizeClassBitSet BufferAllocator::getChunkSizeClassesToMove(
size_t maxSizeClass, ChunkLists& src)
const {
// Make a bitmap of size classes up to |maxSizeClass| which are not present in
// |freeLists| but which are present in available chunks |src|.
//
// The ChunkLists bitmap has an extra bit to represent full chunks compared to
// the FreeLists bitmap. This prevents using the classes methods, but since
// they both fit into a single word we can manipulate the storage directly.
SizeClassBitSet result;
auto& sizeClasses = result.Storage()[
0];
auto& srcAvailable = src.availableSizeClasses().Storage()[
0];
auto& freeAvailable = freeLists.ref().availableSizeClasses().Storage()[
0];
sizeClasses = srcAvailable & ~freeAvailable & BitMask(maxSizeClass +
1);
return result;
}
// Differentiate between small and medium size classes. Large allocations do not
// use size classes.
static bool IsMediumSizeClass(size_t sizeClass) {
MOZ_ASSERT(sizeClass < BufferAllocator::AllocSizeClasses);
return sizeClass >= MinMediumAllocClass;
}
/* static */
BufferAllocator::SizeKind BufferAllocator::SizeClassKind(size_t sizeClass) {
return IsMediumSizeClass(sizeClass) ? SizeKind::Medium : SizeKind::Small;
}
void* BufferAllocator::bumpAlloc(size_t bytes, size_t sizeClass,
size_t maxSizeClass) {
MOZ_ASSERT(SizeClassKind(sizeClass) == SizeClassKind(maxSizeClass));
freeLists.ref().checkAvailable();
// Find smallest suitable size class that has free regions.
sizeClass =
freeLists.ref().getFirstAvailableSizeClass(sizeClass, maxSizeClass);
if (sizeClass == SIZE_MAX) {
return nullptr;
}
FreeRegion* region = freeLists.ref().getFirstRegion(sizeClass);
MOZ_ASSERT(region->size() >= bytes);
void* ptr = allocFromRegion(region, bytes, sizeClass);
updateFreeListsAfterAlloc(&freeLists.ref(), region, sizeClass);
DebugOnlyPoison(ptr, JS_ALLOCATED_BUFFER_PATTERN, bytes,
MemCheckKind::MakeUndefined);
return ptr;
}
#ifdef DEBUG
static size_t GranularityForSizeClass(size_t sizeClass) {
return IsMediumSizeClass(sizeClass) ? MediumAllocGranularity
: SmallAllocGranularity;
}
#endif // DEBUG
void* BufferAllocator::allocFromRegion(FreeRegion* region, size_t bytes,
size_t sizeClass) {
uintptr_t start = region->startAddr;
MOZ_ASSERT(region->getEnd() > start);
MOZ_ASSERT_IF(sizeClass != MaxMediumAllocClass,
region->size() >= SizeClassBytes(sizeClass));
MOZ_ASSERT_IF(sizeClass == MaxMediumAllocClass,
region->size() >= MaxMediumAllocSize);
MOZ_ASSERT(start % GranularityForSizeClass(sizeClass) ==
0);
MOZ_ASSERT(region->size() % GranularityForSizeClass(sizeClass) ==
0);
// Ensure whole region is commited.
if (region->hasDecommittedPages) {
recommitRegion(region);
}
// Allocate from start of region.
void* ptr = reinterpret_cast<
void*>(start);
start += bytes;
MOZ_ASSERT(region->getEnd() >= start);
// Update region start.
region->startAddr = start;
return ptr;
}
// Allocate a region of size |bytes| aligned to |bytes|. The maximum size is
// limited to 256KB. In practice this is only ever used to allocate
// SmallBufferRegions.
void* BufferAllocator::allocMediumAligned(size_t bytes,
bool inGC) {
MOZ_ASSERT(bytes >= MinMediumAllocSize);
MOZ_ASSERT(bytes <= MaxAlignedAllocSize);
MOZ_ASSERT(std::has_single_bit(bytes));
// Get size class from |bytes|.
size_t sizeClass = SizeClassForMediumAlloc(bytes);
void* alloc = alignedAlloc(sizeClass);
if (MOZ_UNLIKELY(!alloc)) {
alloc = retryAlignedAlloc(sizeClass, inGC);
if (!alloc) {
return nullptr;
}
}
setAllocated(alloc, bytes,
false, inGC);
return alloc;
}
MOZ_NEVER_INLINE
void* BufferAllocator::retryAlignedAlloc(size_t sizeClass,
bool inGC) {
// Ensure aligned allocation is possible.
MOZ_ASSERT(sizeClass < MaxMediumAllocClass);
size_t expandedSizeClass = sizeClass +
1;
auto alloc = [&]() {
return alignedAlloc(sizeClass); };
auto growHeap = [&]() {
return stealOrAllocNewChunk(expandedSizeClass, inGC);
};
return refillFreeListsAndRetryAlloc(expandedSizeClass, MaxMediumAllocClass,
alloc, growHeap);
}
void* BufferAllocator::alignedAlloc(size_t sizeClass) {
freeLists.ref().checkAvailable();
// Try the first free region for the smallest possible size class. This will
// fail if that region is for the exact size class requested but the region is
// not aligned.
size_t allocClass = freeLists.ref().getFirstAvailableSizeClass(
sizeClass, MaxMediumAllocClass);
MOZ_ASSERT(allocClass >= sizeClass);
if (allocClass == SIZE_MAX) {
return nullptr;
}
FreeRegion* region = freeLists.ref().getFirstRegion(allocClass);
void* ptr = alignedAllocFromRegion(region, sizeClass);
if (ptr) {
updateFreeListsAfterAlloc(&freeLists.ref(), region, allocClass);
return ptr;
}
// If we couldn't allocate an aligned region, try a larger size class. This
// only happens if we selected the size class equal to the requested size.
MOZ_ASSERT(allocClass == sizeClass);
allocClass = freeLists.ref().getFirstAvailableSizeClass(sizeClass +
1,
MaxMediumAllocClass);
if (allocClass == SIZE_MAX) {
return nullptr;
}
region = freeLists.ref().getFirstRegion(allocClass);
ptr = alignedAllocFromRegion(region, sizeClass);
MOZ_ASSERT(ptr);
updateFreeListsAfterAlloc(&freeLists.ref(), region, allocClass);
return ptr;
}
void* BufferAllocator::alignedAllocFromRegion(FreeRegion* region,
size_t sizeClass) {
// Attempt to allocate an aligned region from |region|.
uintptr_t start = region->startAddr;
MOZ_ASSERT(region->getEnd() > start);
MOZ_ASSERT(region->size() >= SizeClassBytes(sizeClass));
MOZ_ASSERT((region->size() % MinMediumAllocSize) ==
0);
size_t bytes = SizeClassBytes(sizeClass);
size_t alignedStart = RoundUp(start, bytes);
size_t end = alignedStart + bytes;
if (end > region->getEnd()) {
return nullptr;
}
// Align the start of the region, creating a new free region out of the space
// at the start if necessary.
if (alignedStart != start) {
size_t alignBytes = alignedStart - start;
void* prefix = allocFromRegion(region, alignBytes, sizeClass);
MOZ_ASSERT(uintptr_t(prefix) == start);
(
void)prefix;
MOZ_ASSERT(!region->hasDecommittedPages);
FreeRegion* region = makeFreeRegion(start, alignBytes,
false);
pushFreeRegionBack(&freeLists.ref(), region, SizeKind::Medium);
}
// Now the start is aligned we can use the normal allocation method.
MOZ_ASSERT(region->startAddr % bytes ==
0);
return allocFromRegion(region, bytes, sizeClass);
}
void BufferAllocator::setAllocated(
void* alloc, size_t bytes,
bool nurseryOwned,
bool inGC) {
BufferChunk* chunk = BufferChunk::from(alloc);
chunk->setAllocated(alloc, bytes, true);
MOZ_ASSERT(chunk->allocBytes(alloc) == bytes);
MOZ_ASSERT(!chunk->isNurseryOwned(alloc));
chunk->setNurseryOwned(alloc, nurseryOwned);
if (nurseryOwned) {
setChunkHasNurseryAllocs(chunk);
}
MOZ_ASSERT(!chunk->isMarked(alloc));
bool checkThresholds = !inGC;
increaseHeapSize(bytes, nurseryOwned, checkThresholds,
false);
MOZ_ASSERT(!chunk->isSmallBufferRegion(alloc));
}
void BufferAllocator::setChunkHasNurseryAllocs(BufferChunk* chunk) {
MOZ_ASSERT(!chunk->ownsFreeLists);
if (chunk->hasNurseryOwnedAllocs) {
return;
}
tenuredChunks.ref().remove(chunk);
mixedChunks.ref().pushBack(chunk);
chunk->hasNurseryOwnedAllocs = true;
}
void BufferAllocator::updateFreeListsAfterAlloc(FreeLists* freeLists,
FreeRegion* region,
size_t sizeClass) {
// Updates |freeLists| after an allocation from |region| which is currently in
// the |sizeClass| free list. This may move the region to a different free
// list.
freeLists->assertContains(sizeClass, region);
// If the region is still valid for further allocations of this size class
// then there's nothing to do.
size_t classBytes = SizeClassBytes(sizeClass);
size_t newSize = region->size();
MOZ_ASSERT(newSize % GranularityForSizeClass(sizeClass) ==
0);
if (newSize >= classBytes) {
return;
}
// Remove region from this free list.
freeLists->remove(sizeClass, region);
// If the region is now empty then we're done.
if (newSize ==
0) {
return;
}
// Otherwise region is now too small. Move it to the appropriate bucket for
// its reduced size if possible.
if (newSize < MinFreeRegionSize) {
// We can't record a region this small. The free space will not be reused
// until enough adjacent space become free
return;
}
size_t newSizeClass =
SizeClassForFreeRegion(newSize, SizeClassKind(sizeClass));
MOZ_ASSERT_IF(newSizeClass != MaxMediumAllocClass,
newSize >= SizeClassBytes(newSizeClass));
MOZ_ASSERT(newSizeClass <= sizeClass);
MOZ_ASSERT_IF(newSizeClass != MaxMediumAllocClass, newSizeClass < sizeClass);
MOZ_ASSERT(SizeClassKind(newSizeClass) == SizeClassKind(sizeClass));
freeLists->pushFront(newSizeClass, region);
}
void BufferAllocator::recommitRegion(FreeRegion* region) {
MOZ_ASSERT(region->hasDecommittedPages);
MOZ_ASSERT(DecommitEnabled());
BufferChunk* chunk = BufferChunk::from(region);
uintptr_t startAddr = RoundUp(region->startAddr, PageSize);
uintptr_t endAddr = RoundDown(uintptr_t(region), PageSize);
size_t startPage = (startAddr - uintptr_t(chunk)) / PageSize;
size_t endPage = (endAddr - uintptr_t(chunk)) / PageSize;
// If the start of the region does not lie on a page boundary the page it is
// in should be committed as it must either contain the start of the chunk, a
// FreeRegion or an allocation.
MOZ_ASSERT_IF((region->startAddr % PageSize) !=
0,
!chunk->decommittedPages.ref()[startPage -
1]);
// The end of the region should be committed as it holds FreeRegion |region|.
MOZ_ASSERT(!chunk->decommittedPages.ref()[endPage]);
MarkPagesInUseSoft(reinterpret_cast<
void*>(startAddr), endAddr - startAddr);
for (size_t i = startPage; i != endPage; i++) {
chunk->decommittedPages.ref()[i] =
false;
}
region->hasDecommittedPages =
false;
}
static inline StallAndRetry ShouldStallAndRetry(
bool inGC) {
return inGC ? StallAndRetry::Yes : StallAndRetry::No;
}
bool BufferAllocator::stealOrAllocNewChunk(size_t sizeClass,
bool inGC) {
// Attempt to steal a nearly empty chunk that would otherwise be swept so we
// can continue allocating from it.
if (majorState == State::Marking && !tenuredChunksToSweep.ref().isEmpty() &&
gc->isNormalGC()) {
BufferChunk* chunk = tenuredChunksToSweep.ref().getLast();
MOZ_ASSERT(chunk->ownsFreeLists);
// Look for chunks that have a free region at least 1/2 the size of a chunk,
// and that can fit the allocation.
size_t minSizeClass = std::max(sizeClass, MaxMediumAllocClass -
1);
if (chunk->freeLists.ref().getLastAvailableSizeClass(
minSizeClass, MaxMediumAllocClass) != SIZE_MAX) {
// Remove the chunk from the sweep list.
tenuredChunksToSweep.ref().remove(chunk);
// Make the chunk look like a newly allocated one and disable barriers.
chunk->allocatedDuringCollection = true;
// Set a flag so we know to clear the mark bits later. We can't do this
// now because concurrent marking may be accessing them.
chunk->stolenFromSweepList = true;
// Move the chunk to the current allocation lists.
MOZ_ASSERT(!chunk->hasNurseryOwnedAllocs);
tenuredChunks.ref().pushBack(chunk);
freeLists.ref().append(std::move(chunk->freeLists.ref()));
chunk->ownsFreeLists =
false;
chunk->freeLists.ref().assertEmpty();
return true;
}
}
return allocNewChunk(inGC);
}
bool BufferAllocator::allocNewChunk(
bool inGC) {
if (!inGC && js::oom::ShouldFailWithOOM()) {
return false;
}
AutoLockGCBgAlloc lock(gc);
ArenaChunk* baseChunk = gc->getOrAllocChunk(ShouldStallAndRetry(inGC), lock);
if (!baseChunk) {
return false;
}
CheckHighBitsOfPointer(baseChunk);
// Ensure all memory is initially committed.
if (!baseChunk->decommittedPages.IsEmpty()) {
MOZ_ASSERT(DecommitEnabled());
MarkPagesInUseSoft(baseChunk, ChunkSize);
}
// Unpoison past the ChunkBase header.
void* ptr = reinterpret_cast<
void*>(uintptr_t(baseChunk) +
sizeof(ChunkBase));
size_t size = ChunkSize -
sizeof(ChunkBase);
SetMemCheckKind(ptr, size, MemCheckKind::MakeUndefined);
BufferChunk* chunk =
new (baseChunk) BufferChunk(zone);
chunk->allocatedDuringCollection = majorState != State::NotCollecting;
tenuredChunks.ref().pushBack(chunk);
uintptr_t freeStart = uintptr_t(chunk) + FirstMediumAllocOffset;
uintptr_t freeEnd = uintptr_t(chunk) + ChunkSize;
size_t sizeClass =
SizeClassForFreeRegion(freeEnd - freeStart, SizeKind::Medium);
MOZ_ASSERT(sizeClass > MaxSmallAllocClass);
MOZ_ASSERT(sizeClass <= MaxMediumAllocClass);
ptr = reinterpret_cast<
void*>(freeEnd -
sizeof(FreeRegion));
FreeRegion* region =
new (ptr) FreeRegion(freeStart);
MOZ_ASSERT(region->getEnd() == freeEnd);
freeLists.ref().pushFront(sizeClass, region);
return true;
}
bool BufferAllocator::sweepChunk(BufferChunk* chunk, SweepKind sweepKind,
bool shouldDecommit) {
// Find all regions of free space in |chunk| and add them to the swept free
// lists.
// TODO: It could be beneficialy to allocate from most-full chunks first. This
// could happen by sweeping all chunks and then sorting them by how much free
// space they had and then adding their free regions to the free lists in that
// order.
MOZ_ASSERT_IF(sweepKind == SweepKind::Tenured, !chunk->stolenFromSweepList);
MOZ_ASSERT_IF(sweepKind == SweepKind::Tenured,
!chunk->allocatedDuringCollection);
MOZ_ASSERT_IF(sweepKind == SweepKind::Tenured, chunk->ownsFreeLists);
FreeLists& freeLists = chunk->freeLists.ref();
// TODO: For tenured sweeping, check whether anything needs to be swept and
// reuse the existing free regions rather than rebuilding these every time.
freeLists.clear();
chunk->ownsFreeLists = true;
// First sweep any small buffer regions.
bool sweptAny =
false;
bool hasNurseryOwnedSmallRegions =
false;
size_t smallRegionBytesFreed =
0;
for (
auto iter = chunk->smallRegionIter(); !iter.done(); iter.next()) {
SmallBufferRegion* region = iter.get();
MOZ_ASSERT(!chunk->isMarked(region));
MOZ_ASSERT(!chunk->isNurseryOwned(region));
MOZ_ASSERT(chunk->allocBytes(region) == SmallRegionSize);
if (!sweepSmallBufferRegion(chunk, region, sweepKind)) {
chunk->setSmallBufferRegion(region,
false);
chunk->setDeallocated(region, SmallRegionSize);
PoisonAlloc(region, JS_SWEPT_TENURED_PATTERN,
sizeof(SmallBufferRegion),
MemCheckKind::MakeUndefined);
smallRegionBytesFreed += SmallRegionSize;
sweptAny = true;
}
else {
if (sweepKind == SweepKind::Tenured) {
chunk->setMarked(region);
}
if (region->hasNurseryOwnedAllocs()) {
hasNurseryOwnedSmallRegions = true;
}
}
}
if (smallRegionBytesFreed) {
bool inMajorGC = sweepKind == SweepKind::Tenured;
decreaseHeapSize(smallRegionBytesFreed,
false, inMajorGC);
}
auto result =
chunk->sweep(this, freeLists, sweepKind, sweptAny, shouldDecommit);
if (sweepKind == SweepKind::Tenured && result.bytesFreed) {
decreaseHeapSize(result.bytesFreed,
false, true);
}
if (result.isEmpty) {
// Chunk is empty. Give it back to the system.
bool allMemoryCommitted = chunk->decommittedPages.ref().IsEmpty();
chunk->~BufferChunk();
ArenaChunk* tenuredChunk = ArenaChunk::init(chunk, gc, allMemoryCommitted);
AutoLockGC lock(gc);
gc->recycleChunk(tenuredChunk, lock);
return false;
}
chunk->hasNurseryOwnedAllocsAfterSweep =
hasNurseryOwnedSmallRegions || result.hasNurseryOwnedAllocs;
return true;
}
/* static */
bool BufferAllocator::CanSweepAlloc(
bool nurseryOwned,
BufferAllocator::SweepKind sweepKind) {
static_assert(SweepKind::Nursery == SweepKind(uint8_t(true)));
static_assert(SweepKind::Tenured == SweepKind(uint8_t(
false)));
SweepKind requiredKind = SweepKind(uint8_t(nurseryOwned));
return sweepKind == requiredKind;
}
void BufferAllocator::addSweptRegion(BufferChunk* chunk, uintptr_t freeStart,
uintptr_t freeEnd,
bool shouldDecommit,
bool expectUnchanged,
FreeLists& freeLists) {
// Add the region from |freeStart| to |freeEnd| to the appropriate swept free
// list based on its size.
MOZ_ASSERT(freeStart >= FirstMediumAllocOffset);
MOZ_ASSERT(freeStart < freeEnd);
MOZ_ASSERT(freeEnd <= ChunkSize);
MOZ_ASSERT((freeStart % MediumAllocGranularity) ==
0);
MOZ_ASSERT((freeEnd % MediumAllocGranularity) ==
0);
MOZ_ASSERT_IF(shouldDecommit, DecommitEnabled());
// Decommit pages if |shouldDecommit| was specified, but leave space for
// the FreeRegion structure at the end.
bool anyDecommitted =
false;
uintptr_t decommitStart = RoundUp(freeStart, PageSize);
uintptr_t decommitEnd = RoundDown(freeEnd -
sizeof(FreeRegion), PageSize);
size_t endPage = decommitEnd / PageSize;
if (shouldDecommit && decommitEnd > decommitStart) {
void* ptr = reinterpret_cast<
void*>(decommitStart + uintptr_t(chunk));
MarkPagesUnusedSoft(ptr, decommitEnd - decommitStart);
size_t startPage = decommitStart / PageSize;
for (size_t i = startPage; i != endPage; i++) {
chunk->decommittedPages.ref()[i] = true;
}
anyDecommitted = true;
}
else {
// Check for any previously decommitted pages.
uintptr_t startPage = RoundDown(freeStart, PageSize) / PageSize;
for (size_t i = startPage; i != endPage; i++) {
if (chunk->decommittedPages.ref()[i]) {
anyDecommitted = true;
}
}
}
// The last page must have previously been either a live allocation or a
// FreeRegion, so it must already be committed.
MOZ_ASSERT(!chunk->decommittedPages.ref()[endPage]);
freeStart += uintptr_t(chunk);
freeEnd += uintptr_t(chunk);
size_t bytes = freeEnd - freeStart;
FreeRegion* region =
makeFreeRegion(freeStart, bytes, anyDecommitted, expectUnchanged);
pushFreeRegionBack(&freeLists, region, SizeKind::Medium);
}
bool BufferAllocator::sweepSmallBufferRegion(BufferChunk* chunk,
SmallBufferRegion* region,
SweepKind sweepKind) {
FreeLists& freeLists = chunk->freeLists.ref();
auto result = region->sweep(this, freeLists, sweepKind,
false,
false);
if (result.isEmpty) {
return false;
}
region->setHasNurseryOwnedAllocs(result.hasNurseryOwnedAllocs);
return true;
}
template <typename D, size_t S, size_t G>
AllocSpace<D, S, G>::SweepResult AllocSpace<D, S, G>::sweep(
BufferAllocator* allocator, FreeLists& freeLists, SweepKind sweepKind,
bool sweptAnyPreviously,
bool shouldDecommit) {
SweepResult result;
size_t freeStart = firstAllocOffset();
bool sweptAny = sweptAnyPreviously;
for (
auto iter = allocIter(); !iter.done(); iter.next()) {
void* alloc = iter.get();
size_t bytes = allocBytes(alloc);
uintptr_t allocEnd = iter.getOffset() + bytes;
bool nurseryOwned = isNurseryOwned(alloc);
bool canSweep = BufferAllocator::CanSweepAlloc(nurseryOwned, sweepKind);
bool shouldSweep = canSweep && !isMarked(alloc);
if (shouldSweep) {
// Dead. Update allocated bitmap, metadata and heap size accounting.
setDeallocated(alloc, bytes);
PoisonAlloc(alloc, JS_SWEPT_TENURED_PATTERN, bytes,
MemCheckKind::MakeUndefined);
result.bytesFreed += bytes;
sweptAny = true;
}
else {
// Alive. Add any free space before this allocation.
uintptr_t allocStart = iter.getOffset();
if (freeStart != allocStart) {
allocator->addSweptRegion(asDerived(), freeStart, allocStart,
shouldDecommit, !sweptAny, freeLists);
}
freeStart = allocEnd;
if (canSweep) {
setUnmarked(alloc);
}
if (nurseryOwned) {
MOZ_ASSERT(sweepKind == SweepKind::Nursery);
result.hasNurseryOwnedAllocs = true;
}
sweptAny = sweptAnyPreviously;
}
}
// Add any free space from the last allocation to the end of the chunk, but
// not if the space is entirely empty.
result.isEmpty = freeStart == firstAllocOffset();
if (freeStart != SizeBytes && !result.isEmpty) {
allocator->addSweptRegion(asDerived(), freeStart, SizeBytes, shouldDecommit,
!sweptAny, freeLists);
}
return result;
}
void BufferAllocator::addSweptRegion(SmallBufferRegion* region,
uintptr_t freeStart, uintptr_t freeEnd,
bool shouldDecommit,
bool expectUnchanged,
FreeLists& freeLists) {
// Add the region from |freeStart| to |freeEnd| to the appropriate swept free
// list based on its size. Unused pages in small buffer regions are not
// decommitted.
MOZ_ASSERT(freeStart >= FirstSmallAllocOffset);
MOZ_ASSERT(freeStart < freeEnd);
MOZ_ASSERT(freeEnd <= SmallRegionSize);
MOZ_ASSERT(freeStart % SmallAllocGranularity ==
0);
MOZ_ASSERT(freeEnd % SmallAllocGranularity ==
0);
MOZ_ASSERT(!shouldDecommit);
freeStart += uintptr_t(region);
freeEnd += uintptr_t(region);
size_t bytes = freeEnd - freeStart;
FreeRegion* freeRegion =
makeFreeRegion(freeStart, bytes,
false, expectUnchanged);
if (freeRegion) {
pushFreeRegionBack(&freeLists, freeRegion, SizeKind::Small);
}
}
void BufferAllocator::freeMedium(
void* alloc) {
// Free a medium sized allocation. This coalesces the free space with any
// neighboring free regions. Coalescing is necessary for resize to work
// properly.
BufferChunk* chunk = BufferChunk::from(alloc);
MOZ_ASSERT(chunk->zone == zone);
if (!canModifyAllocations(chunk)) {
return;
}
size_t bytes = chunk->allocBytes(alloc);
PoisonAlloc(alloc, JS_FREED_BUFFER_PATTERN, bytes,
MemCheckKind::MakeUndefined);
// Update heap size.
bool updateRetained =
majorState == State::Marking && !chunk->allocatedDuringCollection;
decreaseHeapSize(bytes, chunk->isNurseryOwned(alloc), updateRetained);
// TODO: Since the mark bits are atomic, it's probably OK to unmark even if
// the chunk is currently being swept. If we get lucky the memory will be
// freed sooner.
chunk->setUnmarked(alloc);
// Set region as not allocated and clear metadata.
chunk->setDeallocated(alloc, bytes);
FreeLists* freeLists = getChunkFreeLists(chunk);
uintptr_t startAddr = uintptr_t(alloc);
uintptr_t endAddr = startAddr + bytes;
// If the chunk is in one of the available lists we may need to move it.
ChunkLists* availableChunks = getChunkAvailableLists(chunk);
size_t oldChunkSizeClass = SIZE_MAX;
if (availableChunks) {
oldChunkSizeClass = chunk->sizeClassForAvailableLists();
}
// First check whether there is a free region following the allocation.
FreeRegion* region;
uintptr_t endOffset = endAddr & ChunkMask;
if (endOffset ==
0 || chunk->isAllocated(endOffset)) {
// The allocation abuts the end of the chunk or another allocation. Add the
// allocation as a new free region.
//
// The new region is added to the front of relevant list so as to reuse
// recently freed memory preferentially. This may reduce fragmentation. See
// "The Memory Fragmentation Problem: Solved?" by Johnstone et al.
region = makeFreeRegion(startAddr, bytes,
false);
pushFreeRegionFront(freeLists, region, SizeKind::Medium);
}
else {
// There is a free region following this allocation. Expand the existing
// region down to cover the newly freed space.
region = chunk->findFollowingFreeRegion(endAddr);
MOZ_ASSERT(region->startAddr == endAddr);
updateFreeRegionStart(freeLists, region, startAddr, SizeKind::Medium);
}
// Next check for any preceding free region and coalesce.
FreeRegion* precRegion = chunk->findPrecedingFreeRegion(startAddr);
if (precRegion) {
if (freeLists) {
size_t sizeClass =
SizeClassForFreeRegion(precRegion->size(), SizeKind::Medium);
freeLists->remove(sizeClass, precRegion);
}
updateFreeRegionStart(freeLists, region, precRegion->startAddr,
SizeKind::Medium);
if (precRegion->hasDecommittedPages) {
region->hasDecommittedPages = true;
}
}
if (availableChunks) {
maybeUpdateAvailableLists(availableChunks, chunk, oldChunkSizeClass);
}
}
void BufferAllocator::maybeUpdateAvailableLists(ChunkLists* availableChunks,
BufferChunk* chunk,
size_t oldChunkSizeClass) {
// A realloc or free operation can change the amount of free space in an
// available chunk, so we may need to move it to a different list.
size_t newChunkSizeClass = chunk->sizeClassForAvailableLists();
if (newChunkSizeClass != oldChunkSizeClass) {
availableChunks->remove(oldChunkSizeClass, chunk);
availableChunks->pushBack(newChunkSizeClass, chunk);
}
}
bool BufferAllocator::canModifyAllocations(BufferChunk* chunk) {
// Don't free or resize allocations that may be accessed by concurrent
// marking. Dead allocations will be freed during sweeping.
if (isConcurrentMarking()) {
return false;
}
// We can't change anything if the chunk is currently being swept.
return !isSweepingChunk(chunk);
}
bool BufferAllocator::isConcurrentMarking()
const {
#ifdef JS_GC_CONCURRENT_MARKING
return majorState == State::Marking && gc->isConcurrentMarkingEnabled();
#else
return false;
#endif
}
bool BufferAllocator::isSweepingChunk(BufferChunk* chunk) {
if (minorState == State::Sweeping && chunk->hasNurseryOwnedAllocs) {
// We are currently sweeping nursery owned allocations.
// TODO: We could set a flag for nursery chunks allocated during minor
// collection to allow operations on chunks that are not being swept here.
if (!hasSweepDataToMerge) {
#ifdef DEBUG
{
AutoLock lock(runtime());
MOZ_ASSERT_IF(!hasSweepDataToMerge, !minorSweepingFinished);
}
#endif
// Likely no data to merge so don't bother taking the lock.
return true;
}
// Merge swept data and recheck.
//
// TODO: It would be good to know how often this helps and if it is
// worthwhile.
mergeSweptData();
if (minorState == State::Sweeping && chunk->hasNurseryOwnedAllocs) {
return true;
}
}
if (majorState == State::Sweeping && !chunk->allocatedDuringCollection) {
// We are currently sweeping tenured owned allocations.
return true;
}
return false;
}
BufferAllocator::FreeRegion* BufferAllocator::makeFreeRegion(
uintptr_t start, size_t bytes,
bool anyDecommitted,
bool expectUnchanged
/* = false */) {
static_assert(
sizeof(FreeRegion) <= MinFreeRegionSize);
if (bytes < MinFreeRegionSize) {
// We can't record a region this small. The free space will not be reused
// until enough adjacent space become free.
return nullptr;
}
uintptr_t end = start + bytes;
#ifdef DEBUG
if (expectUnchanged) {
// We didn't free any allocations so there should already be a FreeRegion
// from |start| to |end|.
auto* region = FreeRegion::fromEndAddr(end);
MOZ_ASSERT(region->startAddr == start);
}
#endif
void* ptr = reinterpret_cast<
void*>(end -
sizeof(FreeRegion));
FreeRegion* region =
new (ptr) FreeRegion(start, anyDecommitted);
MOZ_ASSERT(region->getEnd() == end);
return region;
}
void BufferAllocator::pushFreeRegionBack(FreeLists* freeLists,
FreeRegion* region, SizeKind kind) {
MOZ_ASSERT(region);
size_t sizeClass = SizeClassForFreeRegion(region->size(), kind);
CheckFreeRegionClass(region, sizeClass);
freeLists->pushBack(sizeClass, region);
}
void BufferAllocator::pushFreeRegionFront(FreeLists* freeLists,
FreeRegion* region, SizeKind kind) {
MOZ_ASSERT(region);
size_t sizeClass = SizeClassForFreeRegion(region->size(), kind);
CheckFreeRegionClass(region, sizeClass);
freeLists->pushFront(sizeClass, region);
}
/* static */
inline void BufferAllocator::CheckFreeRegionClass(FreeRegion* region,
size_t sizeClass) {
#ifdef DEBUG
size_t bytes = region->size();
MOZ_ASSERT_IF(sizeClass != MaxMediumAllocClass,
bytes >= SizeClassBytes(sizeClass));
MOZ_ASSERT(region->startAddr % GranularityForSizeClass(sizeClass) ==
0);
MOZ_ASSERT(bytes % GranularityForSizeClass(sizeClass) ==
0);
#endif
}
void BufferAllocator::updateFreeRegionStart(FreeLists* freeLists,
FreeRegion* region,
uintptr_t newStart, SizeKind kind) {
MOZ_ASSERT((newStart & ~ChunkMask) == (uintptr_t(region) & ~ChunkMask));
MOZ_ASSERT(region->startAddr != newStart);
// TODO: Support realloc for small regions.
MOZ_ASSERT(kind == SizeKind::Medium);
size_t oldSize = region->size();
region->startAddr = newStart;
if (!freeLists) {
return;
}
size_t currentSizeClass = SizeClassForFreeRegion(oldSize, kind);
size_t newSizeClass = SizeClassForFreeRegion(region->size(), kind);
MOZ_ASSERT(SizeClassKind(newSizeClass) == SizeClassKind(currentSizeClass));
if (currentSizeClass != newSizeClass) {
freeLists->remove(currentSizeClass, region);
freeLists->pushFront(newSizeClass, region);
}
}
bool BufferAllocator::growMedium(
void* alloc, size_t newBytes) {
MOZ_ASSERT(!IsSmallAllocSize(newBytes));
MOZ_ASSERT(!IsLargeAllocSize(newBytes));
newBytes = std::max(newBytes, MinMediumAllocSize);
MOZ_ASSERT(newBytes == GetGoodAllocSize(newBytes));
BufferChunk* chunk = BufferChunk::from(alloc);
MOZ_ASSERT(chunk->zone == zone);
if (!canModifyAllocations(chunk)) {
return false;
}
size_t currentBytes = chunk->allocBytes(alloc);
MOZ_ASSERT(newBytes > currentBytes);
uintptr_t endOffset = (uintptr_t(alloc) & ChunkMask) + currentBytes;
MOZ_ASSERT(endOffset <= ChunkSize);
if (endOffset == ChunkSize) {
return false;
// Can't extend because we're at the end of the chunk.
}
size_t endAddr = uintptr_t(chunk) + endOffset;
if (chunk->isAllocated(endOffset)) {
return false;
// Can't extend because we abut another allocation.
}
FreeRegion* region = chunk->findFollowingFreeRegion(endAddr);
MOZ_ASSERT(region->startAddr == endAddr);
size_t extraBytes = newBytes - currentBytes;
if (region->size() < extraBytes) {
return false;
// Can't extend because following free region is too small.
}
size_t sizeClass = SizeClassForFreeRegion(region->size(), SizeKind::Medium);
allocFromRegion(region, extraBytes, sizeClass);
// If the chunk is in one of the available lists we may need to move it if the
// largest free region has shrunk too much.
ChunkLists* availableChunks = getChunkAvailableLists(chunk);
size_t oldChunkSizeClass = SIZE_MAX;
if (availableChunks) {
oldChunkSizeClass = chunk->sizeClassForAvailableLists();
}
FreeLists* freeLists = getChunkFreeLists(chunk);
updateFreeListsAfterAlloc(freeLists, region, sizeClass);
if (availableChunks) {
maybeUpdateAvailableLists(availableChunks, chunk, oldChunkSizeClass);
}
chunk->updateEndOffset(alloc, currentBytes, newBytes);
MOZ_ASSERT(chunk->allocBytes(alloc) == newBytes);
bool updateRetained =
majorState == State::Marking && !chunk->allocatedDuringCollection;
increaseHeapSize(extraBytes, chunk->isNurseryOwned(alloc), true,
updateRetained);
return true;
}
bool BufferAllocator::shrinkMedium(
void* alloc, size_t newBytes) {
MOZ_ASSERT(!IsSmallAllocSize(newBytes));
MOZ_ASSERT(!IsLargeAllocSize(newBytes));
newBytes = std::max(newBytes, MinMediumAllocSize);
MOZ_ASSERT(newBytes == GetGoodAllocSize(newBytes));
BufferChunk* chunk = BufferChunk::from(alloc);
MOZ_ASSERT(chunk->zone == zone);
if (!canModifyAllocations(chunk)) {
return false;
}
size_t currentBytes = chunk->allocBytes(alloc);
if (newBytes == currentBytes) {
// Requested size is the same after adjusting to a valid medium alloc size.
return false;
}
MOZ_ASSERT(newBytes < currentBytes);
size_t sizeChange = currentBytes - newBytes;
// Update allocation size.
chunk->updateEndOffset(alloc, currentBytes, newBytes);
MOZ_ASSERT(chunk->allocBytes(alloc) == newBytes);
bool updateRetained =
majorState == State::Marking && !chunk->allocatedDuringCollection;
decreaseHeapSize(sizeChange, chunk->isNurseryOwned(alloc), updateRetained);
uintptr_t startOffset = uintptr_t(alloc) & ChunkMask;
uintptr_t oldEndOffset = startOffset + currentBytes;
uintptr_t newEndOffset = startOffset + newBytes;
MOZ_ASSERT(oldEndOffset <= ChunkSize);
// Poison freed memory.
uintptr_t chunkAddr = uintptr_t(chunk);
PoisonAlloc(reinterpret_cast<
void*>(chunkAddr + newEndOffset),
JS_SWEPT_TENURED_PATTERN, sizeChange,
MemCheckKind::MakeUndefined);
// If the chunk is in one of the available lists we may need to move it.
ChunkLists* availableChunks = getChunkAvailableLists(chunk);
size_t oldChunkSizeClass = SIZE_MAX;
if (availableChunks) {
oldChunkSizeClass = chunk->sizeClassForAvailableLists();
}
FreeLists* freeLists = getChunkFreeLists(chunk);
if (oldEndOffset == ChunkSize || chunk->isAllocated(oldEndOffset)) {
// If we abut another allocation then add a new free region.
uintptr_t freeStart = chunkAddr + newEndOffset;
FreeRegion* region = makeFreeRegion(freeStart, sizeChange,
false);
pushFreeRegionFront(freeLists, region, SizeKind::Medium);
}
else {
// Otherwise find the following free region and extend it down.
FreeRegion* region =
chunk->findFollowingFreeRegion(chunkAddr + oldEndOffset);
MOZ_ASSERT(region->startAddr == chunkAddr + oldEndOffset);
updateFreeRegionStart(freeLists, region, chunkAddr + newEndOffset,
SizeKind::Medium);
}
if (availableChunks) {
maybeUpdateAvailableLists(availableChunks, chunk, oldChunkSizeClass);
}
return true;
}
BufferAllocator::FreeLists* BufferAllocator::getChunkFreeLists(
BufferChunk* chunk) {
MOZ_ASSERT_IF(majorState == State::Sweeping,
chunk->allocatedDuringCollection);
MOZ_ASSERT_IF(
majorState == State::Marking && !chunk->allocatedDuringCollection,
chunk->ownsFreeLists);
if (chunk->ownsFreeLists) {
// The chunk is in one of the available lists.
return &chunk->freeLists.ref();
}
return &freeLists.ref();
}
BufferAllocator::ChunkLists* BufferAllocator::getChunkAvailableLists(
BufferChunk* chunk) {
MOZ_ASSERT_IF(majorState == State::Sweeping,
chunk->allocatedDuringCollection);
if (!chunk->ownsFreeLists) {
return nullptr;
// Chunk is not in either available list.
}
if (majorState == State::Marking && !chunk->allocatedDuringCollection) {
return nullptr;
// Chunk is waiting to be swept.
}
if (chunk->hasNurseryOwnedAllocs) {
return &availableMixedChunks.ref();
}
return &availableTenuredChunks.ref();
}
/* static */
size_t BufferAllocator::SizeClassForSmallAlloc(size_t bytes) {
MOZ_ASSERT(bytes >= MinSmallAllocSize);
MOZ_ASSERT(bytes <= MaxSmallAllocSize);
size_t log2Size = mozilla::CeilingLog2(bytes);
MOZ_ASSERT((size_t(
1) << log2Size) >= bytes);
MOZ_ASSERT(MinSizeClassShift == mozilla::CeilingLog2(MinFreeRegionSize));
if (log2Size < MinSizeClassShift) {
return 0;
}
size_t sizeClass = log2Size - MinSizeClassShift;
MOZ_ASSERT(sizeClass <= MaxSmallAllocClass);
return sizeClass;
}
/* static */
size_t BufferAllocator::SizeClassForMediumAlloc(size_t bytes) {
MOZ_ASSERT(bytes >= MinMediumAllocSize);
MOZ_ASSERT(bytes <= MaxMediumAllocSize);
size_t log2Size = mozilla::CeilingLog2(bytes);
MOZ_ASSERT((size_t(
1) << log2Size) >= bytes);
MOZ_ASSERT(log2Size >= MinMediumAllocShift);
size_t sizeClass = log2Size - MinMediumAllocShift + MinMediumAllocClass;
MOZ_ASSERT(sizeClass >= MinMediumAllocClass);
MOZ_ASSERT(sizeClass < AllocSizeClasses);
return sizeClass;
}
/* static */
size_t BufferAllocator::SizeClassForFreeRegion(size_t bytes, SizeKind kind) {
MOZ_ASSERT(bytes >= MinFreeRegionSize);
MOZ_ASSERT(bytes < ChunkSize);
if (kind == SizeKind::Medium && bytes >= MaxMediumAllocSize) {
// Free regions large enough for MaxMediumAllocSize don't have to have
// enough space for that size rounded up to the next power of two, as is the
// case for smaller regions.
return MaxMediumAllocClass;
}
size_t log2Size = mozilla::FloorLog2(bytes);
MOZ_ASSERT((size_t(
1) << log2Size) <= bytes);
MOZ_ASSERT(log2Size >= MinSizeClassShift);
size_t sizeClass =
std::min(log2Size - MinSizeClassShift, AllocSizeClasses -
1);
if (kind == SizeKind::Small) {
return std::min(sizeClass, MaxSmallAllocClass);
}
sizeClass++;
// Medium size classes start after small ones.
MOZ_ASSERT(sizeClass >= MinMediumAllocClass);
MOZ_ASSERT(sizeClass < AllocSizeClasses);
return sizeClass;
}
/* static */
inline size_t BufferAllocator::SizeClassBytes(size_t sizeClass) {
MOZ_ASSERT(sizeClass < AllocSizeClasses);
// The first medium size class is the same size as the last small size class.
if (sizeClass >= MinMediumAllocClass) {
sizeClass--;
}
return 1 << (sizeClass + MinSizeClassShift);
}
/* static */
bool BufferAllocator::IsMediumAlloc(
void* alloc) {
MOZ_ASSERT(IsBufferAlloc(alloc));
// Test for large buffers before calling this so we can assume |alloc| is
// inside a chunk.
MOZ_ASSERT(!IsLargeAlloc(alloc));
BufferChunk* chunk = BufferChunk::from(alloc);
return !chunk->isSmallBufferRegion(alloc);
}
bool BufferAllocator::needLockToAccessBufferMap()
const {
MOZ_ASSERT(CurrentThreadCanAccessZone(zone) || CurrentThreadIsPerformingGC());
return runtime()->needLockToAccessBufferMap();
}
LargeBuffer* BufferAllocator::lookupLargeBuffer(
void* alloc) {
MaybeLock lock;
return lookupLargeBuffer(alloc, lock);
}
LargeBuffer* BufferAllocator::lookupLargeBuffer(
void* alloc, MaybeLock& lock) {
LargeBuffer* buffer = runtime()->lookupLargeBuffer(alloc, lock);
MOZ_ASSERT(buffer->zoneFromAnyThread() == zone);
return buffer;
}
void* BufferAllocator::allocLarge(size_t requestedBytes,
bool nurseryOwned,
bool inGC) {
size_t bytes = RoundUp(requestedBytes, ChunkSize);
if (MOZ_UNLIKELY(bytes < requestedBytes)) {
return nullptr;
}
MOZ_ASSERT(bytes > MaxMediumAllocSize);
// Allocate a small buffer the size of a LargeBuffer to hold the metadata.
static_assert(
sizeof(LargeBuffer) <= MaxSmallAllocSize);
void* bufferPtr = allocSmall(
sizeof(LargeBuffer), nurseryOwned, inGC);
if (!bufferPtr) {
return nullptr;
}
// Large allocations are aligned to the chunk size, even if they are smaller
// than a chunk. This allows us to tell large buffer allocations apart by
// looking at the pointer alignment.
void* alloc = MapAlignedPages(bytes, ChunkSize, ShouldStallAndRetry(inGC));
if (!alloc) {
return nullptr;
}
auto freeGuard = mozilla::MakeScopeExit([&]() { UnmapPages(alloc, bytes); });
CheckHighBitsOfPointer(alloc);
auto* buffer =
new (bufferPtr) LargeBuffer(alloc, bytes, nurseryOwned);
{
MaybeLock lock;
if (needLockToAccessBufferMap()) {
lock.emplace(runtime());
}
if (!runtime()->largeAllocMap.ref().putNew(alloc, buffer)) {
return nullptr;
}
}
freeGuard.release();
if (nurseryOwned) {
largeNurseryAllocs.ref().pushBack(buffer);
}
else {
buffer->allocatedDuringCollection = majorState != State::NotCollecting;
largeTenuredAllocs.ref().pushBack(buffer);
}
// Update memory accounting and trigger an incremental slice if needed.
bool checkThresholds = !inGC;
increaseHeapSize(bytes, nurseryOwned, checkThresholds,
false);
MOZ_ASSERT(IsLargeAlloc(alloc));
return alloc;
}
void BufferAllocator::increaseHeapSize(size_t bytes,
bool nurseryOwned,
bool checkThresholds,
bool updateRetainedSize) {
// Update memory accounting and trigger an incremental slice if needed.
// TODO: This will eventually be attributed to gcHeapSize.
if (nurseryOwned) {
gc->nursery().addMallocedBufferBytes(bytes);
}
else {
zone->mallocHeapSize.addBytes(bytes, updateRetainedSize);
if (checkThresholds) {
gc->maybeTriggerGCAfterMalloc(zone);
}
}
}
void BufferAllocator::decreaseHeapSize(size_t bytes,
bool nurseryOwned,
bool updateRetainedSize) {
if (nurseryOwned) {
gc->nursery().removeMallocedBufferBytes(bytes);
}
else {
zone->mallocHeapSize.removeBytes(bytes, updateRetainedSize);
}
}
/* static */
bool BufferAllocator::IsLargeAlloc(
void* alloc) {
return (uintptr_t(alloc) & ChunkMask) ==
0;
}
bool BufferAllocator::markLargeTenuredBuffer(LargeBuffer* buffer) {
MOZ_ASSERT(!buffer->isNurseryOwned);
if (buffer->allocatedDuringCollection) {
return false;
}
// Also mark the allocation containing the LargeBuffer.
markSmallTenuredAlloc(buffer);
return buffer->isMarked.compareExchange(
false, true);
}
bool BufferAllocator::isLargeTenuredMarked(LargeBuffer* buffer) {
MOZ_ASSERT(!buffer->isNurseryOwned);
MOZ_ASSERT(buffer->zoneFromAnyThread() == zone);
MOZ_ASSERT(!buffer->isInList());
return buffer->isMarked;
}
void BufferAllocator::freeLarge(
void* alloc) {
MaybeLock lock;
LargeBuffer* buffer = lookupLargeBuffer(alloc, lock);
MOZ_ASSERT(buffer->zoneFromAnyThread() == zone);
// Don't free data that may be accessed by concurrent marking.
if (isConcurrentMarking()) {
return;
}
DebugOnlyPoison(alloc, JS_FREED_BUFFER_PATTERN, buffer->allocBytes(),
MemCheckKind::MakeUndefined);
if (!buffer->isNurseryOwned && majorState == State::Sweeping &&
!buffer->allocatedDuringCollection) {
return;
// Large allocations are currently being swept.
}
MOZ_ASSERT(buffer->isInList());
if (buffer->isNurseryOwned) {
largeNurseryAllocs.ref().remove(buffer);
}
else if (majorState == State::Marking &&
!buffer->allocatedDuringCollection) {
largeTenuredAllocsToSweep.ref().remove(buffer);
}
else {
largeTenuredAllocs.ref().remove(buffer);
}
unmapLarge(buffer,
false, lock);
}
bool BufferAllocator::shrinkLarge(LargeBuffer* buffer, size_t newBytes) {
MOZ_ASSERT(IsLargeAllocSize(newBytes));
#ifdef XP_WIN
// Can't unmap part of a region mapped with VirtualAlloc on Windows.
//
// It is possible to decommit the physical pages so we could do that and
// track virtual size as well as committed size. This would also allow us to
// grow the allocation again if necessary.
return false;
#else
MOZ_ASSERT(buffer->zone() == zone);
// Don't free data that may be accessed by concurrent marking.
if (isConcurrentMarking()) {
return false;
}
if (!buffer->isNurseryOwned && majorState == State::Sweeping &&
!buffer->allocatedDuringCollection) {
return false;
// Large allocations are currently being swept.
}
MOZ_ASSERT(buffer->isInList());
newBytes = RoundUp(newBytes, ChunkSize);
size_t oldBytes = buffer->bytes;
MOZ_ASSERT(oldBytes > newBytes);
size_t shrinkBytes = oldBytes - newBytes;
decreaseHeapSize(shrinkBytes, buffer->isNurseryOwned,
false);
buffer->bytes = newBytes;
void* endPtr = reinterpret_cast<
void*>(uintptr_t(buffer->data()) + newBytes);
UnmapPages(endPtr, shrinkBytes);
return true;
#endif
}
void BufferAllocator::unmapLarge(LargeBuffer* buffer,
bool isSweeping,
MaybeLock& lock) {
unregisterLarge(buffer, isSweeping, lock);
UnmapPages(buffer->data(), buffer->bytes);
}
void BufferAllocator::unregisterLarge(LargeBuffer* buffer,
bool isSweeping,
MaybeLock& lock) {
MOZ_ASSERT(buffer->zoneFromAnyThread() == zone);
MOZ_ASSERT(!buffer->isInList());
MOZ_ASSERT_IF(isSweeping || needLockToAccessBufferMap(), lock.isSome());
#ifdef DEBUG
auto ptr = runtime()->largeAllocMap.ref().lookup(buffer->data());
MOZ_ASSERT(ptr && ptr->value() == buffer);
#endif
runtime()->largeAllocMap.ref().remove(buffer->data());
// Drop the lock now we've updated the map.
lock.reset();
if (!buffer->isNurseryOwned || !isSweeping) {
decreaseHeapSize(buffer->bytes, buffer->isNurseryOwned, isSweeping);
}
}
#include "js/Printer.h"
#include "util/GetPidProvider.h"
static const char*
const BufferAllocatorStatsPrefix =
"BufAllc:";
#define FOR_EACH_BUFFER_STATS_FIELD(_) \
_(
"PID",
7,
"%7zu", pid) \
_(
"Runtime",
14,
"0x%12p", runtime) \
_(
"Timestamp",
10,
"%10.6f", timestamp.ToSeconds()) \
_(
"Reason",
20,
"%-20.20s", reason) \
_(
"",
2,
"%2s",
"") \
_(
"TotalKB",
8,
"%8zu", totalBytes /
1024) \
_(
"UsedKB",
8,
"%8zu", stats.usedBytes /
1024) \
_(
"FreeKB",
8,
"%8zu", stats.freeBytes /
1024) \
_(
"Zs",
3,
"%3zu", zoneCount) \
_(
"",
7,
"%7s",
"") \
_(
"MixSRs",
6,
"%6zu", stats.mixedSmallRegions) \
_(
"TnrSRs",
6,
"%6zu", stats.tenuredSmallRegions) \
_(
"MixCs",
6,
"%6zu", stats.mixedChunks) \
_(
"TnrCs",
6,
"%6zu", stats.tenuredChunks) \
_(
"AMixCs",
6,
"%6zu", stats.availableMixedChunks) \
_(
"ATnrCs",
6,
"%6zu", stats.availableTenuredChunks) \
_(
"FreeRs",
6,
"%6zu", stats.freeRegions) \
_(
"LNurAs",
6,
"%6zu", stats.largeNurseryAllocs) \
_(
"LTnrAs",
6,
"%6zu", stats.largeTenuredAllocs)
/* static */
void BufferAllocator::printStatsHeader(FILE* file) {
Sprinter sprinter;
if (!sprinter.init()) {
return;
}
sprinter.put(BufferAllocatorStatsPrefix);
#define PRINT_METADATA_NAME(name, width, _
1, _
2) \
sprinter.printf(
" %-*s", width, name);
FOR_EACH_BUFFER_STATS_FIELD(PRINT_METADATA_NAME)
#undef PRINT_METADATA_NAME
sprinter.put(
"\n");
JS::UniqueChars str = sprinter.release();
if (!str) {
return;
}
fputs(str.get(), file);
}
/* static */
void BufferAllocator::printStats(GCRuntime* gc, mozilla::TimeStamp creationTime,
bool isMajorGC, FILE* file) {
Sprinter sprinter;
if (!sprinter.init()) {
return;
}
sprinter.put(BufferAllocatorStatsPrefix);
size_t pid = getpid();
JSRuntime* runtime = gc->rt;
mozilla::TimeDuration timestamp = mozilla::TimeStamp::Now() - creationTime;
const char* reason = isMajorGC ?
"post major GC" :
"pre minor GC";
size_t zoneCount =
0;
Stats stats;
for (AllZonesIter zone(gc); !zone.done(); zone.next()) {
zoneCount++;
zone->bufferAllocator.getStats(stats);
}
size_t totalBytes = stats.usedBytes + stats.freeBytes + stats.adminBytes;
#define PRINT_FIELD_VALUE(_
1, _
2, format, value) \
sprinter.printf(
" " format, value);
FOR_EACH_BUFFER_STATS_FIELD(PRINT_FIELD_VALUE)
#undef PRINT_FIELD_VALUE
sprinter.put(
"\n");
JS::UniqueChars str = sprinter.release();
if (!str) {
return;
}
fputs(str.get(), file);
}
size_t BufferAllocator::getSizeOfNurseryBuffers() {
checkMainThread();
maybeMergeSweptData();
MOZ_ASSERT(minorState == State::NotCollecting);
MOZ_ASSERT(majorState == State::NotCollecting);
size_t bytes =
0;
for (BufferChunk* chunk : mixedChunks.ref()) {
for (
auto alloc = chunk->allocIter(); !alloc.done(); alloc.next()) {
if (chunk->isNurseryOwned(alloc)) {
bytes += chunk->allocBytes(alloc);
}
}
}
for (
const LargeBuffer* buffer : largeNurseryAllocs.ref()) {
bytes += buffer->allocBytes();
}
return bytes;
}
void BufferAllocator::addBufferSizesAndCounts(
size_t* usedBytesOut, size_t* freeBytesOut, size_t* adminBytesOut,
size_t* totalChunksOut, size_t* freeRegionsOut, size_t* largeAllocsOut) {
checkMainThread();
maybeMergeSweptData();
MOZ_ASSERT(minorState == State::NotCollecting);
MOZ_ASSERT(majorState == State::NotCollecting);
Stats stats;
getStats(stats);
*usedBytesOut += stats.usedBytes;
*freeBytesOut += stats.freeBytes;
*adminBytesOut += stats.adminBytes;
*totalChunksOut += stats.mixedChunks + stats.tenuredChunks +
stats.availableMixedChunks + stats.availableTenuredChunks;
*freeRegionsOut += stats.freeRegions;
*largeAllocsOut += stats.largeNurseryAllocs + stats.largeTenuredAllocs;
}
void BufferAllocator::getStats(Stats& stats) {
checkMainThread();
maybeMergeSweptData();
MOZ_ASSERT(minorState == State::NotCollecting);
for (BufferChunk* chunk : mixedChunks.ref()) {
stats.mixedChunks++;
chunk->getStats(stats);
}
for (
auto chunk = availableMixedChunks.ref().chunkIter(); !chunk.done();
chunk.next()) {
stats.availableMixedChunks++;
chunk->getStats(stats);
}
for (BufferChunk* chunk : tenuredChunks.ref()) {
stats.tenuredChunks++;
chunk->getStats(stats);
}
for (
auto chunk = availableTenuredChunks.ref().chunkIter(); !chunk.done();
chunk.next()) {
stats.availableTenuredChunks++;
chunk->getStats(stats);
}
for (
const LargeBuffer* buffer : largeNurseryAllocs.ref()) {
stats.largeNurseryAllocs++;
stats.usedBytes += buffer->allocBytes();
stats.adminBytes +=
sizeof(LargeBuffer);
}
for (
const LargeBuffer* buffer : largeTenuredAllocs.ref()) {
stats.largeTenuredAllocs++;
stats.usedBytes += buffer->allocBytes();
stats.adminBytes +=
sizeof(LargeBuffer);
}
freeLists.ref().getStats(stats);
}
void BufferChunk::getStats(BufferAllocator::Stats& stats) {
stats.usedBytes += ChunkSize - FirstMediumAllocOffset;
stats.adminBytes += FirstMediumAllocOffset;
for (
auto iter = smallRegionIter(); !iter.done(); iter.next()) {
SmallBufferRegion* region = iter.get();
if (region->hasNurseryOwnedAllocs()) {
stats.mixedSmallRegions++;
}
else {
stats.tenuredSmallRegions++;
}
stats.usedBytes -= FirstSmallAllocOffset;
stats.adminBytes += FirstSmallAllocOffset;
}
if (ownsFreeLists) {
freeLists.ref().getStats(stats);
}
}
void BufferAllocator::FreeLists::getStats(Stats& stats) {
for (
auto region = freeRegionIter(); !region.done(); region.next()) {
stats.freeRegions++;
size_t size = region->size();
MOZ_ASSERT(stats.usedBytes >= size);
stats.usedBytes -= size;
stats.freeBytes += size;
}
}