/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * 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/Allocator.h" #include "mozilla/DebugOnly.h" #include "mozilla/TimeStamp.h" #include #include "gc/GCInternals.h" #include "gc/GCLock.h" #include "gc/GCProbes.h" #include "gc/Nursery.h" #include "threading/CpuCount.h" #include "util/Poison.h" #include "vm/JSContext.h" #include "vm/Runtime.h" #include "vm/StringType.h" #include "vm/TraceLogging.h" #include "gc/ArenaList-inl.h" #include "gc/Heap-inl.h" #include "gc/PrivateIterators-inl.h" #include "vm/JSObject-inl.h" using mozilla::TimeDuration; using mozilla::TimeStamp; using namespace js; using namespace gc; template JSObject* js::AllocateObject(JSContext* cx, AllocKind kind, size_t nDynamicSlots, InitialHeap heap, const JSClass* clasp) { MOZ_ASSERT(IsObjectAllocKind(kind)); size_t thingSize = Arena::thingSize(kind); MOZ_ASSERT(thingSize == Arena::thingSize(kind)); MOZ_ASSERT(thingSize >= sizeof(JSObject_Slots0)); static_assert( sizeof(JSObject_Slots0) >= MinCellSize, "All allocations must be at least the allocator-imposed minimum size."); MOZ_ASSERT_IF(nDynamicSlots != 0, clasp->isNative()); // We cannot trigger GC or make runtime assertions when nursery allocation // is suppressed, either explicitly or because we are off-thread. if (cx->isNurseryAllocSuppressed()) { JSObject* obj = GCRuntime::tryNewTenuredObject(cx, kind, thingSize, nDynamicSlots); if (MOZ_UNLIKELY(allowGC && !obj)) { ReportOutOfMemory(cx); } return obj; } JSRuntime* rt = cx->runtime(); if (!rt->gc.checkAllocatorState(cx, kind)) { return nullptr; } if (cx->nursery().isEnabled() && heap != TenuredHeap) { JSObject* obj = rt->gc.tryNewNurseryObject(cx, thingSize, nDynamicSlots, clasp); if (obj) { return obj; } // Our most common non-jit allocation path is NoGC; thus, if we fail the // alloc and cannot GC, we *must* return nullptr here so that the caller // will do a CanGC allocation to clear the nursery. Failing to do so will // cause all allocations on this path to land in Tenured, and we will not // get the benefit of the nursery. if (!allowGC) { return nullptr; } } return GCRuntime::tryNewTenuredObject(cx, kind, thingSize, nDynamicSlots); } template JSObject* js::AllocateObject(JSContext* cx, gc::AllocKind kind, size_t nDynamicSlots, gc::InitialHeap heap, const JSClass* clasp); template JSObject* js::AllocateObject(JSContext* cx, gc::AllocKind kind, size_t nDynamicSlots, gc::InitialHeap heap, const JSClass* clasp); // Attempt to allocate a new JSObject out of the nursery. If there is not // enough room in the nursery or there is an OOM, this method will return // nullptr. template JSObject* GCRuntime::tryNewNurseryObject(JSContext* cx, size_t thingSize, size_t nDynamicSlots, const JSClass* clasp) { MOZ_RELEASE_ASSERT(!cx->isHelperThreadContext()); MOZ_ASSERT(cx->isNurseryAllocAllowed()); MOZ_ASSERT(!cx->isNurseryAllocSuppressed()); MOZ_ASSERT(!cx->zone()->isAtomsZone()); JSObject* obj = cx->nursery().allocateObject(cx, thingSize, nDynamicSlots, clasp); if (obj) { return obj; } if (allowGC && !cx->suppressGC) { cx->runtime()->gc.minorGC(JS::GCReason::OUT_OF_NURSERY); // Exceeding gcMaxBytes while tenuring can disable the Nursery. if (cx->nursery().isEnabled()) { return cx->nursery().allocateObject(cx, thingSize, nDynamicSlots, clasp); } } return nullptr; } template JSObject* GCRuntime::tryNewTenuredObject(JSContext* cx, AllocKind kind, size_t thingSize, size_t nDynamicSlots) { ObjectSlots* slotsHeader = nullptr; if (nDynamicSlots) { HeapSlot* allocation = cx->maybe_pod_malloc(ObjectSlots::allocCount(nDynamicSlots)); if (MOZ_UNLIKELY(!allocation)) { if (allowGC) { ReportOutOfMemory(cx); } return nullptr; } slotsHeader = new (allocation) ObjectSlots(nDynamicSlots, 0); Debug_SetSlotRangeToCrashOnTouch(slotsHeader->slots(), nDynamicSlots); } JSObject* obj = tryNewTenuredThing(cx, kind, thingSize); if (obj) { if (nDynamicSlots) { static_cast(obj)->initSlots(slotsHeader->slots()); AddCellMemory(obj, ObjectSlots::allocSize(nDynamicSlots), MemoryUse::ObjectSlots); } } else { js_free(slotsHeader); } return obj; } // Attempt to allocate a new string out of the nursery. If there is not enough // room in the nursery or there is an OOM, this method will return nullptr. template JSString* GCRuntime::tryNewNurseryString(JSContext* cx, size_t thingSize, AllocKind kind) { MOZ_ASSERT(IsNurseryAllocable(kind)); MOZ_ASSERT(cx->isNurseryAllocAllowed()); MOZ_ASSERT(!cx->isHelperThreadContext()); MOZ_ASSERT(!cx->isNurseryAllocSuppressed()); MOZ_ASSERT(!cx->zone()->isAtomsZone()); Cell* cell = cx->nursery().allocateString(cx->zone(), thingSize); if (cell) { return static_cast(cell); } if (allowGC && !cx->suppressGC) { cx->runtime()->gc.minorGC(JS::GCReason::OUT_OF_NURSERY); // Exceeding gcMaxBytes while tenuring can disable the Nursery, and // other heuristics can disable nursery strings for this zone. if (cx->nursery().isEnabled() && cx->zone()->allocNurseryStrings) { return static_cast( cx->nursery().allocateString(cx->zone(), thingSize)); } } return nullptr; } template StringAllocT* js::AllocateStringImpl(JSContext* cx, InitialHeap heap) { static_assert(std::is_convertible_v, "must be JSString derived"); AllocKind kind = MapTypeToFinalizeKind::kind; size_t size = sizeof(StringAllocT); MOZ_ASSERT(size == Arena::thingSize(kind)); MOZ_ASSERT(size == sizeof(JSString) || size == sizeof(JSFatInlineString)); // Off-thread alloc cannot trigger GC or make runtime assertions. if (cx->isNurseryAllocSuppressed()) { StringAllocT* str = GCRuntime::tryNewTenuredThing(cx, kind, size); if (MOZ_UNLIKELY(allowGC && !str)) { ReportOutOfMemory(cx); } return str; } JSRuntime* rt = cx->runtime(); if (!rt->gc.checkAllocatorState(cx, kind)) { return nullptr; } if (cx->nursery().isEnabled() && heap != TenuredHeap && cx->nursery().canAllocateStrings() && cx->zone()->allocNurseryStrings) { auto* str = static_cast( rt->gc.tryNewNurseryString(cx, size, kind)); if (str) { return str; } // Our most common non-jit allocation path is NoGC; thus, if we fail the // alloc and cannot GC, we *must* return nullptr here so that the caller // will do a CanGC allocation to clear the nursery. Failing to do so will // cause all allocations on this path to land in Tenured, and we will not // get the benefit of the nursery. if (!allowGC) { return nullptr; } } return GCRuntime::tryNewTenuredThing(cx, kind, size); } // Attempt to allocate a new BigInt out of the nursery. If there is not enough // room in the nursery or there is an OOM, this method will return nullptr. template JS::BigInt* GCRuntime::tryNewNurseryBigInt(JSContext* cx, size_t thingSize, AllocKind kind) { MOZ_ASSERT(IsNurseryAllocable(kind)); MOZ_ASSERT(cx->isNurseryAllocAllowed()); MOZ_ASSERT(!cx->isHelperThreadContext()); MOZ_ASSERT(!cx->isNurseryAllocSuppressed()); MOZ_ASSERT(!cx->zone()->isAtomsZone()); Cell* cell = cx->nursery().allocateBigInt(cx->zone(), thingSize); if (cell) { return static_cast(cell); } if (allowGC && !cx->suppressGC) { cx->runtime()->gc.minorGC(JS::GCReason::OUT_OF_NURSERY); // Exceeding gcMaxBytes while tenuring can disable the Nursery, and // other heuristics can disable nursery BigInts for this zone. if (cx->nursery().isEnabled() && cx->zone()->allocNurseryBigInts) { return static_cast( cx->nursery().allocateBigInt(cx->zone(), thingSize)); } } return nullptr; } template JS::BigInt* js::AllocateBigInt(JSContext* cx, InitialHeap heap) { AllocKind kind = MapTypeToFinalizeKind::kind; size_t size = sizeof(JS::BigInt); MOZ_ASSERT(size == Arena::thingSize(kind)); // Off-thread alloc cannot trigger GC or make runtime assertions. if (cx->isNurseryAllocSuppressed()) { JS::BigInt* bi = GCRuntime::tryNewTenuredThing(cx, kind, size); if (MOZ_UNLIKELY(allowGC && !bi)) { ReportOutOfMemory(cx); } return bi; } JSRuntime* rt = cx->runtime(); if (!rt->gc.checkAllocatorState(cx, kind)) { return nullptr; } if (cx->nursery().isEnabled() && heap != TenuredHeap && cx->nursery().canAllocateBigInts() && cx->zone()->allocNurseryBigInts) { auto* bi = static_cast( rt->gc.tryNewNurseryBigInt(cx, size, kind)); if (bi) { return bi; } // Our most common non-jit allocation path is NoGC; thus, if we fail the // alloc and cannot GC, we *must* return nullptr here so that the caller // will do a CanGC allocation to clear the nursery. Failing to do so will // cause all allocations on this path to land in Tenured, and we will not // get the benefit of the nursery. if (!allowGC) { return nullptr; } } return GCRuntime::tryNewTenuredThing(cx, kind, size); } template JS::BigInt* js::AllocateBigInt(JSContext* cx, gc::InitialHeap heap); template JS::BigInt* js::AllocateBigInt(JSContext* cx, gc::InitialHeap heap); #define DECL_ALLOCATOR_INSTANCES(allocKind, traceKind, type, sizedType, \ bgfinal, nursery, compact) \ template type* js::AllocateStringImpl(JSContext * cx, \ InitialHeap heap); \ template type* js::AllocateStringImpl(JSContext * cx, \ InitialHeap heap); FOR_EACH_NURSERY_STRING_ALLOCKIND(DECL_ALLOCATOR_INSTANCES) #undef DECL_ALLOCATOR_INSTANCES template T* js::Allocate(JSContext* cx) { static_assert(!std::is_convertible_v, "must not be JSObject derived"); static_assert( sizeof(T) >= MinCellSize, "All allocations must be at least the allocator-imposed minimum size."); AllocKind kind = MapTypeToFinalizeKind::kind; size_t thingSize = sizeof(T); MOZ_ASSERT(thingSize == Arena::thingSize(kind)); if (!cx->isHelperThreadContext()) { if (!cx->runtime()->gc.checkAllocatorState(cx, kind)) { return nullptr; } } return GCRuntime::tryNewTenuredThing(cx, kind, thingSize); } #define DECL_ALLOCATOR_INSTANCES(allocKind, traceKind, type, sizedType, \ bgFinal, nursery, compact) \ template type* js::Allocate(JSContext * cx); \ template type* js::Allocate(JSContext * cx); FOR_EACH_NONOBJECT_NONNURSERY_ALLOCKIND(DECL_ALLOCATOR_INSTANCES) #undef DECL_ALLOCATOR_INSTANCES template /* static */ T* GCRuntime::tryNewTenuredThing(JSContext* cx, AllocKind kind, size_t thingSize) { // Bump allocate in the arena's current free-list span. auto* t = reinterpret_cast(cx->freeLists().allocate(kind)); if (MOZ_UNLIKELY(!t)) { // Get the next available free list and allocate out of it. This may // acquire a new arena, which will lock the chunk list. If there are no // chunks available it may also allocate new memory directly. t = reinterpret_cast(refillFreeListFromAnyThread(cx, kind)); if (MOZ_UNLIKELY(!t)) { if (allowGC) { cx->runtime()->gc.attemptLastDitchGC(cx); t = tryNewTenuredThing(cx, kind, thingSize); } if (!t) { if (allowGC) { ReportOutOfMemory(cx); } return nullptr; } } } checkIncrementalZoneState(cx, t); gcprobes::TenuredAlloc(t, kind); // We count this regardless of the profiler's state, assuming that it costs // just as much to count it, as to check the profiler's state and decide not // to count it. cx->noteTenuredAlloc(); return t; } void GCRuntime::attemptLastDitchGC(JSContext* cx) { // Either there was no memory available for a new chunk or the heap hit its // size limit. Try to perform an all-compartments, non-incremental, shrinking // GC and wait for it to finish. if (cx->isHelperThreadContext()) { return; } if (!lastLastDitchTime.IsNull() && TimeStamp::Now() - lastLastDitchTime <= tunables.minLastDitchGCPeriod()) { return; } JS::PrepareForFullGC(cx); gc(GC_SHRINK, JS::GCReason::LAST_DITCH); waitBackgroundAllocEnd(); waitBackgroundFreeEnd(); lastLastDitchTime = mozilla::TimeStamp::Now(); } template bool GCRuntime::checkAllocatorState(JSContext* cx, AllocKind kind) { if (allowGC) { if (!gcIfNeededAtAllocation(cx)) { return false; } } #if defined(JS_GC_ZEAL) || defined(DEBUG) MOZ_ASSERT_IF(cx->zone()->isAtomsZone(), kind == AllocKind::ATOM || kind == AllocKind::FAT_INLINE_ATOM || kind == AllocKind::SYMBOL || kind == AllocKind::JITCODE || kind == AllocKind::SCOPE); MOZ_ASSERT_IF(!cx->zone()->isAtomsZone(), kind != AllocKind::ATOM && kind != AllocKind::FAT_INLINE_ATOM); MOZ_ASSERT_IF(cx->zone()->isSelfHostingZone(), !rt->parentRuntime && !selfHostingZoneFrozen); MOZ_ASSERT(!JS::RuntimeHeapIsBusy()); #endif // Crash if we perform a GC action when it is not safe. if (allowGC && !cx->suppressGC) { cx->verifyIsSafeToGC(); } // For testing out of memory conditions if (js::oom::ShouldFailWithOOM()) { // If we are doing a fallible allocation, percolate up the OOM // instead of reporting it. if (allowGC) { ReportOutOfMemory(cx); } return false; } return true; } inline bool GCRuntime::gcIfNeededAtAllocation(JSContext* cx) { #ifdef JS_GC_ZEAL if (needZealousGC()) { runDebugGC(); } #endif // Invoking the interrupt callback can fail and we can't usefully // handle that here. Just check in case we need to collect instead. if (cx->hasAnyPendingInterrupt()) { gcIfRequested(); } return true; } template /* static */ void GCRuntime::checkIncrementalZoneState(JSContext* cx, T* t) { #ifdef DEBUG if (cx->isHelperThreadContext() || !t) { return; } TenuredCell* cell = &t->asTenured(); Zone* zone = cell->zone(); if (zone->isGCMarkingOrSweeping()) { MOZ_ASSERT(cell->isMarkedBlack()); } else { MOZ_ASSERT(!cell->isMarkedAny()); } #endif } TenuredCell* js::gc::AllocateCellInGC(Zone* zone, AllocKind thingKind) { TenuredCell* cell = zone->arenas.allocateFromFreeList(thingKind); if (!cell) { AutoEnterOOMUnsafeRegion oomUnsafe; cell = GCRuntime::refillFreeListInGC(zone, thingKind); if (!cell) { oomUnsafe.crash(ChunkSize, "Failed not allocate new chunk during GC"); } } return cell; } // /////////// Arena -> Thing Allocator ////////////////////////////////////// void GCRuntime::startBackgroundAllocTaskIfIdle() { AutoLockHelperThreadState lock; if (!allocTask.wasStarted(lock)) { // Join the previous invocation of the task. This will return immediately // if the thread has never been started. allocTask.joinWithLockHeld(lock); allocTask.startWithLockHeld(lock); } } /* static */ TenuredCell* GCRuntime::refillFreeListFromAnyThread(JSContext* cx, AllocKind thingKind) { MOZ_ASSERT(cx->freeLists().isEmpty(thingKind)); if (!cx->isHelperThreadContext()) { return refillFreeListFromMainThread(cx, thingKind); } return refillFreeListFromHelperThread(cx, thingKind); } /* static */ TenuredCell* GCRuntime::refillFreeListFromMainThread(JSContext* cx, AllocKind thingKind) { // It should not be possible to allocate on the main thread while we are // inside a GC. MOZ_ASSERT(!JS::RuntimeHeapIsBusy(), "allocating while under GC"); return cx->zone()->arenas.refillFreeListAndAllocate( cx->freeLists(), thingKind, ShouldCheckThresholds::CheckThresholds); } /* static */ TenuredCell* GCRuntime::refillFreeListFromHelperThread(JSContext* cx, AllocKind thingKind) { // A GC may be happening on the main thread, but zones used by off thread // tasks are never collected. Zone* zone = cx->zone(); MOZ_ASSERT(!zone->wasGCStarted()); return zone->arenas.refillFreeListAndAllocate( cx->freeLists(), thingKind, ShouldCheckThresholds::CheckThresholds); } /* static */ TenuredCell* GCRuntime::refillFreeListInGC(Zone* zone, AllocKind thingKind) { // Called by compacting GC to refill a free list while we are in a GC. MOZ_ASSERT(JS::RuntimeHeapIsCollecting()); MOZ_ASSERT_IF(!JS::RuntimeHeapIsMinorCollecting(), !zone->runtimeFromMainThread()->gc.isBackgroundSweeping()); return zone->arenas.refillFreeListAndAllocate( zone->arenas.freeLists(), thingKind, ShouldCheckThresholds::DontCheckThresholds); } TenuredCell* ArenaLists::refillFreeListAndAllocate( FreeLists& freeLists, AllocKind thingKind, ShouldCheckThresholds checkThresholds) { MOZ_ASSERT(freeLists.isEmpty(thingKind)); JSRuntime* rt = runtimeFromAnyThread(); mozilla::Maybe maybeLock; // See if we can proceed without taking the GC lock. if (concurrentUse(thingKind) != ConcurrentUse::None) { maybeLock.emplace(rt); } Arena* arena = arenaList(thingKind).takeNextArena(); if (arena) { // Empty arenas should be immediately freed. MOZ_ASSERT(!arena->isEmpty()); return freeLists.setArenaAndAllocate(arena, thingKind); } // Parallel threads have their own ArenaLists, but chunks are shared; // if we haven't already, take the GC lock now to avoid racing. if (maybeLock.isNothing()) { maybeLock.emplace(rt); } TenuredChunk* chunk = rt->gc.pickChunk(maybeLock.ref()); if (!chunk) { return nullptr; } // Although our chunk should definitely have enough space for another arena, // there are other valid reasons why TenuredChunk::allocateArena() may fail. arena = rt->gc.allocateArena(chunk, zone_, thingKind, checkThresholds, maybeLock.ref()); if (!arena) { return nullptr; } addNewArena(arena, thingKind); return freeLists.setArenaAndAllocate(arena, thingKind); } inline void ArenaLists::addNewArena(Arena* arena, AllocKind thingKind) { ArenaList& al = zone_->isGCMarking() ? newArenasInMarkPhase(thingKind) : arenaList(thingKind); MOZ_ASSERT(al.isCursorAtEnd()); al.insertBeforeCursor(arena); } inline TenuredCell* FreeLists::setArenaAndAllocate(Arena* arena, AllocKind kind) { #ifdef DEBUG auto old = freeLists_[kind]; if (!old->isEmpty()) { old->getArena()->checkNoMarkedFreeCells(); } #endif FreeSpan* span = arena->getFirstFreeSpan(); freeLists_[kind] = span; Zone* zone = arena->zone; if (MOZ_UNLIKELY(zone->isGCMarkingOrSweeping())) { arena->arenaAllocatedDuringGC(); } TenuredCell* thing = span->allocate(Arena::thingSize(kind)); MOZ_ASSERT(thing); // This allocation is infallible. return thing; } void Arena::arenaAllocatedDuringGC() { // Ensure that anything allocated during the mark or sweep phases of an // incremental GC will be marked black by pre-marking all free cells in the // arena we are about to allocate from. MOZ_ASSERT(zone->isGCMarkingOrSweeping()); for (ArenaFreeCellIter cell(this); !cell.done(); cell.next()) { MOZ_ASSERT(!cell->isMarkedAny()); cell->markBlack(); } } void GCRuntime::setParallelAtomsAllocEnabled(bool enabled) { // This can only be changed on the main thread otherwise we could race. MOZ_ASSERT(CurrentThreadCanAccessRuntime(rt)); MOZ_ASSERT(enabled == rt->hasHelperThreadZones()); atomsZone->arenas.setParallelAllocEnabled(enabled); } void ArenaLists::setParallelAllocEnabled(bool enabled) { MOZ_ASSERT(zone_->isAtomsZone()); static const ConcurrentUse states[2] = {ConcurrentUse::None, ConcurrentUse::ParallelAlloc}; for (auto kind : AllAllocKinds()) { MOZ_ASSERT(concurrentUse(kind) == states[!enabled]); concurrentUse(kind) = states[enabled]; } } void GCRuntime::setParallelUnmarkEnabled(bool enabled) { // This can only be changed on the main thread otherwise we could race. MOZ_ASSERT(CurrentThreadCanAccessRuntime(rt)); MOZ_ASSERT(JS::RuntimeHeapIsMajorCollecting()); for (GCZonesIter zone(this); !zone.done(); zone.next()) { zone->arenas.setParallelUnmarkEnabled(enabled); } } void ArenaLists::setParallelUnmarkEnabled(bool enabled) { static const ConcurrentUse states[2] = {ConcurrentUse::None, ConcurrentUse::ParallelUnmark}; for (auto kind : AllAllocKinds()) { MOZ_ASSERT(concurrentUse(kind) == states[!enabled]); concurrentUse(kind) = states[enabled]; } } // /////////// TenuredChunk -> Arena Allocator /////////////////////////////// bool GCRuntime::wantBackgroundAllocation(const AutoLockGC& lock) const { // To minimize memory waste, we do not want to run the background chunk // allocation if we already have some empty chunks or when the runtime has // a small heap size (and therefore likely has a small growth rate). return allocTask.enabled() && emptyChunks(lock).count() < tunables.minEmptyChunkCount(lock) && (fullChunks(lock).count() + availableChunks(lock).count()) >= 4; } Arena* GCRuntime::allocateArena(TenuredChunk* chunk, Zone* zone, AllocKind thingKind, ShouldCheckThresholds checkThresholds, const AutoLockGC& lock) { MOZ_ASSERT(chunk->hasAvailableArenas()); // Fail the allocation if we are over our heap size limits. if ((checkThresholds != ShouldCheckThresholds::DontCheckThresholds) && (heapSize.bytes() >= tunables.gcMaxBytes())) return nullptr; Arena* arena = chunk->allocateArena(this, zone, thingKind, lock); zone->gcHeapSize.addGCArena(); // Trigger an incremental slice if needed. if (checkThresholds != ShouldCheckThresholds::DontCheckThresholds) { maybeTriggerGCAfterAlloc(zone); } return arena; } Arena* TenuredChunk::allocateArena(GCRuntime* gc, Zone* zone, AllocKind thingKind, const AutoLockGC& lock) { Arena* arena = info.numArenasFreeCommitted > 0 ? fetchNextFreeArena(gc) : fetchNextDecommittedArena(); arena->init(zone, thingKind, lock); updateChunkListAfterAlloc(gc, lock); return arena; } inline void GCRuntime::updateOnFreeArenaAlloc(const TenuredChunkInfo& info) { MOZ_ASSERT(info.numArenasFreeCommitted <= numArenasFreeCommitted); --numArenasFreeCommitted; } Arena* TenuredChunk::fetchNextFreeArena(GCRuntime* gc) { MOZ_ASSERT(info.numArenasFreeCommitted > 0); MOZ_ASSERT(info.numArenasFreeCommitted <= info.numArenasFree); Arena* arena = info.freeArenasHead; info.freeArenasHead = arena->next; --info.numArenasFreeCommitted; --info.numArenasFree; gc->updateOnFreeArenaAlloc(info); return arena; } Arena* TenuredChunk::fetchNextDecommittedArena() { MOZ_ASSERT(info.numArenasFreeCommitted == 0); MOZ_ASSERT(info.numArenasFree > 0); unsigned offset = findDecommittedArenaOffset(); info.lastDecommittedArenaOffset = offset + 1; --info.numArenasFree; decommittedArenas[offset] = false; Arena* arena = &arenas[offset]; MarkPagesInUseSoft(arena, ArenaSize); arena->setAsNotAllocated(); return arena; } /* * Search for and return the next decommitted Arena. Our goal is to keep * lastDecommittedArenaOffset "close" to a free arena. We do this by setting * it to the most recently freed arena when we free, and forcing it to * the last alloc + 1 when we allocate. */ uint32_t TenuredChunk::findDecommittedArenaOffset() { /* Note: lastFreeArenaOffset can be past the end of the list. */ for (unsigned i = info.lastDecommittedArenaOffset; i < ArenasPerChunk; i++) { if (decommittedArenas[i]) { return i; } } for (unsigned i = 0; i < info.lastDecommittedArenaOffset; i++) { if (decommittedArenas[i]) { return i; } } MOZ_CRASH("No decommitted arenas found."); } // /////////// System -> TenuredChunk Allocator ////////////////////////////// TenuredChunk* GCRuntime::getOrAllocChunk(AutoLockGCBgAlloc& lock) { TenuredChunk* chunk = emptyChunks(lock).pop(); if (!chunk) { chunk = TenuredChunk::allocate(this); if (!chunk) { return nullptr; } MOZ_ASSERT(chunk->info.numArenasFreeCommitted == 0); } if (wantBackgroundAllocation(lock)) { lock.tryToStartBackgroundAllocation(); } return chunk; } void GCRuntime::recycleChunk(TenuredChunk* chunk, const AutoLockGC& lock) { AlwaysPoison(chunk, JS_FREED_CHUNK_PATTERN, sizeof(ChunkBase), MemCheckKind::MakeNoAccess); emptyChunks(lock).push(chunk); } TenuredChunk* GCRuntime::pickChunk(AutoLockGCBgAlloc& lock) { if (availableChunks(lock).count()) { return availableChunks(lock).head(); } TenuredChunk* chunk = getOrAllocChunk(lock); if (!chunk) { return nullptr; } chunk->init(this); MOZ_ASSERT(chunk->info.numArenasFreeCommitted == 0); MOZ_ASSERT(chunk->unused()); MOZ_ASSERT(!fullChunks(lock).contains(chunk)); MOZ_ASSERT(!availableChunks(lock).contains(chunk)); availableChunks(lock).push(chunk); return chunk; } BackgroundAllocTask::BackgroundAllocTask(GCRuntime* gc, ChunkPool& pool) : GCParallelTask(gc), chunkPool_(pool), enabled_(CanUseExtraThreads() && GetCPUCount() >= 2) {} void BackgroundAllocTask::run(AutoLockHelperThreadState& lock) { AutoUnlockHelperThreadState unlock(lock); TraceLoggerThread* logger = TraceLoggerForCurrentThread(); AutoTraceLog logAllocation(logger, TraceLogger_GCAllocation); AutoLockGC gcLock(gc); while (!isCancelled() && gc->wantBackgroundAllocation(gcLock)) { TenuredChunk* chunk; { AutoUnlockGC unlock(gcLock); chunk = TenuredChunk::allocate(gc); if (!chunk) { break; } chunk->init(gc); } chunkPool_.ref().push(chunk); } } /* static */ TenuredChunk* TenuredChunk::allocate(GCRuntime* gc) { void* chunk = MapAlignedPages(ChunkSize, ChunkSize); if (!chunk) { return nullptr; } gc->stats().count(gcstats::COUNT_NEW_CHUNK); return static_cast(chunk); } void TenuredChunk::init(GCRuntime* gc) { /* The chunk may still have some regions marked as no-access. */ MOZ_MAKE_MEM_UNDEFINED(this, ChunkSize); /* * Poison the chunk. Note that decommitAllArenas() below will mark the * arenas as inaccessible (for memory sanitizers). */ Poison(this, JS_FRESH_TENURED_PATTERN, ChunkSize, MemCheckKind::MakeUndefined); new (this) TenuredChunk(gc->rt); /* * Decommit the arenas. We do this after poisoning so that if the OS does * not have to recycle the pages, we still get the benefit of poisoning. */ decommitAllArenas(); /* The rest of info fields are initialized in pickChunk. */ } void TenuredChunk::decommitAllArenas() { decommittedArenas.SetAll(); MarkPagesUnusedSoft(&arenas[0], ArenasPerChunk * ArenaSize); info.freeArenasHead = nullptr; info.lastDecommittedArenaOffset = 0; info.numArenasFree = ArenasPerChunk; info.numArenasFreeCommitted = 0; }