summaryrefslogtreecommitdiffstats
path: root/js/src/gc/Allocator.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/gc/Allocator.cpp')
-rw-r--r--js/src/gc/Allocator.cpp886
1 files changed, 886 insertions, 0 deletions
diff --git a/js/src/gc/Allocator.cpp b/js/src/gc/Allocator.cpp
new file mode 100644
index 0000000000..a5fd34b3f0
--- /dev/null
+++ b/js/src/gc/Allocator.cpp
@@ -0,0 +1,886 @@
+/* -*- 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 <type_traits>
+
+#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 <AllowGC allowGC /* = CanGC */>
+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<NoGC>(cx, kind, thingSize,
+ nDynamicSlots);
+ if (MOZ_UNLIKELY(allowGC && !obj)) {
+ ReportOutOfMemory(cx);
+ }
+ return obj;
+ }
+
+ JSRuntime* rt = cx->runtime();
+ if (!rt->gc.checkAllocatorState<allowGC>(cx, kind)) {
+ return nullptr;
+ }
+
+ if (cx->nursery().isEnabled() && heap != TenuredHeap) {
+ JSObject* obj = rt->gc.tryNewNurseryObject<allowGC>(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<allowGC>(cx, kind, thingSize,
+ nDynamicSlots);
+}
+template JSObject* js::AllocateObject<NoGC>(JSContext* cx, gc::AllocKind kind,
+ size_t nDynamicSlots,
+ gc::InitialHeap heap,
+ const JSClass* clasp);
+template JSObject* js::AllocateObject<CanGC>(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 <AllowGC allowGC>
+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 <AllowGC allowGC>
+JSObject* GCRuntime::tryNewTenuredObject(JSContext* cx, AllocKind kind,
+ size_t thingSize,
+ size_t nDynamicSlots) {
+ ObjectSlots* slotsHeader = nullptr;
+ if (nDynamicSlots) {
+ HeapSlot* allocation =
+ cx->maybe_pod_malloc<HeapSlot>(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<JSObject, allowGC>(cx, kind, thingSize);
+
+ if (obj) {
+ if (nDynamicSlots) {
+ static_cast<NativeObject*>(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 <AllowGC allowGC>
+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<JSString*>(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<JSString*>(
+ cx->nursery().allocateString(cx->zone(), thingSize));
+ }
+ }
+ return nullptr;
+}
+
+template <typename StringAllocT, AllowGC allowGC /* = CanGC */>
+StringAllocT* js::AllocateStringImpl(JSContext* cx, InitialHeap heap) {
+ static_assert(std::is_convertible_v<StringAllocT*, JSString*>,
+ "must be JSString derived");
+
+ AllocKind kind = MapTypeToFinalizeKind<StringAllocT>::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<StringAllocT, NoGC>(cx, kind, size);
+ if (MOZ_UNLIKELY(allowGC && !str)) {
+ ReportOutOfMemory(cx);
+ }
+ return str;
+ }
+
+ JSRuntime* rt = cx->runtime();
+ if (!rt->gc.checkAllocatorState<allowGC>(cx, kind)) {
+ return nullptr;
+ }
+
+ if (cx->nursery().isEnabled() && heap != TenuredHeap &&
+ cx->nursery().canAllocateStrings() && cx->zone()->allocNurseryStrings) {
+ auto* str = static_cast<StringAllocT*>(
+ rt->gc.tryNewNurseryString<allowGC>(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<StringAllocT, allowGC>(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 <AllowGC allowGC>
+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<JS::BigInt*>(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<JS::BigInt*>(
+ cx->nursery().allocateBigInt(cx->zone(), thingSize));
+ }
+ }
+ return nullptr;
+}
+
+template <AllowGC allowGC /* = CanGC */>
+JS::BigInt* js::AllocateBigInt(JSContext* cx, InitialHeap heap) {
+ AllocKind kind = MapTypeToFinalizeKind<JS::BigInt>::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<JS::BigInt, NoGC>(cx, kind, size);
+ if (MOZ_UNLIKELY(allowGC && !bi)) {
+ ReportOutOfMemory(cx);
+ }
+ return bi;
+ }
+
+ JSRuntime* rt = cx->runtime();
+ if (!rt->gc.checkAllocatorState<allowGC>(cx, kind)) {
+ return nullptr;
+ }
+
+ if (cx->nursery().isEnabled() && heap != TenuredHeap &&
+ cx->nursery().canAllocateBigInts() && cx->zone()->allocNurseryBigInts) {
+ auto* bi = static_cast<JS::BigInt*>(
+ rt->gc.tryNewNurseryBigInt<allowGC>(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<JS::BigInt, allowGC>(cx, kind, size);
+}
+template JS::BigInt* js::AllocateBigInt<NoGC>(JSContext* cx,
+ gc::InitialHeap heap);
+template JS::BigInt* js::AllocateBigInt<CanGC>(JSContext* cx,
+ gc::InitialHeap heap);
+
+#define DECL_ALLOCATOR_INSTANCES(allocKind, traceKind, type, sizedType, \
+ bgfinal, nursery, compact) \
+ template type* js::AllocateStringImpl<type, NoGC>(JSContext * cx, \
+ InitialHeap heap); \
+ template type* js::AllocateStringImpl<type, CanGC>(JSContext * cx, \
+ InitialHeap heap);
+FOR_EACH_NURSERY_STRING_ALLOCKIND(DECL_ALLOCATOR_INSTANCES)
+#undef DECL_ALLOCATOR_INSTANCES
+
+template <typename T, AllowGC allowGC /* = CanGC */>
+T* js::Allocate(JSContext* cx) {
+ static_assert(!std::is_convertible_v<T*, JSObject*>,
+ "must not be JSObject derived");
+ static_assert(
+ sizeof(T) >= MinCellSize,
+ "All allocations must be at least the allocator-imposed minimum size.");
+
+ AllocKind kind = MapTypeToFinalizeKind<T>::kind;
+ size_t thingSize = sizeof(T);
+ MOZ_ASSERT(thingSize == Arena::thingSize(kind));
+
+ if (!cx->isHelperThreadContext()) {
+ if (!cx->runtime()->gc.checkAllocatorState<allowGC>(cx, kind)) {
+ return nullptr;
+ }
+ }
+
+ return GCRuntime::tryNewTenuredThing<T, allowGC>(cx, kind, thingSize);
+}
+
+#define DECL_ALLOCATOR_INSTANCES(allocKind, traceKind, type, sizedType, \
+ bgFinal, nursery, compact) \
+ template type* js::Allocate<type, NoGC>(JSContext * cx); \
+ template type* js::Allocate<type, CanGC>(JSContext * cx);
+FOR_EACH_NONOBJECT_NONNURSERY_ALLOCKIND(DECL_ALLOCATOR_INSTANCES)
+#undef DECL_ALLOCATOR_INSTANCES
+
+template <typename T, AllowGC allowGC>
+/* 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<T*>(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<T*>(refillFreeListFromAnyThread(cx, kind));
+
+ if (MOZ_UNLIKELY(!t)) {
+ if (allowGC) {
+ cx->runtime()->gc.attemptLastDitchGC(cx);
+ t = tryNewTenuredThing<T, NoGC>(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 <AllowGC allowGC>
+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 <typename T>
+/* 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<AutoLockGCBgAlloc> 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<TenuredChunk*>(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;
+}