lib/VM/gcs/HadesGC.cpp (2,254 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include "hermes/VM/HadesGC.h" #include "GCBase-WeakMap.h" #include "hermes/Support/Compiler.h" #include "hermes/Support/ErrorHandling.h" #include "hermes/VM/AllocResult.h" #include "hermes/VM/CheckHeapWellFormedAcceptor.h" #include "hermes/VM/FillerCell.h" #include "hermes/VM/GCBase-inline.h" #include "hermes/VM/GCPointer.h" #include "hermes/VM/RootAndSlotAcceptorDefault.h" #include <array> #include <functional> #include <stack> namespace hermes { namespace vm { static const char *kGCName = kConcurrentGC ? "hades (concurrent)" : "hades (incremental)"; static const char *kCompacteeNameForCrashMgr = "COMPACT"; // We have a target max pause time of 50ms. static constexpr size_t kTargetMaxPauseMs = 50; // A free list cell is always variable-sized. const VTable HadesGC::OldGen::FreelistCell::vt{ CellKind::FreelistKind, /*variableSize*/ 0}; void FreelistBuildMeta(const GCCell *cell, Metadata::Builder &mb) { mb.setVTable(&HadesGC::OldGen::FreelistCell::vt); } HadesGC::HeapSegment::HeapSegment(AlignedStorage storage) : AlignedHeapSegment{std::move(storage)} { // Make sure end() is at the maxSize. growToLimit(); } GCCell * HadesGC::OldGen::finishAlloc(GCCell *cell, uint32_t sz, uint16_t segmentIdx) { // Track the number of allocated bytes in a segment. incrementAllocatedBytes(sz, segmentIdx); // Write a mark bit so this entry doesn't get free'd by the sweeper. HeapSegment::setCellMarkBit(cell); // Could overwrite the VTable, but the allocator will write a new one in // anyway. return cell; } void HadesGC::OldGen::addCellToFreelist( void *addr, uint32_t sz, size_t segmentIdx) { assert( sz >= sizeof(FreelistCell) && "Cannot construct a FreelistCell into an allocation in the OG"); FreelistCell *newFreeCell = constructCell<FreelistCell>(addr, sz); HeapSegment::setCellHead(static_cast<GCCell *>(addr), sz); addCellToFreelist(newFreeCell, segmentIdx); } void HadesGC::OldGen::addCellToFreelist(FreelistCell *cell, size_t segmentIdx) { const size_t sz = cell->getAllocatedSize(); // Get the size bucket for the cell being added; const uint32_t bucket = getFreelistBucket(sz); // Push onto the size-specific free list for this bucket and segment. cell->next_ = freelistSegmentsBuckets_[segmentIdx][bucket]; freelistSegmentsBuckets_[segmentIdx][bucket] = CompressedPointer::encodeNonNull(cell, gc_->getPointerBase()); // Set a bit indicating that there are now available blocks in this segment // for the given bucket. freelistBucketSegmentBitArray_[bucket].set(segmentIdx); // Set a bit indicating that there are now available blocks for this bucket. freelistBucketBitArray_.set(bucket, true); // In ASAN builds, poison the memory outside of the FreelistCell so that // accesses are flagged as illegal while it is in the freelist. // Here, and in other places where FreelistCells are poisoned, use +1 on the // pointer to skip towards the memory region directly after the FreelistCell // header of a cell. This way the header is always intact and readable, and // only the contents of the cell are poisoned. __asan_poison_memory_region(cell + 1, sz - sizeof(FreelistCell)); } void HadesGC::OldGen::addCellToFreelistFromSweep( char *freeRangeStart, char *freeRangeEnd, bool setHead) { assert( gc_->concurrentPhase_ == Phase::Sweep && "addCellToFreelistFromSweep should only be called during sweeping."); size_t newCellSize = freeRangeEnd - freeRangeStart; // While coalescing, sweeping may generate new cells, so make sure the cell // head is updated. if (setHead) HeapSegment::setCellHead( reinterpret_cast<GCCell *>(freeRangeStart), newCellSize); auto *newCell = constructCell<FreelistCell>(freeRangeStart, newCellSize); // Get the size bucket for the cell being added; const uint32_t bucket = getFreelistBucket(newCellSize); // Push onto the size-specific free list for this bucket and segment. newCell->next_ = freelistSegmentsBuckets_[sweepIterator_.segNumber][bucket]; freelistSegmentsBuckets_[sweepIterator_.segNumber][bucket] = CompressedPointer::encodeNonNull(newCell, gc_->getPointerBase()); __asan_poison_memory_region(newCell + 1, newCellSize - sizeof(FreelistCell)); } HadesGC::OldGen::FreelistCell *HadesGC::OldGen::removeCellFromFreelist( size_t bucket, size_t segmentIdx) { return removeCellFromFreelist( &freelistSegmentsBuckets_[segmentIdx][bucket], bucket, segmentIdx); } HadesGC::OldGen::FreelistCell *HadesGC::OldGen::removeCellFromFreelist( AssignableCompressedPointer *prevLoc, size_t bucket, size_t segmentIdx) { FreelistCell *cell = vmcast<FreelistCell>(prevLoc->getNonNull(gc_->getPointerBase())); assert(cell && "Cannot get a null cell from freelist"); // Update whatever was pointing to the cell we are removing. *prevLoc = cell->next_; // Update the bit arrays if the given freelist is now empty. if (!freelistSegmentsBuckets_[segmentIdx][bucket]) { // Set the bit for this segment and bucket to 0. freelistBucketSegmentBitArray_[bucket].reset(segmentIdx); // If setting the bit to 0 above made this bucket empty for all segments, // set the bucket bit to 0 as well. freelistBucketBitArray_.set( bucket, !freelistBucketSegmentBitArray_[bucket].empty()); } // Unpoison the memory so that the mutator can use it. __asan_unpoison_memory_region( cell + 1, cell->getAllocatedSize() - sizeof(FreelistCell)); return cell; } void HadesGC::OldGen::eraseSegmentFreelists(size_t segmentIdx) { for (size_t bucket = 0; bucket < kNumFreelistBuckets; ++bucket) { freelistBucketSegmentBitArray_[bucket].reset(segmentIdx); auto iter = freelistBucketSegmentBitArray_[bucket].begin(); auto endIter = freelistBucketSegmentBitArray_[bucket].end(); while (iter != endIter && *iter <= segmentIdx) iter++; // Move all the set bits after segmentIdx down by 1. while (iter != endIter) { // Increment the iterator before manipulating values because the // manipulation may invalidate our current iterator. size_t idx = *iter++; freelistBucketSegmentBitArray_[bucket].set(idx - 1); freelistBucketSegmentBitArray_[bucket].reset(idx); } freelistBucketBitArray_.set( bucket, !freelistBucketSegmentBitArray_[bucket].empty()); } freelistSegmentsBuckets_.erase(freelistSegmentsBuckets_.begin() + segmentIdx); } /* static */ void HadesGC::HeapSegment::setCellHead( const GCCell *cellStart, const size_t sz) { const char *start = reinterpret_cast<const char *>(cellStart); const char *end = start + sz; CardTable *cards = cardTableCovering(start); auto boundary = cards->nextBoundary(start); // If this object crosses a card boundary, then update boundaries // appropriately. if (boundary.address() < end) { cards->updateBoundaries(&boundary, start, end); } } GCCell *HadesGC::HeapSegment::getFirstCellHead(size_t cardIdx) { CardTable &cards = cardTable(); GCCell *cell = cards.firstObjForCard(cardIdx); assert(cell->isValid() && "Object head doesn't point to a valid object"); return cell; } template <typename CallbackFunction> void HadesGC::HeapSegment::forAllObjs(CallbackFunction callback) { for (GCCell *cell : cells()) { // Skip free-list entries. if (!vmisa<OldGen::FreelistCell>(cell)) { callback(cell); } } } template <typename CallbackFunction> void HadesGC::HeapSegment::forCompactedObjs( CallbackFunction callback, PointerBase &base) { void *const stop = level(); GCCell *cell = reinterpret_cast<GCCell *>(start()); while (cell < stop) { if (cell->hasMarkedForwardingPointer()) { // This cell has been evacuated, do nothing. cell = reinterpret_cast<GCCell *>( reinterpret_cast<char *>(cell) + cell->getMarkedForwardingPointer() .getNonNull(base) ->getAllocatedSize()); } else { // This cell is being compacted away, call the callback on it. // NOTE: We do not check if it is a FreelistCell here in order to avoid // the extra overhead of that check in YG. The callback should add that // check if required. callback(cell); cell = cell->nextCell(); } } } GCCell *HadesGC::OldGen::FreelistCell::carve(uint32_t sz) { const auto origSize = getAllocatedSize(); assert( origSize >= sz + minAllocationSize() && "Can't split if it would leave too small of a second cell"); const auto finalSize = origSize - sz; char *newCellAddress = reinterpret_cast<char *>(this) + finalSize; GCCell *const newCell = reinterpret_cast<GCCell *>(newCellAddress); setSizeFromGC(finalSize); HeapSegment::setCellHead(newCell, sz); return newCell; } class HadesGC::CollectionStats final { public: using Clock = std::chrono::steady_clock; using TimePoint = std::chrono::time_point<Clock>; using Duration = std::chrono::microseconds; CollectionStats(HadesGC *gc, std::string cause, std::string collectionType) : gc_{gc}, cause_{std::move(cause)}, collectionType_{std::move(collectionType)} {} ~CollectionStats() { assert(usedDbg_ && "Stats not submitted."); } void addCollectionType(std::string collectionType) { if (std::find(tags_.begin(), tags_.end(), collectionType) == tags_.end()) tags_.emplace_back(std::move(collectionType)); } /// Record the allocated bytes in the heap and its size before a collection /// begins. void setBeforeSizes(uint64_t allocated, uint64_t external, uint64_t sz) { allocatedBefore_ = allocated; externalBefore_ = external; sizeBefore_ = sz; } /// Record how many bytes were swept during the collection. void setSweptBytes(uint64_t sweptBytes) { sweptBytes_ = sweptBytes; } void setSweptExternalBytes(uint64_t externalBytes) { sweptExternalBytes_ = externalBytes; } void setAfterSize(uint64_t sz) { sizeAfter_ = sz; } /// Record that a collection is beginning right now. void setBeginTime() { assert(beginTime_ == Clock::time_point{} && "Begin time already set"); beginTime_ = Clock::now(); } /// Record that a collection is ending right now. void setEndTime() { assert(endTime_ == Clock::time_point{} && "End time already set"); endTime_ = Clock::now(); } std::chrono::milliseconds getElapsedTime() { return std::chrono::duration_cast<std::chrono::milliseconds>( Clock::now() - beginTime_); } /// Record this amount of CPU time was taken. /// Call begin/end in each thread that does work to correctly count CPU time. /// NOTE: Can only be used by one thread at a time. void beginCPUTimeSection() { assert( cpuTimeSectionStart_ == Duration{} && "Must end cpu time section before starting another"); cpuTimeSectionStart_ = oscompat::thread_cpu_time(); } void endCPUTimeSection() { cpuDuration_ += oscompat::thread_cpu_time() - cpuTimeSectionStart_; cpuTimeSectionStart_ = {}; } uint64_t beforeAllocatedBytes() const { return allocatedBefore_; } /// Since Hades allows allocations during an old gen collection, use the /// initially allocated bytes and the swept bytes to determine the actual /// impact of the GC. uint64_t afterAllocatedBytes() const { return allocatedBefore_ - sweptBytes_; } uint64_t afterExternalBytes() const { return externalBefore_ - sweptExternalBytes_; } double survivalRatio() const { return allocatedBefore_ ? static_cast<double>(afterAllocatedBytes()) / allocatedBefore_ : 0; } void markUsed() { assert(!std::exchange(usedDbg_, true) && "markUsed called twice"); } GCAnalyticsEvent getEvent() && { markUsed(); return GCAnalyticsEvent{ gc_->getName(), gc_->getKindAsStr(), collectionType_, std::move(cause_), std::chrono::duration_cast<std::chrono::milliseconds>( endTime_ - beginTime_), std::chrono::duration_cast<std::chrono::milliseconds>(cpuDuration_), /*allocated*/ BeforeAndAfter{allocatedBefore_, afterAllocatedBytes()}, /*size*/ BeforeAndAfter{sizeBefore_, sizeAfter_}, /*external*/ BeforeAndAfter{externalBefore_, afterExternalBytes()}, /*survivalRatio*/ survivalRatio(), /*tags*/ std::move(tags_)}; } private: HadesGC *gc_; std::string cause_; std::string collectionType_; std::vector<std::string> tags_; TimePoint beginTime_{}; TimePoint endTime_{}; Duration cpuTimeSectionStart_{}; Duration cpuDuration_{}; uint64_t allocatedBefore_{0}; uint64_t externalBefore_{0}; uint64_t sizeBefore_{0}; uint64_t sizeAfter_{0}; uint64_t sweptBytes_{0}; uint64_t sweptExternalBytes_{0}; #ifndef NDEBUG bool usedDbg_{false}; #endif }; template <typename T> static T convertPtr(PointerBase &, CompressedPointer cp) { return cp; } template <> /* static */ GCCell *convertPtr(PointerBase &base, CompressedPointer cp) { return cp.get(base); } template <typename T> static T convertPtr(PointerBase &, GCCell *p) { return p; } template <> /* static */ CompressedPointer convertPtr(PointerBase &base, GCCell *a) { return CompressedPointer::encodeNonNull(a, base); } template <bool CompactionEnabled> class HadesGC::EvacAcceptor final : public RootAndSlotAcceptor, public WeakRootAcceptor { public: EvacAcceptor(HadesGC &gc) : gc{gc}, pointerBase_{gc.getPointerBase()}, copyListHead_{nullptr}, isTrackingIDs_{gc.isTrackingIDs()} {} ~EvacAcceptor() {} // TODO: Implement a purely CompressedPointer version of this. That will let // us avoid decompressing pointers altogether if they point outside the // YG/compactee. inline bool shouldForward(const void *ptr) const { return gc.inYoungGen(ptr) || (CompactionEnabled && gc.compactee_.evacContains(ptr)); } inline bool shouldForward(CompressedPointer ptr) const { return gc.inYoungGen(ptr) || (CompactionEnabled && gc.compactee_.evacContains(ptr)); } LLVM_NODISCARD GCCell *acceptRoot(GCCell *ptr) { if (shouldForward(ptr)) return forwardCell<GCCell *>(ptr); return ptr; } LLVM_NODISCARD GCCell *acceptHeap(GCCell *ptr, void *heapLoc) { if (shouldForward(ptr)) { assert( HeapSegment::getCellMarkBit(ptr) && "Should only evacuate marked objects."); return forwardCell<GCCell *>(ptr); } if (CompactionEnabled && gc.compactee_.contains(ptr)) { // If a compaction is about to take place, dirty the card for any newly // evacuated cells, since the marker may miss them. HeapSegment::cardTableCovering(heapLoc)->dirtyCardForAddress(heapLoc); } return ptr; } LLVM_NODISCARD CompressedPointer acceptHeap(CompressedPointer cptr, void *heapLoc) { if (shouldForward(cptr)) { GCCell *ptr = cptr.getNonNull(pointerBase_); assert( HeapSegment::getCellMarkBit(ptr) && "Should only evacuate marked objects."); return forwardCell<CompressedPointer>(ptr); } if (CompactionEnabled && gc.compactee_.contains(cptr)) { // If a compaction is about to take place, dirty the card for any newly // evacuated cells, since the marker may miss them. HeapSegment::cardTableCovering(heapLoc)->dirtyCardForAddress(heapLoc); } return cptr; } template <typename T> LLVM_NODISCARD T forwardCell(GCCell *const cell) { assert( HeapSegment::getCellMarkBit(cell) && "Cannot forward unmarked object"); if (cell->hasMarkedForwardingPointer()) { // Get the forwarding pointer from the header of the object. CompressedPointer forwardedCell = cell->getMarkedForwardingPointer(); assert( forwardedCell.getNonNull(pointerBase_)->isValid() && "Cell was forwarded incorrectly"); return convertPtr<T>(pointerBase_, forwardedCell); } assert(cell->isValid() && "Encountered an invalid cell"); const auto cellSize = cell->getAllocatedSize(); // Newly discovered cell, first forward into the old gen. GCCell *const newCell = gc.oldGen_.alloc(cellSize); HERMES_SLOW_ASSERT( gc.inOldGen(newCell) && "Evacuated cell not in the old gen"); assert( HeapSegment::getCellMarkBit(newCell) && "Cell must be marked when it is allocated into the old gen"); // Copy the contents of the existing cell over before modifying it. std::memcpy(newCell, cell, cellSize); assert(newCell->isValid() && "Cell was copied incorrectly"); evacuatedBytes_ += cellSize; CopyListCell *const copyCell = static_cast<CopyListCell *>(cell); // Set the forwarding pointer in the old spot copyCell->setMarkedForwardingPointer( CompressedPointer::encodeNonNull(newCell, pointerBase_)); if (isTrackingIDs_) { gc.moveObject(cell, cellSize, newCell, cellSize); } // Push onto the copied list. push(copyCell); return convertPtr<T>(pointerBase_, newCell); } void accept(GCCell *&ptr) override { ptr = acceptRoot(ptr); } void accept(GCPointerBase &ptr) override { ptr.setInGC(acceptHeap(ptr, &ptr)); } void accept(PinnedHermesValue &hv) override { assert((!hv.isPointer() || hv.getPointer()) && "Value is not nullable."); acceptNullable(hv); } void acceptNullable(PinnedHermesValue &hv) override { if (hv.isPointer()) { GCCell *forwardedPtr = acceptRoot(static_cast<GCCell *>(hv.getPointer())); hv.setInGC(hv.updatePointer(forwardedPtr), &gc); } } void accept(GCHermesValue &hv) override { if (hv.isPointer()) { GCCell *forwardedPtr = acceptHeap(static_cast<GCCell *>(hv.getPointer()), &hv); hv.setInGC(hv.updatePointer(forwardedPtr), &gc); } } void accept(GCSmallHermesValue &hv) override { if (hv.isPointer()) { CompressedPointer forwardedPtr = acceptHeap(hv.getPointer(), &hv); hv.setInGC(hv.updatePointer(forwardedPtr), &gc); } } void acceptWeak(WeakRootBase &wr) override { // It's safe to not do a read barrier here since this is happening in the GC // and does not extend the lifetime of the referent. GCCell *const ptr = wr.getNoBarrierUnsafe(pointerBase_); if (!shouldForward(ptr)) return; if (ptr->hasMarkedForwardingPointer()) { // Get the forwarding pointer from the header of the object. CompressedPointer forwardedCell = ptr->getMarkedForwardingPointer(); assert( forwardedCell.getNonNull(pointerBase_)->isValid() && "Cell was forwarded incorrectly"); // Assign back to the input pointer location. wr = forwardedCell; } else { wr = nullptr; } } void accept(const RootSymbolID &sym) override {} void accept(const GCSymbolID &sym) override {} uint64_t evacuatedBytes() const { return evacuatedBytes_; } CopyListCell *pop() { if (!copyListHead_) { return nullptr; } else { CopyListCell *const cell = static_cast<CopyListCell *>(copyListHead_.getNonNull(pointerBase_)); assert(HeapSegment::getCellMarkBit(cell) && "Discovered unmarked object"); copyListHead_ = cell->next_; return cell; } } private: HadesGC &gc; PointerBase &pointerBase_; /// The copy list is managed implicitly in the body of each copied YG object. AssignableCompressedPointer copyListHead_; const bool isTrackingIDs_; uint64_t evacuatedBytes_{0}; void push(CopyListCell *cell) { cell->next_ = copyListHead_; copyListHead_ = CompressedPointer::encodeNonNull(cell, pointerBase_); } }; class MarkWorklist { private: /// Like std::vector but has a fixed capacity specified by N to reduce memory /// allocation. template <typename T, size_t N> class FixedCapacityVector { public: explicit FixedCapacityVector() : FixedCapacityVector(0) {} explicit FixedCapacityVector(size_t sz) : size_(sz) {} explicit FixedCapacityVector(const FixedCapacityVector &vec) : data_(vec.data_), size_(vec.size_) {} size_t size() const { return size_; } constexpr size_t capacity() const { return N; } bool empty() const { return size_ == 0; } void push_back(T elem) { assert( size_ < N && "Trying to push off the end of a FixedCapacityVector"); data_[size_++] = elem; } T *data() { return data_.data(); } void clear() { #ifndef NDEBUG // Fill the push chunk with bad memory in debug modes to catch invalid // reads. std::memset(data_.data(), kInvalidHeapValue, sizeof(GCCell *) * N); #endif size_ = 0; } private: std::array<T, N> data_; size_t size_; }; public: /// Adds an element to the end of the queue. void enqueue(GCCell *cell) { pushChunk_.push_back(cell); if (pushChunk_.size() == pushChunk_.capacity()) { // Once the chunk has reached its max size, move it to the pull chunks. flushPushChunk(); } } /// Empty and return the current worklist llvh::SmallVector<GCCell *, 0> drain() { llvh::SmallVector<GCCell *, 0> cells; // Move the list (in O(1) time) to a local variable, and then release the // lock. This unblocks the mutator faster. std::lock_guard<Mutex> lk{mtx_}; std::swap(cells, worklist_); assert(worklist_.empty() && "worklist isn't cleared out"); // Keep the previously allocated size to minimise allocations worklist_.reserve(cells.size()); return cells; } /// While the world is stopped, move the push chunk to the list of pull chunks /// to finish draining the mark worklist. /// WARN: This can only be called by the mutator or when the world is stopped. void flushPushChunk() { std::lock_guard<Mutex> lk{mtx_}; worklist_.insert( worklist_.end(), pushChunk_.data(), pushChunk_.data() + pushChunk_.size()); // Set the size back to 0 and refill the same buffer. pushChunk_.clear(); } /// \return true if there is still some work to be drained, with the exception /// of the push chunk. bool hasPendingWork() { std::lock_guard<Mutex> lk{mtx_}; return !worklist_.empty(); } #ifndef NDEBUG /// WARN: This can only be called when the world is stopped. bool empty() { std::lock_guard<Mutex> lk{mtx_}; return pushChunk_.empty() && worklist_.empty(); } #endif private: Mutex mtx_; static constexpr size_t kChunkSize = 128; using Chunk = FixedCapacityVector<GCCell *, kChunkSize>; Chunk pushChunk_; // Use a SmallVector of size 0 since it is more aggressive with PODs llvh::SmallVector<GCCell *, 0> worklist_; }; class HadesGC::MarkAcceptor final : public RootAndSlotAcceptor, public WeakRefAcceptor { public: MarkAcceptor(HadesGC &gc) : gc{gc}, pointerBase_{gc.getPointerBase()}, markedSymbols_{gc.gcCallbacks_.getSymbolsEnd()}, writeBarrierMarkedSymbols_{gc.gcCallbacks_.getSymbolsEnd()} {} void acceptHeap(GCCell *cell, const void *heapLoc) { assert(cell && "Cannot pass null pointer to acceptHeap"); assert(!gc.inYoungGen(heapLoc) && "YG slot found in OG marking"); if (gc.compactee_.contains(cell) && !gc.compactee_.contains(heapLoc)) { // This is a pointer in the heap pointing into the compactee, dirty the // corresponding card. HeapSegment::cardTableCovering(heapLoc)->dirtyCardForAddress(heapLoc); } if (HeapSegment::getCellMarkBit(cell)) { // Points to an already marked object, do nothing. return; } // This must be done after checking the cell mark bit, to avoid reading the // metadata of cells in the YG, which we do not have the lock for. assert(cell->isValid() && "Encountered an invalid cell"); push(cell); } void acceptRoot(GCCell *cell) { assert(cell->isValid() && "Encountered an invalid cell"); if (!HeapSegment::getCellMarkBit(cell)) push(cell); } void accept(GCCell *&ptr) override { if (ptr) acceptRoot(ptr); } void accept(GCPointerBase &ptr) override { if (auto cp = concurrentRead<CompressedPointer>(ptr)) acceptHeap(cp.getNonNull(pointerBase_), &ptr); } void accept(GCHermesValue &hvRef) override { HermesValue hv = concurrentRead<HermesValue>(hvRef); if (hv.isPointer()) { acceptHeap(static_cast<GCCell *>(hv.getPointer()), &hvRef); } else if (hv.isSymbol()) { acceptSym(hv.getSymbol()); } } void accept(PinnedHermesValue &hv) override { // PinnedHermesValue is a root type and cannot live in the heap. Therefore // there's no risk of a concurrent access. if (hv.isPointer()) { acceptRoot(static_cast<GCCell *>(hv.getPointer())); } else if (hv.isSymbol()) { acceptSym(hv.getSymbol()); } } void acceptNullable(PinnedHermesValue &hv) override { if (hv.isPointer()) { if (void *ptr = hv.getPointer()) acceptRoot(static_cast<GCCell *>(ptr)); } else if (hv.isSymbol()) { acceptSym(hv.getSymbol()); } } void accept(GCSmallHermesValue &hvRef) override { const SmallHermesValue hv = concurrentRead<SmallHermesValue>(hvRef); if (hv.isPointer()) { acceptHeap(hv.getPointer(pointerBase_), &hvRef); } else if (hv.isSymbol()) { acceptSym(hv.getSymbol()); } } void acceptSym(SymbolID sym) { const uint32_t idx = sym.unsafeGetIndex(); if (sym.isInvalid() || idx >= markedSymbols_.size()) { // Ignore symbols that aren't valid or are pointing outside of the range // when the collection began. return; } markedSymbols_[idx] = true; } void accept(const RootSymbolID &sym) override { acceptSym(sym); } void accept(const GCSymbolID &sym) override { acceptSym(concurrentRead<SymbolID>(sym)); } /// Interface for symbols marked by a write barrier. void markSymbol(SymbolID sym) { assert( !gc.calledByBackgroundThread() && "Write barrier invoked by background thread."); const uint32_t idx = sym.unsafeGetIndex(); if (sym.isInvalid() || idx >= writeBarrierMarkedSymbols_.size()) { // Ignore symbols that aren't valid or are pointing outside of the range // when the collection began. return; } writeBarrierMarkedSymbols_[idx] = true; } void accept(WeakRefBase &wr) override { assert( gc.weakRefMutex() && "Must hold weak ref mutex when marking a WeakRef."); WeakRefSlot *slot = wr.unsafeGetSlot(); assert( slot->state() != WeakSlotState::Free && "marking a freed weak ref slot"); if (slot->state() != WeakSlotState::Marked) { slot->mark(); } } /// Set the drain rate that'll be used for any future calls to drain APIs. void setDrainRate(size_t rate) { assert(!kConcurrentGC && "Drain rate is only used by incremental GC."); byteDrainRate_ = rate; } /// \return the total number of bytes marked so far. uint64_t markedBytes() const { return markedBytes_; } /// Drain the mark stack of cells to be processed. /// \post localWorklist is empty. Any flushed values in the global worklist at /// the start of the call are drained. void drainAllWork() { // This should only be called from the mutator. This means no write barriers // should occur, and there's no need to check the global worklist more than // once. drainSomeWork(std::numeric_limits<size_t>::max()); assert(localWorklist_.empty() && "Some work left that wasn't completed"); } /// Drains some of the worklist, using a drain rate specified by /// \c setDrainRate or kConcurrentMarkLimit. /// \return true if there is any remaining work in the local worklist. bool drainSomeWork() { // See the comment in setDrainRate for why the drain rate isn't used for // concurrent collections. constexpr size_t kConcurrentMarkLimit = 8192; return drainSomeWork(kConcurrentGC ? kConcurrentMarkLimit : byteDrainRate_); } /// Drain some of the work to be done for marking. /// \param markLimit Only mark up to this many bytes from the local /// worklist. /// NOTE: This does *not* guarantee that the marker thread /// has upper bounds on the amount of work it does before reading from the /// global worklist. Any individual cell can be quite large (such as an /// ArrayStorage). /// \return true if there is any remaining work in the local worklist. bool drainSomeWork(const size_t markLimit) { assert(gc.gcMutex_ && "Must hold the GC lock while accessing mark bits."); // Pull any new items off the global worklist. auto cells = globalWorklist_.drain(); for (GCCell *cell : cells) { assert( cell->isValid() && "Invalid cell received off the global worklist"); assert( !gc.inYoungGen(cell) && "Shouldn't ever traverse a YG object in this loop"); HERMES_SLOW_ASSERT( gc.dbgContains(cell) && "Non-heap cell found in global worklist"); if (!HeapSegment::getCellMarkBit(cell)) { // Cell has not yet been marked. push(cell); } } size_t numMarkedBytes = 0; assert(markLimit && "markLimit must be non-zero!"); while (!localWorklist_.empty() && numMarkedBytes < markLimit) { GCCell *const cell = localWorklist_.top(); localWorklist_.pop(); assert(cell->isValid() && "Invalid cell in marking"); assert(HeapSegment::getCellMarkBit(cell) && "Discovered unmarked object"); assert( !gc.inYoungGen(cell) && "Shouldn't ever traverse a YG object in this loop"); HERMES_SLOW_ASSERT( gc.dbgContains(cell) && "Non-heap object discovered during marking"); const auto sz = cell->getAllocatedSize(); numMarkedBytes += sz; gc.markCell(cell, *this); } markedBytes_ += numMarkedBytes; return !localWorklist_.empty(); } MarkWorklist &globalWorklist() { return globalWorklist_; } std::vector<JSWeakMap *> &reachableWeakMaps() { return reachableWeakMaps_; } /// Merge the symbols marked by the MarkAcceptor and by the write barrier, /// then return a reference to it. /// WARN: This should only be called when the mutator is paused, as /// otherwise there is a race condition between reading this and a symbol /// write barrier getting executed. llvh::BitVector &markedSymbols() { assert(gc.gcMutex_ && "Cannot call markedSymbols without a lock"); markedSymbols_ |= writeBarrierMarkedSymbols_; // No need to clear writeBarrierMarkedSymbols_, or'ing it again won't change // the bit vector. return markedSymbols_; } private: HadesGC &gc; PointerBase &pointerBase_; /// A worklist local to the marking thread, that is only pushed onto by the /// marking thread. If this is empty, the global worklist must be consulted /// to ensure that pointers modified in write barriers are handled. std::stack<GCCell *, std::vector<GCCell *>> localWorklist_; /// A worklist that other threads may add to as objects to be marked and /// considered alive. These objects will *not* have their mark bits set, /// because the mutator can't be modifying mark bits at the same time as the /// marker thread. MarkWorklist globalWorklist_; /// The WeakMap objects that have been discovered to be reachable. std::vector<JSWeakMap *> reachableWeakMaps_; /// markedSymbols_ represents which symbols have been proven live so far in /// a collection. True means that it is live, false means that it could /// possibly be garbage. The SymbolID's internal value is used as the index /// into this vector. Once the collection is finished, this vector is passed /// to IdentifierTable so that it can free symbols. If any new symbols are /// allocated after the collection began, assume they are live. llvh::BitVector markedSymbols_; /// A vector the same size as markedSymbols_ that will collect all symbols /// marked by write barriers. Merge this with markedSymbols_ to have complete /// information about marked symbols. Kept separate to avoid synchronization. llvh::BitVector writeBarrierMarkedSymbols_; /// The number of bytes to drain per call to drainSomeWork. A higher rate /// means more objects will be marked. /// Only used by incremental collections. size_t byteDrainRate_{0}; /// The number of bytes that have been marked so far. uint64_t markedBytes_{0}; void push(GCCell *cell) { assert( !HeapSegment::getCellMarkBit(cell) && "A marked object should never be pushed onto a worklist"); assert( !gc.inYoungGen(cell) && "Shouldn't ever push a YG object onto the worklist"); HeapSegment::setCellMarkBit(cell); // There could be a race here: however, the mutator will never change a // cell's kind after initialization. The GC thread might to a free cell, but // only during sweeping, not concurrently with this operation. Therefore // there's no need for any synchronization here. if (vmisa<JSWeakMap>(cell)) { reachableWeakMaps_.push_back(vmcast<JSWeakMap>(cell)); } else { localWorklist_.push(cell); } } template <typename T> T concurrentReadImpl(const T &valRef) { using Storage = typename std::conditional<sizeof(T) == 4, uint32_t, uint64_t>::type; static_assert(sizeof(T) == sizeof(Storage), "Sizes must match"); union { Storage storage; T val; } ret{}; // There is a benign data race here, as the GC can read a pointer while // it's being modified by the mutator; however, the following rules we // obey prevent it from being a problem: // * The only things being modified that the GC reads are the GCPointers // and GCHermesValue in an object. All other fields are ignored. // * Those fields are fewer than 64 bits. // * Therefore, on 64-bit platforms, those fields are atomic // automatically. // * On 32-bit platforms, we don't run this code concurrently, and // instead yield cooperatively with the mutator. // * Thanks to the write barrier, if something is modified its old value // is placed in the globalWorklist, so we don't miss marking it. // * Since the global worklist is pushed onto *before* the write // happens, we know that there's no way the loop will exit unless it // reads the old value. // * If it observes the old value (pre-write-barrier value) here, the // new value will still be live, either by being pre-marked by the // allocator, or because something else's write barrier will catch // the modification. TsanIgnoreReadsBegin(); // The cast to a volatile variable forces a read from valRef, since // reads from volatile variables are considered observable behaviour. This // prevents the compiler from optimizing away the returned value, // guaranteeing that we will not observe changes to the underlying value // past this point. Not using volatile here could lead to a TOCTOU bug, // because the underlying value may change after a pointer check (in the // case of HermesValue) or a null check (for pointers). ret.storage = *reinterpret_cast<Storage const volatile *>(&valRef); TsanIgnoreReadsEnd(); return ret.val; } template <typename T> T concurrentRead(const T &ref) { return kConcurrentGC ? concurrentReadImpl<T>(ref) : ref; } }; /// Mark weak roots separately from the MarkAcceptor since this is done while /// the world is stopped. /// Don't use the default weak root acceptor because fine-grained control of /// writes of compressed pointers is important. class HadesGC::MarkWeakRootsAcceptor final : public WeakRootAcceptor { public: MarkWeakRootsAcceptor(HadesGC &gc) : gc_{gc} {} void acceptWeak(WeakRootBase &wr) override { if (!wr) { return; } GCCell *const cell = wr.getNoBarrierUnsafe(gc_.getPointerBase()); HERMES_SLOW_ASSERT(gc_.dbgContains(cell) && "ptr not in heap"); if (HeapSegment::getCellMarkBit(cell)) { // If the cell is marked, no need to do any writes. return; } // Reset weak root if target GCCell is dead. wr = nullptr; } private: HadesGC &gc_; }; class HadesGC::Executor { public: Executor() : thread_([this] { worker(); }) {} ~Executor() { { std::lock_guard<std::mutex> lk(mtx_); shutdown_ = true; cv_.notify_one(); } thread_.join(); } std::future<void> add(std::function<void()> fn) { std::lock_guard<std::mutex> lk(mtx_); // Use a shared_ptr because we cannot std::move the promise into the // lambda in C++11. auto promise = std::make_shared<std::promise<void>>(); auto ret = promise->get_future(); queue_.push_back([promise, fn] { fn(); promise->set_value(); }); cv_.notify_one(); return ret; } std::thread::id getThreadId() const { return thread_.get_id(); } private: void worker() { oscompat::set_thread_name("hades"); std::unique_lock<std::mutex> lk(mtx_); while (!shutdown_) { cv_.wait(lk, [this]() { return !queue_.empty() || shutdown_; }); while (!queue_.empty()) { auto fn = std::move(queue_.front()); queue_.pop_front(); lk.unlock(); fn(); lk.lock(); } } } std::mutex mtx_; std::condition_variable cv_; std::deque<std::function<void()>> queue_; bool shutdown_{false}; std::thread thread_; }; bool HadesGC::OldGen::sweepNext(bool backgroundThread) { // Check if there are any more segments to sweep. Note that in the case where // OG has zero segments, this also skips updating the stats and survival ratio // at the end of this function, since they are not required. if (!sweepIterator_.segNumber) return false; assert(gc_->gcMutex_ && "gcMutex_ must be held while sweeping."); sweepIterator_.segNumber--; gc_->oldGen_.updatePeakAllocatedBytes(sweepIterator_.segNumber); const bool isTracking = gc_->isTrackingIDs(); // Re-evaluate this start point each time, as releasing the gcMutex_ allows // allocations into the old gen, which might boost the credited memory. const uint64_t externalBytesBefore = externalBytes(); // Clear the head pointers so that we can construct a new freelist. The // freelist bits will be updated after the segment is swept. The bits will // be inconsistent with the actual freelist for the duration of sweeping, // but this is fine because gcMutex_ is during the entire period. for (auto &head : freelistSegmentsBuckets_[sweepIterator_.segNumber]) head = nullptr; char *freeRangeStart = nullptr, *freeRangeEnd = nullptr; size_t mergedCells = 0; int32_t segmentSweptBytes = 0; for (GCCell *cell : segments_[sweepIterator_.segNumber].cells()) { assert(cell->isValid() && "Invalid cell in sweeping"); if (HeapSegment::getCellMarkBit(cell)) { // Cannot concurrently trim storage. Technically just checking // backgroundThread would suffice, but the kConcurrentGC lets us compile // away this check in incremental mode. if (kConcurrentGC && backgroundThread) continue; const uint32_t cellSize = cell->getAllocatedSize(); const uint32_t trimmedSize = cell->getVT()->getTrimmedSize(cell, cellSize); assert(cellSize >= trimmedSize && "Growing objects is not supported."); assert( trimmedSize >= minAllocationSize() && "Trimmed object must still meet minimum size."); assert( isSizeHeapAligned(trimmedSize) && "Trimmed size must also be heap aligned"); const uint32_t trimmableBytes = cellSize - trimmedSize; // If this cell has extra space we can trim, trim it. if (LLVM_UNLIKELY(trimmableBytes >= minAllocationSize())) { static_cast<VariableSizeRuntimeCell *>(cell)->setSizeFromGC( trimmedSize); GCCell *newCell = cell->nextCell(); // Just create a FillerCell, the next iteration will free it. constructCell<FillerCell>(newCell, trimmableBytes); assert( !HeapSegment::getCellMarkBit(newCell) && "Trimmed space cannot be marked"); HeapSegment::setCellHead(newCell, trimmableBytes); } continue; } const auto sz = cell->getAllocatedSize(); char *const cellCharPtr = reinterpret_cast<char *>(cell); if (freeRangeEnd != cellCharPtr) { assert( freeRangeEnd < cellCharPtr && "Should not overshoot the start of an object"); // We are starting a new free range, flush the previous one. if (LLVM_LIKELY(freeRangeStart)) addCellToFreelistFromSweep( freeRangeStart, freeRangeEnd, mergedCells > 1); mergedCells = 0; freeRangeEnd = freeRangeStart = cellCharPtr; } // Expand the current free range to include the current cell. freeRangeEnd += sz; mergedCells++; if (vmisa<FreelistCell>(cell)) continue; segmentSweptBytes += sz; // Cell is dead, run its finalizer first if it has one. cell->getVT()->finalizeIfExists(cell, gc_); if (isTracking && !vmisa<FillerCell>(cell)) { gc_->untrackObject(cell, sz); } } // Flush any free range that was left over. if (freeRangeStart) addCellToFreelistFromSweep(freeRangeStart, freeRangeEnd, mergedCells > 1); // Update the freelist bit arrays to match the newly set freelist heads. for (size_t bucket = 0; bucket < kNumFreelistBuckets; ++bucket) { // For each bucket, set the bit for the current segment based on whether // it has a non-null freelist head for that bucket. if (freelistSegmentsBuckets_[sweepIterator_.segNumber][bucket]) freelistBucketSegmentBitArray_[bucket].set(sweepIterator_.segNumber); else freelistBucketSegmentBitArray_[bucket].reset(sweepIterator_.segNumber); // In case the change above has changed the availability of a bucket // across all segments, update the overall bit array. freelistBucketBitArray_.set( bucket, !freelistBucketSegmentBitArray_[bucket].empty()); } // Correct the allocated byte count. incrementAllocatedBytes(-segmentSweptBytes, sweepIterator_.segNumber); sweepIterator_.sweptBytes += segmentSweptBytes; sweepIterator_.sweptExternalBytes += externalBytesBefore - externalBytes(); // There are more iterations to go. if (sweepIterator_.segNumber) return true; // This was the last sweep iteration, finish the collection. auto &stats = *gc_->ogCollectionStats_; stats.setSweptBytes(sweepIterator_.sweptBytes); stats.setSweptExternalBytes(sweepIterator_.sweptExternalBytes); const uint64_t targetSizeBytes = (stats.afterAllocatedBytes() + stats.afterExternalBytes()) / gc_->occupancyTarget_; // In a very large heap, use the configured max heap size as a backstop to // prevent the target size crossing it (which would delay collection and cause // an OOM). This is just an approximation, a precise accounting would subtract // segment metadata and YG memory. uint64_t clampedSizeBytes = std::min(targetSizeBytes, gc_->maxHeapSize_); targetSizeBytes_.update(clampedSizeBytes); sweepIterator_ = {}; return false; } void HadesGC::OldGen::initializeSweep() { assert( !sweepIterator_.segNumber && !sweepIterator_.sweptBytes && !sweepIterator_.sweptExternalBytes && "Sweep is already in progress."); sweepIterator_.segNumber = segments_.size(); } size_t HadesGC::OldGen::sweepSegmentsRemaining() const { return sweepIterator_.segNumber; } size_t HadesGC::OldGen::getMemorySize() const { size_t memorySize = segments_.size() * sizeof(HeapSegment); memorySize += segmentAllocatedBytes_.capacity() * sizeof(std::pair<uint32_t, uint32_t>); memorySize += freelistSegmentsBuckets_.capacity() * sizeof(std::array<FreelistCell *, kNumFreelistBuckets>); memorySize += sizeof(std::array<llvh::SparseBitVector<>, kNumFreelistBuckets>); // SparseBitVector doesn't have a getMemorySize function defined. return memorySize; } // Assume about 30% of the YG will survive initially. constexpr double kYGInitialSurvivalRatio = 0.3; HadesGC::OldGen::OldGen(HadesGC *gc) : gc_(gc) {} HadesGC::HadesGC( GCCallbacks &gcCallbacks, PointerBase &pointerBase, const GCConfig &gcConfig, std::shared_ptr<CrashManager> crashMgr, std::shared_ptr<StorageProvider> provider, experiments::VMExperimentFlags vmExperimentFlags) : GCBase( gcCallbacks, pointerBase, gcConfig, std::move(crashMgr), HeapKind::HadesGC), maxHeapSize_{std::max<uint64_t>( gcConfig.getMaxHeapSize(), // At least one YG segment and one OG segment. 2 * AlignedStorage::size())}, provider_(std::move(provider)), oldGen_{this}, backgroundExecutor_{ kConcurrentGC ? std::make_unique<Executor>() : nullptr}, promoteYGToOG_{!gcConfig.getAllocInYoung()}, revertToYGAtTTI_{gcConfig.getRevertToYGAtTTI()}, occupancyTarget_(gcConfig.getOccupancyTarget()), ygAverageSurvivalBytes_{ /*weight*/ 0.5, /*init*/ kYGInitialSizeFactor * HeapSegment::maxSize() * kYGInitialSurvivalRatio} { (void)vmExperimentFlags; std::lock_guard<Mutex> lk(gcMutex_); crashMgr_->setCustomData("HermesGC", getKindAsStr().c_str()); // createSegment relies on member variables and should not be called until // they are initialised. llvh::ErrorOr<HeapSegment> newYoungGen = createSegment(); if (!newYoungGen) hermes_fatal("Failed to initialize the young gen", newYoungGen.getError()); setYoungGen(std::move(newYoungGen.get())); const size_t initHeapSize = std::max<uint64_t>( {gcConfig.getMinHeapSize(), gcConfig.getInitHeapSize(), HeapSegment::maxSize()}); oldGen_.setTargetSizeBytes(initHeapSize - HeapSegment::maxSize()); } HadesGC::~HadesGC() { // finalizeAll calls waitForCollectionToFinish, so there should be no ongoing // collection. assert( concurrentPhase_ == Phase::None && "Must call finalizeAll before destructor."); } void HadesGC::getHeapInfo(HeapInfo &info) { std::lock_guard<Mutex> lk{gcMutex_}; GCBase::getHeapInfo(info); info.allocatedBytes = allocatedBytes(); // Heap size includes fragmentation, which means every segment is fully used. info.heapSize = (oldGen_.numSegments() + 1) * AlignedStorage::size(); // If YG isn't empty, its bytes haven't been accounted for yet, add them here. info.totalAllocatedBytes = totalAllocatedBytes_ + youngGen().used(); info.va = info.heapSize; info.externalBytes = oldGen_.externalBytes() + getYoungGenExternalBytes(); } void HadesGC::getHeapInfoWithMallocSize(HeapInfo &info) { // Get the usual heap info. getHeapInfo(info); // Get GCBase's malloc size estimate. GCBase::getHeapInfoWithMallocSize(info); std::lock_guard<Mutex> lk{gcMutex_}; // First add the usage by the runtime's roots. info.mallocSizeEstimate += gcCallbacks_.mallocSize(); // Scan all objects for their malloc size. This operation is what makes // getHeapInfoWithMallocSize O(heap size). forAllObjs([&info](GCCell *cell) { info.mallocSizeEstimate += cell->getVT()->getMallocSize(cell); }); } void HadesGC::getCrashManagerHeapInfo( CrashManager::HeapInformation &crashInfo) { HeapInfo info; getHeapInfo(info); crashInfo.size_ = info.heapSize; crashInfo.used_ = info.allocatedBytes; } void HadesGC::createSnapshot(llvh::raw_ostream &os) { std::lock_guard<Mutex> lk{gcMutex_}; // No allocations are allowed throughout the entire heap snapshot process. NoAllocScope scope{this}; // Let any existing collections complete before taking the snapshot. waitForCollectionToFinish("snapshot"); { GCCycle cycle{this, &gcCallbacks_, "Heap Snapshot"}; WeakRefLock lk{weakRefMutex()}; GCBase::createSnapshot(this, os); } } void HadesGC::snapshotAddGCNativeNodes(HeapSnapshot &snap) { GCBase::snapshotAddGCNativeNodes(snap); if (nativeIDs_.ygFinalizables == IDTracker::kInvalidNode) { nativeIDs_.ygFinalizables = getIDTracker().nextNativeID(); } if (nativeIDs_.og == IDTracker::kInvalidNode) { nativeIDs_.og = getIDTracker().nextNativeID(); } snap.beginNode(); snap.endNode( HeapSnapshot::NodeType::Native, "std::vector<GCCell *>", nativeIDs_.ygFinalizables, youngGenFinalizables_.capacity() * sizeof(GCCell *), 0); snap.beginNode(); snap.endNode( HeapSnapshot::NodeType::Native, "OldGen", nativeIDs_.og, sizeof(oldGen_) + oldGen_.getMemorySize(), 0); } void HadesGC::snapshotAddGCNativeEdges(HeapSnapshot &snap) { GCBase::snapshotAddGCNativeEdges(snap); // All native ids should be lazily initialized in snapshotAddGCNativeNodes, // because that is called first. assert(nativeIDs_.ygFinalizables != IDTracker::kInvalidNode); assert(nativeIDs_.og != IDTracker::kInvalidNode); snap.addNamedEdge( HeapSnapshot::EdgeType::Internal, "youngGenFinalizables", nativeIDs_.ygFinalizables); snap.addNamedEdge(HeapSnapshot::EdgeType::Internal, "oldGen", nativeIDs_.og); } void HadesGC::enableHeapProfiler( std::function<void( uint64_t, std::chrono::microseconds, std::vector<GCBase::AllocationLocationTracker::HeapStatsUpdate>)> fragmentCallback) { std::lock_guard<Mutex> lk{gcMutex_}; // Let any existing collections complete before enabling the profiler. waitForCollectionToFinish("heap profiler enable"); GCBase::enableHeapProfiler(std::move(fragmentCallback)); } void HadesGC::disableHeapProfiler() { std::lock_guard<Mutex> lk{gcMutex_}; // Let any existing collections complete before disabling the profiler. waitForCollectionToFinish("heap profiler disable"); GCBase::disableHeapProfiler(); } void HadesGC::enableSamplingHeapProfiler( size_t samplingInterval, int64_t seed) { std::lock_guard<Mutex> lk{gcMutex_}; // Let any existing collections complete before enabling the profiler. waitForCollectionToFinish("sampling heap profiler enable"); GCBase::enableSamplingHeapProfiler(samplingInterval, seed); } void HadesGC::disableSamplingHeapProfiler(llvh::raw_ostream &os) { std::lock_guard<Mutex> lk{gcMutex_}; // Let any existing collections complete before disabling the profiler. waitForCollectionToFinish("sampling heap profiler disable"); GCBase::disableSamplingHeapProfiler(os); } void HadesGC::printStats(JSONEmitter &json) { GCBase::printStats(json); json.emitKey("specific"); json.openDict(); json.emitKeyValue("collector", getKindAsStr()); json.emitKey("stats"); json.openDict(); json.closeDict(); json.closeDict(); } std::string HadesGC::getKindAsStr() const { return kGCName; } void HadesGC::collect(std::string cause, bool /*canEffectiveOOM*/) { { // Wait for any existing collections to finish before starting a new one. std::lock_guard<Mutex> lk{gcMutex_}; // Disable the YG promotion mode. A forced GC via collect will do a full // collection immediately anyway, so there's no need to avoid collecting YG. // This is especially important when the forced GC is a memory warning. promoteYGToOG_ = false; waitForCollectionToFinish(cause); } // This function should block until a collection finishes. // YG needs to be empty in order to do an OG collection. youngGenCollection(cause, /*forceOldGenCollection*/ true); { // Wait for the collection to complete. std::lock_guard<Mutex> lk{gcMutex_}; waitForCollectionToFinish(cause); } // Start a second YG collection to complete any pending compaction. // Since YG is empty, this will only be evacuating the compactee. // Note that it's possible for this call to start another OG collection if the // occupancy target is >= 75%. That doesn't break the contract of this // function though, and we don't want to bother with waiting for that // collection to complete because it won't find any garbage anyway. youngGenCollection(std::move(cause), /*forceOldGenCollection*/ false); } void HadesGC::waitForCollectionToFinish(std::string cause) { assert( gcMutex_ && "gcMutex_ must be held before calling waitForCollectionToFinish"); if (concurrentPhase_ == Phase::None) { return; } GCCycle cycle{this, &gcCallbacks_, "Old Gen (Direct)"}; assert(!ygCollectionStats_ && "Cannot collect OG during a YG collection"); CollectionStats waitingStats(this, std::move(cause), "waiting"); waitingStats.beginCPUTimeSection(); waitingStats.setBeginTime(); while (concurrentPhase_ != Phase::None) incrementalCollect(false); waitingStats.endCPUTimeSection(); waitingStats.setEndTime(); recordGCStats(std::move(waitingStats).getEvent(), true); } void HadesGC::oldGenCollection(std::string cause, bool forceCompaction) { // Full collection: // * Mark all live objects by iterating through a worklist. // * Sweep dead objects onto the free lists. // This function must be called while the gcMutex_ is held. assert(gcMutex_ && "gcMutex_ must be held when starting an OG collection"); assert( gcMutex_.depth() == 1 && "Need ability to release mutex in oldGenCollection."); assert( concurrentPhase_ == Phase::None && "Starting a second old gen collection"); // Wait for any lingering background task to finish. if (kConcurrentGC && ogThreadStatus_.valid()) { // This is just making sure that any leftover work is completed before // starting a new collection. Since concurrentPhase_ == None here, there is // no collection ongoing. However, the background task may need to acquire // the lock in order to observe the value of concurrentPhase_. std::unique_lock<Mutex> lk{gcMutex_, std::adopt_lock}; lk.unlock(); ogThreadStatus_.get(); lk.lock(); lk.release(); } // We know ygCollectionStats_ exists because oldGenCollection is only called // by youngGenCollection. ygCollectionStats_->addCollectionType("old gen start"); #ifdef HERMES_SLOW_DEBUG checkWellFormed(); #endif if (ogCollectionStats_) recordGCStats(std::move(*ogCollectionStats_).getEvent(), false); ogCollectionStats_ = std::make_unique<CollectionStats>(this, std::move(cause), "old"); // NOTE: Leave CPU time as zero if the collection isn't concurrent, as the // times aren't useful. if (kConcurrentGC) ogCollectionStats_->beginCPUTimeSection(); ogCollectionStats_->setBeginTime(); ogCollectionStats_->setBeforeSizes( oldGen_.allocatedBytes(), oldGen_.externalBytes(), segmentFootprint()); if (revertToYGAtTTI_) { // If we've reached the first OG collection, and reverting behavior is // requested, switch back to YG mode. promoteYGToOG_ = false; } // First, clear any mark bits that were set by a previous collection or // direct-to-OG allocation, they aren't needed anymore. for (HeapSegment &seg : oldGen_) seg.markBitArray().clear(); // Unmark all symbols in the identifier table, as Symbol liveness will be // determined during the collection. gcCallbacks_.unmarkSymbols(); // Mark phase: discover all pointers that are live. // This assignment will reset any leftover memory from the last collection. We // leave the last marker alive to avoid a race condition with setting // concurrentPhase_, oldGenMarker_ and the write barrier. oldGenMarker_.reset(new MarkAcceptor{*this}); { // Roots are marked before a marking thread is spun up, so that the root // marking is atomic. DroppingAcceptor<MarkAcceptor> nameAcceptor{*oldGenMarker_}; markRoots(nameAcceptor, /*markLongLived*/ true); // Do not call markWeakRoots here, as weak roots can only be cleared // after liveness is known. } concurrentPhase_ = Phase::Mark; // Before the thread starts up, make sure that any write barriers are aware // that concurrent marking is happening. ogMarkingBarriers_ = true; // prepareCompactee must be called before the new thread is spawned, in order // to ensure that write barriers start operating immediately, and that any // objects promoted during an intervening YG collection are correctly scanned. prepareCompactee(forceCompaction); // Setup the sweep iterator when collection begins, because the number of // segments can change if a YG collection interleaves. There's no need to // sweep those extra segments since they are full of newly promoted // objects from YG (which have all their mark bits set), thus the sweep // iterator doesn't need to be updated. We also don't need to sweep any // segments made since the start of the collection, since they won't have // any unmarked objects anyway. // NOTE: this must be called after prepareCompactee, which may remove segments // from the heap. oldGen_.initializeSweep(); if (!kConcurrentGC) { // 32-bit system: 64-bit HermesValues cannot be updated in one atomic // instruction. Have YG collections interleave marking work. // In this version of the system, the mark stack will be drained in batches // during each YG collection. Once the mark stack is fully drained, the rest // of the collection finishes while blocking a YG GC. This allows // incremental work without actually using multiple threads. // Initialize the drain rate. oldGenMarker_->setDrainRate(getDrainRate()); return; } ogCollectionStats_->endCPUTimeSection(); // 64-bit system: 64-bit HermesValues can be updated in one atomic // instruction. Start up a separate thread for doing marking work. // NOTE: Since the "this" value (the HadesGC instance) is copied to the // executor, the GC cannot be destructed until the new thread completes. This // means that before destroying the GC, waitForCollectionToFinish must be // called. collectOGInBackground(); // Use concurrentPhase_ to be able to tell when the collection finishes. } void HadesGC::collectOGInBackground() { assert(gcMutex_ && "Must hold GC mutex when scheduling background work."); assert( !backgroundTaskActive_ && "Should only have one active task at a time"); assert(kConcurrentGC && "Background work can only be done in concurrent GC"); #ifndef NDEBUG backgroundTaskActive_ = true; #endif ogThreadStatus_ = backgroundExecutor_->add([this]() { std::unique_lock<Mutex> lk(gcMutex_); while (true) { // If the mutator has requested the background task to stop and yield // gcMutex_, wait on ogPauseCondVar_ until the mutator acquires the mutex // and signals that we may resume. ogPauseCondVar_.wait( lk, [this] { return !ogPaused_.load(std::memory_order_relaxed); }); assert( backgroundTaskActive_ && "backgroundTaskActive_ must be true when the background task is in the loop."); incrementalCollect(true); if (concurrentPhase_ == Phase::None || concurrentPhase_ == Phase::CompleteMarking) { #ifndef NDEBUG backgroundTaskActive_ = false; #endif break; } } }); } std::unique_lock<Mutex> HadesGC::pauseBackgroundTask() { assert(kConcurrentGC && "Should not be called in incremental mode"); assert(!calledByBackgroundThread() && "Must be called from mutator"); // Signal to the background thread that it should stop and wait on // ogPauseCondVar_. ogPaused_.store(true, std::memory_order_relaxed); // Acquire gcMutex_ as soon as it is released by the background thread. // TODO(T102252908): Once we have C++17, make this a lock_guard and use // mandatory NRVO. std::unique_lock<Mutex> lk(gcMutex_); // Signal to the background thread that it may resume. Note that it will just // go to wait on gcMutex_, since it is currently held by this thread. ogPaused_.store(false, std::memory_order_relaxed); ogPauseCondVar_.notify_one(); return lk; } void HadesGC::incrementalCollect(bool backgroundThread) { assert(gcMutex_ && "Must hold the GC mutex when calling incrementalCollect"); switch (concurrentPhase_) { case Phase::None: break; case Phase::Mark: if (!kConcurrentGC && ygCollectionStats_) ygCollectionStats_->addCollectionType("marking"); // Drain some work from the mark worklist. If the work has finished // completely, move on to CompleteMarking. if (!oldGenMarker_->drainSomeWork()) concurrentPhase_ = Phase::CompleteMarking; break; case Phase::CompleteMarking: // Background task should exit, the mutator will restart it after the STW // pause. if (!backgroundThread) { if (ygCollectionStats_) ygCollectionStats_->addCollectionType("complete marking"); completeMarking(); concurrentPhase_ = Phase::Sweep; } break; case Phase::Sweep: if (!kConcurrentGC && ygCollectionStats_) ygCollectionStats_->addCollectionType("sweeping"); // Calling oldGen_.sweepNext() will sweep the next segment. if (!oldGen_.sweepNext(backgroundThread)) { // Finish any collection bookkeeping. ogCollectionStats_->setEndTime(); ogCollectionStats_->setAfterSize(segmentFootprint()); compacteeHandleForSweep_.reset(); concurrentPhase_ = Phase::None; if (!backgroundThread) checkTripwireAndSubmitStats(); } break; default: llvm_unreachable("No other possible state between iterations"); } } void HadesGC::prepareCompactee(bool forceCompaction) { assert(gcMutex_); assert( compactee_.empty() && "Ongoing compaction at the start of an OG collection."); if (promoteYGToOG_) return; llvh::Optional<size_t> compacteeIdx; // We should compact if the actual size of the heap is more than 5% larger // than the target size. Since the selected segment will be removed from the // heap, we only want to compact if there are at least 2 segments in the OG. double threshold = oldGen_.targetSizeBytes() * 1.05; uint64_t totalBytes = oldGen_.size() + oldGen_.externalBytes(); if ((forceCompaction || totalBytes > threshold) && oldGen_.numSegments() > 1) { // Select the one with the fewest allocated bytes, to // minimise scanning and copying. We intentionally avoid selecting the very // last segment, since that is going to be the most recently added segment // and is unlikely to be fragmented enough to be a good compaction // candidate. uint64_t minBytes = HeapSegment::maxSize(); for (size_t i = 0; i < oldGen_.numSegments() - 1; ++i) { const size_t curBytes = oldGen_.allocatedBytes(i); if (curBytes < minBytes) { compacteeIdx = i; minBytes = curBytes; } } } #ifdef HERMESVM_SANITIZE_HANDLES // Handle-SAN forces a compaction on random segments to move the heap. if (sanitizeRate_ && oldGen_.numSegments()) { std::uniform_int_distribution<> distrib(0, oldGen_.numSegments() - 1); compacteeIdx = distrib(randomEngine_); } #endif if (compacteeIdx) { compactee_.allocatedBytes = oldGen_.allocatedBytes(*compacteeIdx); compactee_.segment = std::make_shared<HeapSegment>(oldGen_.removeSegment(*compacteeIdx)); addSegmentExtentToCrashManager( *compactee_.segment, kCompacteeNameForCrashMgr); compactee_.start = compactee_.segment->lowLim(); compactee_.startCP = CompressedPointer::encodeNonNull( reinterpret_cast<GCCell *>(compactee_.segment->lowLim()), getPointerBase()); compacteeHandleForSweep_ = compactee_.segment; } } void HadesGC::updateOldGenThreshold() { // TODO: Dynamic threshold is not used in incremental mode because // getDrainRate computes the mark rate directly based on the threshold. This // means that increasing the threshold would operate like a one way ratchet. if (!kConcurrentGC) return; const double markedBytes = oldGenMarker_->markedBytes(); const double preAllocated = ogCollectionStats_->beforeAllocatedBytes(); assert(markedBytes <= preAllocated && "Cannot mark more than was allocated"); const double postAllocated = oldGen_.allocatedBytes() + compactee_.allocatedBytes; assert(postAllocated >= preAllocated && "Cannot free memory during marking"); // Calculate the number of bytes marked for each byte allocated into the old // generation. Note that this is skewed heavily by the number of young gen // collections that occur during marking, and therefore the size of the heap. // 1. In small heaps, this underestimates the true mark rate, because marking // may finish shortly after it starts, but we have to wait until the next YG // collection is complete. This is desirable, because we need a more // conservative margin in small heaps to avoid running over the heap limit. // 2. In large heaps, this approaches the true mark rate, because there will // be several young gen collections, giving us more accurate and finer grained // information on the allocation rate. const auto concurrentMarkRate = markedBytes / std::max(postAllocated - preAllocated, 1.0); // If the heap is completely full and we are running into blocking // collections, then it is possible that almost nothing is allocated into the // OG during the mark phase. Without correction, can become a self-reinforcing // cycle because it will cause the mark rate to be overestimated, making // collections start later, further increasing the odds of a blocking // collection. Empirically, the mark rate can be much higher than the below // limit, but we get diminishing returns with increasing mark rate, since the // threshold just asymptotically approaches 1. const auto clampedRate = std::min(concurrentMarkRate, 20.0); // Update the collection threshold using the newly computed mark rate. To add // a margin of safety, we assume everything in the heap at the time we hit the // threshold needs to be marked. This margin allows for variance in the // marking rate, and gives time for sweeping to start. The update is // calculated by viewing the threshold as the bytes to mark and the remainder // after the threshold as the bytes to fill. We can then solve for it using: // MarkRate = Threshold / (1 - Threshold) // This has some nice properties: // 1. As the threshold increases, the safety margin also increases (since the // safety margin is just the difference between the threshold and the // occupancy ratio). // 2. It neatly fits the range [0, 1) for a mark rate in [0, infinity). There // is no risk of division by zero. ogThreshold_.update(clampedRate / (clampedRate + 1)); } void HadesGC::completeMarking() { assert(inGC() && "inGC_ must be set during the STW pause"); // Update the collection threshold before marking anything more, so that only // the concurrently marked bytes are part of the calculation. updateOldGenThreshold(); ogMarkingBarriers_ = false; // No locks are needed here because the world is stopped and there is only 1 // active thread. oldGenMarker_->globalWorklist().flushPushChunk(); { // Remark any roots that may have changed without executing barriers. DroppingAcceptor<MarkAcceptor> nameAcceptor{*oldGenMarker_}; gcCallbacks_.markRootsForCompleteMarking(nameAcceptor); } // Drain the marking queue. oldGenMarker_->drainAllWork(); assert( oldGenMarker_->globalWorklist().empty() && "Marking worklist wasn't drained"); completeWeakMapMarking(*oldGenMarker_); // Update the compactee tracking pointers so that the next YG collection will // do a compaction. compactee_.evacStart = compactee_.start; compactee_.evacStartCP = compactee_.startCP; assert( oldGenMarker_->globalWorklist().empty() && "Marking worklist wasn't drained"); // Reset weak roots to null after full reachability has been // determined. MarkWeakRootsAcceptor acceptor{*this}; markWeakRoots(acceptor, /*markLongLived*/ true); // Now free symbols and weak refs. gcCallbacks_.freeSymbols(oldGenMarker_->markedSymbols()); // NOTE: If sweeping is done concurrently with YG collection, weak references // could be handled during the sweep pass instead of the mark pass. The read // barrier will need to be updated to handle the case where a WeakRef points // to an now-empty cell. updateWeakReferencesForOldGen(); // Nothing needs oldGenMarker_ from this point onward. oldGenMarker_.reset(); } void HadesGC::finalizeAll() { std::lock_guard<Mutex> lk{gcMutex_}; // Terminate any existing OG collection. concurrentPhase_ = Phase::None; if (ogCollectionStats_) ogCollectionStats_->markUsed(); // In case of an OOM, we may be in the middle of a YG collection. if (ygCollectionStats_) ygCollectionStats_->markUsed(); // Now finalize the heap. // We might be in the middle of a YG collection, with some objects promoted to // the OG, and some not. Only finalize objects that have not been promoted to // OG, and let the OG finalize the promoted objects. finalizeYoungGenObjects(); // If we are in the middle of a YG collection, some objects may have already // been promoted to the OG. Assume that any remaining external memory in the // YG belongs to those objects. transferExternalMemoryToOldGen(); const auto finalizeCallback = [this](GCCell *cell) { assert(cell->isValid() && "Invalid cell in finalizeAll"); cell->getVT()->finalizeIfExists(cell, this); }; if (compactee_.segment) compactee_.segment->forCompactedObjs(finalizeCallback, getPointerBase()); for (HeapSegment &seg : oldGen_) seg.forAllObjs(finalizeCallback); } void HadesGC::creditExternalMemory(GCCell *cell, uint32_t sz) { assert(canAllocExternalMemory(sz) && "Precondition"); if (inYoungGen(cell)) { size_t newYGExtBytes = getYoungGenExternalBytes() + sz; setYoungGenExternalBytes(newYGExtBytes); auto adj = std::min<size_t>(sz, youngGen_.available()); youngGen_.setEffectiveEnd(youngGen_.effectiveEnd() - adj); } else { std::lock_guard<Mutex> lk{gcMutex_}; oldGen_.creditExternalMemory(sz); uint64_t totalBytes = oldGen_.allocatedBytes() + oldGen_.externalBytes(); if (totalBytes > oldGen_.targetSizeBytes()) youngGen_.setEffectiveEnd(youngGen_.level()); } } void HadesGC::debitExternalMemory(GCCell *cell, uint32_t sz) { if (inYoungGen(cell)) { size_t oldYGExtBytes = getYoungGenExternalBytes(); assert( oldYGExtBytes >= sz && "Debiting more native memory than was credited"); setYoungGenExternalBytes(oldYGExtBytes - sz); // Don't modify the effective end here. creditExternalMemory is in charge // of tracking this. We don't expect many debits to not be from finalizers // anyway. } else { std::lock_guard<Mutex> lk{gcMutex_}; oldGen_.debitExternalMemory(sz); } } void HadesGC::writeBarrierSlow(const GCHermesValue *loc, HermesValue value) { if (ogMarkingBarriers_) { snapshotWriteBarrierInternal(*loc); } if (!value.isPointer()) { return; } relocationWriteBarrier(loc, value.getPointer()); } void HadesGC::writeBarrierSlow( const GCSmallHermesValue *loc, SmallHermesValue value) { if (ogMarkingBarriers_) { snapshotWriteBarrierInternal(*loc); } if (!value.isPointer()) { return; } relocationWriteBarrier(loc, value.getPointer(getPointerBase())); } void HadesGC::writeBarrierSlow(const GCPointerBase *loc, const GCCell *value) { if (*loc && ogMarkingBarriers_) snapshotWriteBarrierInternal(*loc); // Always do the non-snapshot write barrier in order for YG to be able to // scan cards. relocationWriteBarrier(loc, value); } void HadesGC::constructorWriteBarrierSlow( const GCHermesValue *loc, HermesValue value) { // A constructor never needs to execute a SATB write barrier, since its // previous value was definitely not live. if (!value.isPointer()) { return; } relocationWriteBarrier(loc, value.getPointer()); } void HadesGC::constructorWriteBarrierSlow( const GCSmallHermesValue *loc, SmallHermesValue value) { // A constructor never needs to execute a SATB write barrier, since its // previous value was definitely not live. if (!value.isPointer()) { return; } relocationWriteBarrier(loc, value.getPointer(getPointerBase())); } void HadesGC::constructorWriteBarrierRangeSlow( const GCHermesValue *start, uint32_t numHVs) { assert( AlignedStorage::containedInSame(start, start + numHVs) && "Range must start and end within a heap segment."); // Most constructors should be running in the YG, so in the common case, we // can avoid doing anything for the whole range. If the range is in the OG, // then just dirty all the cards corresponding to it, and we can scan them for // pointers later. This is less precise but makes the write barrier faster. AlignedHeapSegment::cardTableCovering(start)->dirtyCardsForAddressRange( start, start + numHVs); } void HadesGC::constructorWriteBarrierRangeSlow( const GCSmallHermesValue *start, uint32_t numHVs) { assert( AlignedStorage::containedInSame(start, start + numHVs) && "Range must start and end within a heap segment."); AlignedHeapSegment::cardTableCovering(start)->dirtyCardsForAddressRange( start, start + numHVs); } void HadesGC::snapshotWriteBarrierRangeSlow( const GCHermesValue *start, uint32_t numHVs) { for (uint32_t i = 0; i < numHVs; ++i) { snapshotWriteBarrierInternal(start[i]); } } void HadesGC::snapshotWriteBarrierRangeSlow( const GCSmallHermesValue *start, uint32_t numHVs) { for (uint32_t i = 0; i < numHVs; ++i) { snapshotWriteBarrierInternal(start[i]); } } void HadesGC::snapshotWriteBarrierInternal(GCCell *oldValue) { assert( (oldValue->isValid()) && "Invalid cell encountered in snapshotWriteBarrier"); if (!inYoungGen(oldValue)) { HERMES_SLOW_ASSERT( dbgContains(oldValue) && "Non-heap pointer encountered in snapshotWriteBarrier"); oldGenMarker_->globalWorklist().enqueue(oldValue); } } void HadesGC::snapshotWriteBarrierInternal(CompressedPointer oldValue) { assert( (oldValue.get(getPointerBase())->isValid()) && "Invalid cell encountered in snapshotWriteBarrier"); if (!inYoungGen(oldValue)) { GCCell *ptr = oldValue.get(getPointerBase()); HERMES_SLOW_ASSERT( dbgContains(ptr) && "Non-heap pointer encountered in snapshotWriteBarrier"); oldGenMarker_->globalWorklist().enqueue(ptr); } } void HadesGC::snapshotWriteBarrierInternal(HermesValue oldValue) { if (oldValue.isPointer()) { snapshotWriteBarrierInternal(static_cast<GCCell *>(oldValue.getPointer())); } else if (oldValue.isSymbol()) { // Symbols need snapshot write barriers. snapshotWriteBarrierInternal(oldValue.getSymbol()); } } void HadesGC::snapshotWriteBarrierInternal(SmallHermesValue oldValue) { if (oldValue.isPointer()) { snapshotWriteBarrierInternal(oldValue.getPointer()); } else if (oldValue.isSymbol()) { // Symbols need snapshot write barriers. snapshotWriteBarrierInternal(oldValue.getSymbol()); } } void HadesGC::snapshotWriteBarrierInternal(SymbolID symbol) { HERMES_SLOW_ASSERT( ogMarkingBarriers_ && "snapshotWriteBarrier should only be called while the OG is marking"); oldGenMarker_->markSymbol(symbol); } void HadesGC::relocationWriteBarrier(const void *loc, const void *value) { assert(!inYoungGen(loc) && "Pre-condition from other callers"); // Do not dirty cards for compactee->compactee, yg->yg, or yg->compactee // pointers. But do dirty cards for compactee->yg pointers, since compaction // may not happen in the next YG. if (AlignedStorage::containedInSame(loc, value)) { return; } if (inYoungGen(value) || compactee_.contains(value)) { // Only dirty a card if it's an old-to-young or old-to-compactee pointer. // This is fine to do since the GC never modifies card tables outside of // allocation. // Note that this *only* applies since the boundaries are updated separately // from the card table being marked itself. HeapSegment::cardTableCovering(loc)->dirtyCardForAddress(loc); } } void HadesGC::weakRefReadBarrier(GCCell *value) { assert( !calledByBackgroundThread() && "Read barrier invoked by background thread."); // If the GC is marking, conservatively mark the value as live. if (ogMarkingBarriers_) snapshotWriteBarrierInternal(value); // Otherwise, if no GC is active at all, the weak ref must be alive. // During sweeping there's no special handling either. } void HadesGC::weakRefReadBarrier(HermesValue value) { // For now, WeakRefs must be pointers. If they are extended in the future, // this barrier should handle both pointers and symbols. weakRefReadBarrier(static_cast<GCCell *>(value.getPointer())); } bool HadesGC::canAllocExternalMemory(uint32_t size) { return size <= maxHeapSize_; } WeakRefSlot *HadesGC::allocWeakSlot(HermesValue init) { assert( !calledByBackgroundThread() && "allocWeakSlot should only be called from the mutator"); // The weak ref mutex doesn't need to be held since weakSlots_ and // firstFreeWeak_ are only modified while the world is stopped. WeakRefSlot *slot; if (firstFreeWeak_) { assert( firstFreeWeak_->state() == WeakSlotState::Free && "invalid free slot state"); slot = firstFreeWeak_; firstFreeWeak_ = firstFreeWeak_->nextFree(); slot->reset(init); } else { weakSlots_.push_back({init}); slot = &weakSlots_.back(); } if (ogMarkingBarriers_) { // During the mark phase, if a WeakRef is created, it might not be marked // if the object holding this new WeakRef has already been visited. // This doesn't need the WeakRefMutex because nothing is using this slot // yet. slot->mark(); } return slot; } void HadesGC::freeWeakSlot(WeakRefSlot *slot) { // Sets the given WeakRefSlot to point to firstFreeWeak_ instead of a cell. slot->free(firstFreeWeak_); firstFreeWeak_ = slot; } void HadesGC::forAllObjs(const std::function<void(GCCell *)> &callback) { std::lock_guard<Mutex> lk{gcMutex_}; youngGen().forAllObjs(callback); const auto skipGarbageCallback = [callback](GCCell *cell) { // If we're doing this check during sweeping, or between sweeping and // compaction, there might be some objects that are dead, and could // potentially have garbage in them. There's no need to check the // pointers of those objects. if (HeapSegment::getCellMarkBit(cell)) { callback(cell); } }; for (HeapSegment &seg : oldGen_) { if (concurrentPhase_ != Phase::Sweep) seg.forAllObjs(callback); else seg.forAllObjs(skipGarbageCallback); } if (compactee_.segment) { if (!compactee_.evacActive()) compactee_.segment->forAllObjs(callback); else compactee_.segment->forAllObjs(skipGarbageCallback); } } void HadesGC::ttiReached() { promoteYGToOG_ = false; } #ifndef NDEBUG bool HadesGC::calledByBackgroundThread() const { // If the background thread is active, check if this thread matches the // background thread. return kConcurrentGC && backgroundExecutor_->getThreadId() == std::this_thread::get_id(); } bool HadesGC::validPointer(const void *p) const { return dbgContains(p) && static_cast<const GCCell *>(p)->isValid(); } bool HadesGC::dbgContains(const void *p) const { return inYoungGen(p) || inOldGen(p); } void HadesGC::trackReachable(CellKind kind, unsigned sz) {} #endif void *HadesGC::allocSlow(uint32_t sz) { AllocResult res; // Failed to alloc in young gen, do a young gen collection. youngGenCollection( kNaturalCauseForAnalytics, /*forceOldGenCollection*/ false); res = youngGen().bumpAlloc(sz); if (res.success) return res.ptr; // Still fails after YG collection, perhaps it is a large alloc, try growing // the YG to full size. youngGen().clearExternalMemoryCharge(); res = youngGen().bumpAlloc(sz); if (res.success) return res.ptr; // A YG collection is guaranteed to fully evacuate, leaving all the space // available, so the only way this could fail is if sz is greater than // a segment size. // This would be an error in VM code to ever allow such a size to be // allocated, and thus there's an assert at the top of this function to // disallow that. This case is for production, if we miss a test case. oom(make_error_code(OOMError::SuperSegmentAlloc)); } void *HadesGC::allocLongLived(uint32_t sz) { assert( isSizeHeapAligned(sz) && "Call to allocLongLived must use a size aligned to HeapAlign"); if (kConcurrentGC) { HERMES_SLOW_ASSERT( !weakRefMutex() && "WeakRef mutex should not be held when allocLongLived is called"); } assert(gcMutex_ && "GC mutex must be held when calling allocLongLived"); totalAllocatedBytes_ += sz; // Alloc directly into the old gen. return oldGen_.alloc(sz); } GCCell *HadesGC::OldGen::alloc(uint32_t sz) { assert( isSizeHeapAligned(sz) && "Should be aligned before entering this function"); assert(sz >= minAllocationSize() && "Allocating too small of an object"); assert(sz <= maxAllocationSize() && "Allocating too large of an object"); assert(gc_->gcMutex_ && "gcMutex_ must be held before calling oldGenAlloc"); if (GCCell *cell = search(sz)) { return cell; } // Before waiting for a collection to finish, check if we're below the max // heap size and can simply allocate another segment. This will prevent // blocking the YG unnecessarily. llvh::ErrorOr<HeapSegment> seg = gc_->createSegment(); if (seg) { // Complete this allocation using a bump alloc. AllocResult res = seg->bumpAlloc(sz); assert( res.success && "A newly created segment should always be able to allocate"); // Set the cell head for any successful alloc, so that write barriers can // move from dirty cards to the head of the object. seg->setCellHead(static_cast<GCCell *>(res.ptr), sz); // Add the segment to segments_ and add the remainder of the segment to the // free list. addSegment(std::move(seg.get())); GCCell *newObj = static_cast<GCCell *>(res.ptr); HeapSegment::setCellMarkBit(newObj); return newObj; } // TODO(T109282643): Block on any pending OG collections here in case they // free up space. // Repeat the search in case the collection did free memory. if (GCCell *cell = search(sz)) { return cell; } // The GC didn't recover enough memory, OOM. // Re-use the error code from the earlier heap segment allocation, because // it's either that the max heap size was reached, or that segment failed to // allocate. gc_->oom(seg.getError()); } uint32_t HadesGC::OldGen::getFreelistBucket(uint32_t size) { // If the size corresponds to the "small" portion of the freelist, then the // bucket is just (size) / (heap alignment) if (size < kMinSizeForLargeBlock) { auto bucket = size >> LogHeapAlign; assert( bucket < kNumSmallFreelistBuckets && "Small blocks must be within the small free list range"); return bucket; } // Otherwise, index into the large portion of the freelist, which is based on // powers of 2 auto bucket = kNumSmallFreelistBuckets + llvh::Log2_32(size) - kLogMinSizeForLargeBlock; assert( bucket < kNumFreelistBuckets && "Block size must be within the freelist range!"); return bucket; } GCCell *HadesGC::OldGen::search(uint32_t sz) { size_t bucket = getFreelistBucket(sz); if (bucket < kNumSmallFreelistBuckets) { // Fast path: There already exists a size bucket for this alloc. Check if // there's a free cell to take and exit. if (freelistBucketBitArray_.at(bucket)) { int segmentIdx = freelistBucketSegmentBitArray_[bucket].find_first(); assert( segmentIdx >= 0 && "Set bit in freelistBucketBitArray_ must correspond to segment index."); FreelistCell *cell = removeCellFromFreelist(bucket, segmentIdx); assert( cell->getAllocatedSize() == sz && "Size bucket should be an exact match"); return finishAlloc(cell, sz, segmentIdx); } // Make sure we start searching at the smallest possible size that could fit bucket = getFreelistBucket(sz + minAllocationSize()); } // Once we're examining the rest of the free list, it's a first-fit algorithm. // This approach approximates finding the smallest possible fit. bucket = freelistBucketBitArray_.findNextSetBitFrom(bucket); for (; bucket < kNumFreelistBuckets; bucket = freelistBucketBitArray_.findNextSetBitFrom(bucket + 1)) { for (size_t segmentIdx : freelistBucketSegmentBitArray_[bucket]) { assert( freelistSegmentsBuckets_[segmentIdx][bucket] && "Empty bucket should not have bit set!"); // Need to track the previous entry in order to change the next pointer. AssignableCompressedPointer *prevLoc = &freelistSegmentsBuckets_[segmentIdx][bucket]; AssignableCompressedPointer cellCP = freelistSegmentsBuckets_[segmentIdx][bucket]; while (cellCP) { auto *cell = vmcast<FreelistCell>(cellCP.getNonNull(gc_->getPointerBase())); assert( cellCP == *prevLoc && "prevLoc should be updated in each iteration"); assert( (!cell->next_ || cell->next_.getNonNull(gc_->getPointerBase())->isValid()) && "Next pointer points to an invalid cell"); const auto cellSize = cell->getAllocatedSize(); assert( getFreelistBucket(cellSize) == bucket && "Found an incorrectly sized block in this bucket"); // Check if the size is large enough that the cell could be split. if (cellSize >= sz + minAllocationSize()) { // Split the free cell. In order to avoid initializing // soon-to-be-unused values like the size and the next pointer, copy // the return path here. auto newCell = cell->carve(sz); // Since the size of cell has changed, we may need to add it to a // different free list bucket. if (getFreelistBucket(cell->getAllocatedSize()) != bucket) { removeCellFromFreelist(prevLoc, bucket, segmentIdx); addCellToFreelist(cell, segmentIdx); } // Because we carved newCell out before removing cell from the // freelist, newCell is still poisoned (regardless of whether the // conditional above executed). Unpoison it. __asan_unpoison_memory_region(newCell, sz); return finishAlloc(newCell, sz, segmentIdx); } else if (cellSize == sz) { // Exact match, take it. removeCellFromFreelist(prevLoc, bucket, segmentIdx); return finishAlloc(cell, sz, segmentIdx); } // Non-exact matches, or anything just barely too small to fit, will // need to find another block. // NOTE: This is due to restrictions on the minimum cell size to keep // the heap parseable, especially in debug mode. If this minimum size // becomes smaller (smaller header, size becomes part of it // automatically, debug magic field is handled differently), this // decisions can be re-examined. An example alternative is to make a // special fixed-size cell that is only as big as an empty GCCell. That // alternative only works if the empty is small enough to fit in any gap // in the heap. That's not true in debug modes currently. prevLoc = &cell->next_; cellCP = cell->next_; } } } return nullptr; } template <typename Acceptor> void HadesGC::youngGenEvacuateImpl(Acceptor &acceptor, bool doCompaction) { // Marking each object puts it onto an embedded free list. { DroppingAcceptor<Acceptor> nameAcceptor{acceptor}; markRoots(nameAcceptor, /*markLongLived*/ doCompaction); } // Find old-to-young pointers, as they are considered roots for YG // collection. scanDirtyCards(acceptor); // Iterate through the copy list to find new pointers. while (CopyListCell *const copyCell = acceptor.pop()) { assert( copyCell->hasMarkedForwardingPointer() && "Discovered unmarked object"); assert( (inYoungGen(copyCell) || compactee_.evacContains(copyCell)) && "Unexpected object in YG collection"); // Update the pointers inside the forwarded object, since the old // object is only there for the forwarding pointer. GCCell *const cell = copyCell->getMarkedForwardingPointer().getNonNull(getPointerBase()); markCell(cell, acceptor); } // Mark weak roots. We only need to update the long lived weak roots if we are // evacuating part of the OG. markWeakRoots(acceptor, /*markLongLived*/ doCompaction); } void HadesGC::youngGenCollection( std::string cause, bool forceOldGenCollection) { ygCollectionStats_ = std::make_unique<CollectionStats>(this, cause, "young"); ygCollectionStats_->beginCPUTimeSection(); ygCollectionStats_->setBeginTime(); // Acquire the GC lock for the duration of the YG collection. auto lk = kConcurrentGC ? pauseBackgroundTask() : std::unique_lock<Mutex>(); // The YG is not parseable while a collection is occurring. assert(!inGC() && "Cannot be in GC at the start of YG!"); GCCycle cycle{this, &gcCallbacks_, "Young Gen"}; #ifdef HERMES_SLOW_DEBUG checkWellFormed(); // Check that the card tables are well-formed before the collection. verifyCardTable(); #endif assert( youngGen().markBitArray().findNextUnmarkedBitFrom(0) == youngGen().markBitArray().size() && "Young gen segment must have all mark bits set"); struct { uint64_t before{0}; uint64_t after{0}; } heapBytes, externalBytes; heapBytes.before = youngGen().used(); externalBytes.before = getYoungGenExternalBytes(); // YG is about to be emptied, add all of the allocations. totalAllocatedBytes_ += youngGen().used(); const bool doCompaction = compactee_.evacActive(); // Attempt to promote the YG segment to OG if the flag is set. If this call // fails for any reason, proceed with a GC. if (promoteYoungGenToOldGen()) { // Leave sweptBytes and sweptExternalBytes as defaults (which are 0). // Don't update the average YG survival ratio since no liveness was // calculated for the promotion case. ygCollectionStats_->setBeforeSizes( heapBytes.before, externalBytes.before, segmentFootprint()); ygCollectionStats_->addCollectionType("promotion"); assert(!doCompaction && "Cannot do compactions during YG promotions."); } else { auto &yg = youngGen(); if (compactee_.segment) { EvacAcceptor<true> acceptor{*this}; youngGenEvacuateImpl(acceptor, doCompaction); // The remaining bytes after the collection is just the number of bytes // that were evacuated. heapBytes.after = acceptor.evacuatedBytes(); } else { EvacAcceptor<false> acceptor{*this}; youngGenEvacuateImpl(acceptor, false); heapBytes.after = acceptor.evacuatedBytes(); } { WeakRefLock weakRefLock{weakRefMutex_}; // Now that all YG objects have been marked, update weak references. updateWeakReferencesForYoungGen(); } // Inform trackers about objects that died during this YG collection. if (isTrackingIDs()) { auto trackerCallback = [this](GCCell *cell) { // The compactee might have free list cells, which are not tracked. // untrackObject requires the object to have been tracked previously. // So skip free list cells here. if (!vmisa<OldGen::FreelistCell>(cell)) { untrackObject(cell, cell->getAllocatedSize()); } }; yg.forCompactedObjs(trackerCallback, getPointerBase()); if (doCompaction) { compactee_.segment->forCompactedObjs(trackerCallback, getPointerBase()); } } // Run finalizers for young gen objects. finalizeYoungGenObjects(); // This was modified by debitExternalMemoryFromFinalizer, called by // finalizers. The difference in the value before to now was the swept bytes externalBytes.after = getYoungGenExternalBytes(); // Now the copy list is drained, and all references point to the old // gen. Clear the level of the young gen. yg.resetLevel(); assert( yg.markBitArray().findNextUnmarkedBitFrom(0) == yg.markBitArray().size() && "Young gen segment must have all mark bits set"); if (doCompaction) { ygCollectionStats_->addCollectionType("compact"); heapBytes.before += compactee_.allocatedBytes; uint64_t ogExternalBefore = oldGen_.externalBytes(); // Run finalisers on compacted objects. compactee_.segment->forCompactedObjs( [this](GCCell *cell) { cell->getVT()->finalizeIfExists(cell, this); }, getPointerBase()); const uint64_t externalCompactedBytes = ogExternalBefore - oldGen_.externalBytes(); // Since we can't track the actual number of external bytes that were in // this segment, just use the swept external byte count. externalBytes.before += externalCompactedBytes; externalBytes.after += externalCompactedBytes; const size_t segIdx = SegmentInfo::segmentIndexFromStart(compactee_.segment->lowLim()); segmentIndices_.push_back(segIdx); removeSegmentExtentFromCrashManager(std::to_string(segIdx)); removeSegmentExtentFromCrashManager(kCompacteeNameForCrashMgr); compactee_ = {}; } // Move external memory accounting from YG to OG as well. transferExternalMemoryToOldGen(); // Potentially resize the YG if this collection did not meet our pause time // goals. Exclude compacting collections and the portion of YG time spent on // incremental OG collections, since they distort pause times and are // unaffected by YG size. if (!doCompaction) updateYoungGenSizeFactor(); // The effective end of our YG is no longer accurate for multiple reasons: // 1. transferExternalMemoryToOldGen resets the effectiveEnd to be the end. // 2. Creating a large alloc in the YG can increase the effectiveEnd. // 3. The duration of this collection may not have met our pause time goals. youngGen().setEffectiveEnd( youngGen().start() + static_cast<size_t>(ygSizeFactor_ * HeapSegment::maxSize())); // We have to set these after the collection, in case a compaction took // place and updated these metrics. ygCollectionStats_->setBeforeSizes( heapBytes.before, externalBytes.before, segmentFootprint()); // The swept bytes are just the bytes that were not evacuated. ygCollectionStats_->setSweptBytes(heapBytes.before - heapBytes.after); ygCollectionStats_->setSweptExternalBytes( externalBytes.before - externalBytes.after); ygCollectionStats_->setAfterSize(segmentFootprint()); // If this is not a compacting YG, update the average survival bytes. // In a compacting YG, since the evacuatedBytes counter tracks both // segments, this value is not a useful predictor of future collections. if (!doCompaction) ygAverageSurvivalBytes_.update( ygCollectionStats_->afterAllocatedBytes() + ygCollectionStats_->afterExternalBytes()); } #ifdef HERMES_SLOW_DEBUG // Check that the card tables are well-formed after the collection. verifyCardTable(); #endif // Perform any pending work for an ongoing OG collection. // Do this before starting a new collection in case we need collections // back-to-back. Also, don't check this after starting a collection to avoid // waiting for something that is both unlikely, and will increase the pause // time if it does happen. yieldToOldGen(); // We can only consider starting a new OG collection if any previous OG // collection is fully completed and has not left a pending compaction. This // handles the rare case where an OG collection was completed during this YG // collection, and the compaction will therefore only be completed in the next // collection. if (concurrentPhase_ == Phase::None && !compactee_.evacActive()) { // There is no OG collection running, check the tripwire in case this is the // first YG after an OG completed. checkTripwireAndSubmitStats(); if (forceOldGenCollection) { // If an OG collection is being forced, it's because something called // collect directly, most likely from a memory warning. In order to // respond to memory pressure effectively, the OG should compact. oldGenCollection(std::move(cause), /*forceCompaction*/ true); } else { // If the OG is sufficiently full after the collection finishes, begin // an OG collection. // External bytes are part of the numerator and denominator, because they // should not be included as part of determining the heap's occupancy, but // instead just influence when collections begin. const uint64_t totalAllocated = oldGen_.allocatedBytes() + oldGen_.externalBytes(); const uint64_t totalBytes = oldGen_.targetSizeBytes(); double allocatedRatio = static_cast<double>(totalAllocated) / totalBytes; if (allocatedRatio >= ogThreshold_) { oldGenCollection(kNaturalCauseForAnalytics, /*forceCompaction*/ false); } } } #ifdef HERMES_SLOW_DEBUG // Run a well-formed check before exiting. checkWellFormed(); #endif ygCollectionStats_->setEndTime(); ygCollectionStats_->endCPUTimeSection(); recordGCStats(std::move(*ygCollectionStats_).getEvent(), true); ygCollectionStats_.reset(); } bool HadesGC::promoteYoungGenToOldGen() { if (!promoteYGToOG_) { return false; } // Attempt to create a new segment, if that fails, turn off the flag to // disable GC and return false so we proceed with a GC. // TODO: Add more stringent criteria for turning off this flag, for instance, // once the heap reaches a certain size. That would avoid growing the heap to // the maximum possible size before stopping promotions. llvh::ErrorOr<HeapSegment> newYoungGen = createSegment(); if (!newYoungGen) { promoteYGToOG_ = false; return false; } // Move the external memory costs to the OG. Needs to happen here so that the // YG segment moved to OG is not left with an effective end. transferExternalMemoryToOldGen(); // The flag is on to prevent GC until TTI. Promote the whole YG segment // directly into OG. // Before promoting it, set the cell heads correctly for the segment // going into OG. This could be done at allocation time, but at a cost // to YG alloc times for a case that might not come up. youngGen_.forAllObjs([this](GCCell *cell) { youngGen_.setCellHead(cell, cell->getAllocatedSize()); }); // It is important that this operation is just a move of pointers to // segments. The addresses have to stay the same or else it would // require a marking pass through all objects. // This will also rename the segment in the crash data. oldGen_.addSegment(setYoungGen(std::move(newYoungGen.get()))); return true; } HadesGC::HeapSegment HadesGC::setYoungGen(HeapSegment seg) { addSegmentExtentToCrashManager(seg, "YG"); youngGenFinalizables_.clear(); std::swap(youngGen_, seg); youngGenCP_ = CompressedPointer::encodeNonNull( reinterpret_cast<GCCell *>(youngGen_.lowLim()), getPointerBase()); return seg; } void HadesGC::checkTripwireAndSubmitStats() { assert( gcMutex_ && "gcMutex must be held before calling checkTripwireAndSubmitStats"); assert( concurrentPhase_ == Phase::None && "Cannot check stats while OG collection is ongoing."); if (!ogCollectionStats_) { return; } const auto usedBytes = ogCollectionStats_->afterAllocatedBytes() + ogCollectionStats_->afterExternalBytes(); // We use the amount of live data from after a GC completed as the minimum // bound of what is live. checkTripwire(usedBytes); recordGCStats(std::move(*ogCollectionStats_).getEvent(), false); ogCollectionStats_.reset(); } void HadesGC::transferExternalMemoryToOldGen() { oldGen_.creditExternalMemory(getYoungGenExternalBytes()); setYoungGenExternalBytes(0); youngGen_.clearExternalMemoryCharge(); } void HadesGC::updateYoungGenSizeFactor() { assert( ygSizeFactor_ <= 1.0 && ygSizeFactor_ >= 0.25 && "YG size out of range."); const auto ygDuration = ygCollectionStats_->getElapsedTime().count(); // If the YG collection has taken less than 20% of our budgeted time, increase // the size of the YG by 10%. if (ygDuration < kTargetMaxPauseMs * 0.2) ygSizeFactor_ = std::min(ygSizeFactor_ * 1.1, 1.0); // If the YG collection has taken more than 40% of our budgeted time, decrease // the size of the YG by 10%. This is meant to leave some time for OG work. // However, don't let the YG size drop below 25% of the segment size. else if (ygDuration > kTargetMaxPauseMs * 0.4) ygSizeFactor_ = std::max(ygSizeFactor_ * 0.9, 0.25); } template <bool CompactionEnabled> void HadesGC::scanDirtyCardsForSegment( SlotVisitor<EvacAcceptor<CompactionEnabled>> &visitor, HeapSegment &seg) { const auto &cardTable = seg.cardTable(); // Use level instead of end in case the OG segment is still in bump alloc // mode. const char *const origSegLevel = seg.level(); size_t from = cardTable.addressToIndex(seg.start()); const size_t to = cardTable.addressToIndex(origSegLevel - 1) + 1; // If a compaction is taking place during sweeping, we may scan cards that // contain dead objects which in turn point to dead objects in the compactee. // In order to avoid promoting these dead objects, we should skip unmarked // objects altogether when compaction and sweeping happen at the same time. const bool visitUnmarked = !CompactionEnabled || concurrentPhase_ != Phase::Sweep; while (const auto oiBegin = cardTable.findNextDirtyCard(from, to)) { const auto iBegin = *oiBegin; const auto oiEnd = cardTable.findNextCleanCard(iBegin, to); const auto iEnd = oiEnd ? *oiEnd : to; assert( (iEnd == to || !cardTable.isCardForIndexDirty(iEnd)) && cardTable.isCardForIndexDirty(iEnd - 1) && "end should either be the end of the card table, or the first " "non-dirty card after a sequence of dirty cards"); assert(iBegin < iEnd && "Indices must be apart by at least one"); const char *const begin = cardTable.indexToAddress(iBegin); const char *const end = cardTable.indexToAddress(iEnd); // Don't try to mark any cell past the original boundary of the segment. const void *const boundary = std::min(end, origSegLevel); // Use the object heads rather than the card table to discover the head // of the object. GCCell *const firstObj = seg.getFirstCellHead(iBegin); GCCell *obj = firstObj; // Throughout this loop, objects are being marked which could promote // other objects into the OG. Such objects might be promoted onto a dirty // card, and be visited a second time. This is only a problem if the // acceptor isn't idempotent. Luckily, EvacAcceptor happens to be // idempotent, and so there's no correctness issue with visiting an object // multiple times. If EvacAcceptor wasn't idempotent, we'd have to be able // to identify objects promoted from YG in this loop, which would be // expensive. // Mark the first object with respect to the dirty card boundaries. if (visitUnmarked || HeapSegment::getCellMarkBit(obj)) markCellWithinRange(visitor, obj, obj->getKind(), begin, end); obj = obj->nextCell(); // If there are additional objects in this card, scan them. if (LLVM_LIKELY(obj < boundary)) { // Mark the objects that are entirely contained within the dirty card // boundaries. In a given iteration, obj is the start of a given object, // and next is the start of the next object. Iterate until the last // object where next is within the card. for (GCCell *next = obj->nextCell(); next < boundary; next = next->nextCell()) { if (visitUnmarked || HeapSegment::getCellMarkBit(obj)) markCell(visitor, obj, obj->getKind()); obj = next; } // Mark the final object in the range with respect to the dirty card // boundaries. assert( obj < boundary && obj->nextCell() >= boundary && "Last object in card must touch or cross cross the card boundary"); if (visitUnmarked || HeapSegment::getCellMarkBit(obj)) markCellWithinRange(visitor, obj, obj->getKind(), begin, end); } from = iEnd; } } template <bool CompactionEnabled> void HadesGC::scanDirtyCards(EvacAcceptor<CompactionEnabled> &acceptor) { SlotVisitor<EvacAcceptor<CompactionEnabled>> visitor{acceptor}; const bool preparingCompaction = CompactionEnabled && !compactee_.evacActive(); // The acceptors in this loop can grow the old gen by adding another // segment, if there's not enough room to evac the YG objects discovered. // Since segments are always placed at the end, we can use indices instead // of iterators, which aren't invalidated. It's ok to not scan newly added // segments, since they are going to be handled from the rest of YG // collection. const auto segEnd = oldGen_.numSegments(); for (size_t i = 0; i < segEnd; ++i) { // It is safe to hold this reference across a push_back into // oldGen_.segments_ since references into a deque are not invalidated. HeapSegment &seg = oldGen_[i]; scanDirtyCardsForSegment(visitor, seg); // Do not clear the card table if the OG thread is currently marking to // prepare for a compaction. Note that we should clear the card tables if // the compaction is currently ongoing. if (!preparingCompaction) seg.cardTable().clear(); } // No need to search dirty cards in the compactee segment if it is // currently being evacuated, since it will be scanned fully. if (preparingCompaction) scanDirtyCardsForSegment(visitor, *compactee_.segment); } void HadesGC::finalizeYoungGenObjects() { for (GCCell *cell : youngGenFinalizables_) { if (!cell->hasMarkedForwardingPointer()) { cell->getVT()->finalize(cell, this); } } youngGenFinalizables_.clear(); } void HadesGC::updateWeakReferencesForYoungGen() { assert(gcMutex_ && "gcMutex must be held when updating weak refs"); for (auto &slot : weakSlots_) { switch (slot.state()) { case WeakSlotState::Free: break; case WeakSlotState::Marked: // WeakRefSlots may only be marked while an OG collection is in the mark // phase or in the STW pause. The OG collection should unmark any slots // after it is complete. assert(ogMarkingBarriers_); LLVM_FALLTHROUGH; case WeakSlotState::Unmarked: { // Both marked and unmarked weak ref slots need to be updated. if (!slot.hasPointer()) { // Non-pointers need no work. break; } auto *const cell = static_cast<GCCell *>(slot.getPointer()); if (!inYoungGen(cell) && !compactee_.evacContains(cell)) { break; } // A young-gen GC doesn't know if a weak ref is reachable via old gen // references, so be conservative and do nothing to the slot. // The value must also be forwarded. if (cell->hasMarkedForwardingPointer()) { GCCell *const forwardedCell = cell->getMarkedForwardingPointer().getNonNull(getPointerBase()); HERMES_SLOW_ASSERT( validPointer(forwardedCell) && "Forwarding weak ref must be to a valid cell"); slot.setPointer(forwardedCell); } else { // Can't free this slot because it might only be used by an OG // object. slot.clearPointer(); } break; } } } } void HadesGC::updateWeakReferencesForOldGen() { for (auto &slot : weakSlots_) { switch (slot.state()) { case WeakSlotState::Free: // Skip free weak slots. break; case WeakSlotState::Marked: { // Set all allocated slots to unmarked. slot.unmark(); if (!slot.hasPointer()) { // Skip non-pointers. break; } auto *const cell = static_cast<GCCell *>(slot.getPointer()); // If the object isn't live, clear the weak ref. // YG has all of its mark bits set whenever there's no YG collection // happening, so this also excludes clearing any pointers to YG objects. if (!HeapSegment::getCellMarkBit(cell)) { slot.clearPointer(); } break; } case WeakSlotState::Unmarked: { freeWeakSlot(&slot); break; } } } } void HadesGC::completeWeakMapMarking(MarkAcceptor &acceptor) { gcheapsize_t weakMapAllocBytes = GCBase::completeWeakMapMarking( this, acceptor, acceptor.reachableWeakMaps(), /*objIsMarked*/ HeapSegment::getCellMarkBit, /*markFromVal*/ [&acceptor](GCCell *valCell, GCHermesValue &valRef) { if (HeapSegment::getCellMarkBit(valCell)) { return false; } acceptor.accept(valRef); // The weak ref lock is held throughout this entire section, so no need // to re-lock it. acceptor.drainAllWork(); return true; }, /*drainMarkStack*/ [](MarkAcceptor &acceptor) { // The weak ref lock is held throughout this entire section, so no need // to re-lock it. acceptor.drainAllWork(); }, /*checkMarkStackOverflow (HadesGC does not have mark stack overflow)*/ []() { return false; }); acceptor.reachableWeakMaps().clear(); (void)weakMapAllocBytes; } uint64_t HadesGC::allocatedBytes() const { // This can be called very early in initialization, before YG is initialized. return (youngGen_ ? youngGen_.used() : 0) + oldGen_.allocatedBytes(); } uint64_t HadesGC::externalBytes() const { return getYoungGenExternalBytes() + oldGen_.externalBytes(); } uint64_t HadesGC::segmentFootprint() const { size_t totalSegments = oldGen_.numSegments() + (youngGen_ ? 1 : 0); return totalSegments * AlignedStorage::size(); } uint64_t HadesGC::heapFootprint() const { return segmentFootprint() + externalBytes(); } uint64_t HadesGC::OldGen::allocatedBytes() const { return allocatedBytes_; } uint64_t HadesGC::OldGen::allocatedBytes(uint16_t segmentIdx) const { return segmentAllocatedBytes_[segmentIdx].first; } uint64_t HadesGC::OldGen::peakAllocatedBytes(uint16_t segmentIdx) const { return segmentAllocatedBytes_[segmentIdx].second; } void HadesGC::OldGen::incrementAllocatedBytes( int32_t incr, uint16_t segmentIdx) { assert(segmentIdx < numSegments()); allocatedBytes_ += incr; segmentAllocatedBytes_[segmentIdx].first += incr; assert( allocatedBytes_ <= size() && segmentAllocatedBytes_[segmentIdx].first <= HeapSegment::maxSize() && "Invalid increment"); } void HadesGC::OldGen::updatePeakAllocatedBytes(uint16_t segmentIdx) { segmentAllocatedBytes_[segmentIdx].second = std::max( segmentAllocatedBytes_[segmentIdx].second, segmentAllocatedBytes_[segmentIdx].first); } uint64_t HadesGC::OldGen::externalBytes() const { assert(gc_->gcMutex_ && "OG external bytes must be accessed under gcMutex_."); return externalBytes_; } uint64_t HadesGC::OldGen::size() const { return numSegments() * HeapSegment::maxSize(); } uint64_t HadesGC::OldGen::targetSizeBytes() const { assert( gc_->gcMutex_ && "Must hold gcMutex_ when accessing targetSizeBytes_."); return targetSizeBytes_; } size_t HadesGC::getYoungGenExternalBytes() const { assert( !calledByBackgroundThread() && "ygExternalBytes_ is only accessible from the mutator."); return ygExternalBytes_; } void HadesGC::setYoungGenExternalBytes(size_t sz) { assert( !calledByBackgroundThread() && "ygExternalBytes_ is only accessible from the mutator."); ygExternalBytes_ = sz; } llvh::ErrorOr<size_t> HadesGC::getVMFootprintForTest() const { // Start by adding the YG. size_t footprint = 0; auto ygFootprint = hermes::oscompat::vm_footprint(youngGen_.start(), youngGen_.hiLim()); // Check if the call failed. if (!ygFootprint) return ygFootprint; // Add each OG segment. for (const HeapSegment &seg : oldGen_) { auto segFootprint = hermes::oscompat::vm_footprint(seg.start(), seg.hiLim()); if (!segFootprint) return segFootprint; footprint += *segFootprint; } return footprint; } std::deque<HadesGC::HeapSegment>::iterator HadesGC::OldGen::begin() { return segments_.begin(); } std::deque<HadesGC::HeapSegment>::iterator HadesGC::OldGen::end() { return segments_.end(); } std::deque<HadesGC::HeapSegment>::const_iterator HadesGC::OldGen::begin() const { return segments_.begin(); } std::deque<HadesGC::HeapSegment>::const_iterator HadesGC::OldGen::end() const { return segments_.end(); } size_t HadesGC::OldGen::numSegments() const { return segments_.size(); } HadesGC::HeapSegment &HadesGC::OldGen::operator[](size_t i) { return segments_[i]; } llvh::ErrorOr<HadesGC::HeapSegment> HadesGC::createSegment() { // No heap size limit when Handle-SAN is on, to allow the heap enough room to // keep moving things around. if (!sanitizeRate_ && heapFootprint() >= maxHeapSize_) return make_error_code(OOMError::MaxHeapReached); auto res = AlignedStorage::create(provider_.get(), "hades-segment"); if (!res) { return res.getError(); } HeapSegment seg(std::move(res.get())); // Even if compressed pointers are off, we still use the segment index for // crash manager indices. size_t segIdx; if (segmentIndices_.size()) { segIdx = segmentIndices_.back(); segmentIndices_.pop_back(); } else { segIdx = ++numSegments_; } pointerBase_.setSegment(segIdx, seg.lowLim()); addSegmentExtentToCrashManager(seg, std::to_string(segIdx)); seg.markBitArray().markAll(); return llvh::ErrorOr<HadesGC::HeapSegment>(std::move(seg)); } void HadesGC::OldGen::addSegment(HeapSegment seg) { uint16_t newSegIdx = segments_.size(); segments_.emplace_back(std::move(seg)); segmentAllocatedBytes_.push_back({0, 0}); HeapSegment &newSeg = segments_.back(); incrementAllocatedBytes(newSeg.used(), newSegIdx); // Add a set of freelist buckets for this segment. freelistSegmentsBuckets_.emplace_back(); assert( freelistSegmentsBuckets_.size() == segments_.size() && "Must have as many freelists as segments."); // Add the remainder of the segment to the freelist. uint32_t sz = newSeg.available(); if (sz >= minAllocationSize()) { auto res = newSeg.bumpAlloc(sz); assert(res.success); addCellToFreelist(res.ptr, sz, segments_.size() - 1); } gc_->addSegmentExtentToCrashManager(newSeg, std::to_string(numSegments())); } HadesGC::HeapSegment HadesGC::OldGen::removeSegment(size_t segmentIdx) { assert(segmentIdx < segments_.size()); eraseSegmentFreelists(segmentIdx); allocatedBytes_ -= segmentAllocatedBytes_[segmentIdx].first; segmentAllocatedBytes_.erase(segmentAllocatedBytes_.begin() + segmentIdx); auto oldSeg = std::move(segments_[segmentIdx]); segments_.erase(segments_.begin() + segmentIdx); return oldSeg; } void HadesGC::OldGen::setTargetSizeBytes(size_t targetSizeBytes) { assert( gc_->gcMutex_ && "Must hold gcMutex_ when accessing targetSizeBytes_."); assert(!targetSizeBytes_ && "Should only initialise targetSizeBytes_ once."); targetSizeBytes_ = ExponentialMovingAverage(0.5, targetSizeBytes); } bool HadesGC::inOldGen(const void *p) const { // If it isn't in any OG segment or the compactee, then this pointer is not in // the OG. return compactee_.contains(p) || std::any_of(oldGen_.begin(), oldGen_.end(), [p](const HeapSegment &seg) { return seg.contains(p); }); } void HadesGC::yieldToOldGen() { assert(inGC() && "Must be in GC when yielding to old gen"); if (!kConcurrentGC && concurrentPhase_ != Phase::None) { // If there is an ongoing collection, update the drain rate before // collecting. if (concurrentPhase_ == Phase::Mark) oldGenMarker_->setDrainRate(getDrainRate()); constexpr uint32_t kYGIncrementalCollectBudget = kTargetMaxPauseMs / 2; const auto initialPhase = concurrentPhase_; // If the phase hasn't changed and we are still under 25ms after the first // iteration, then we can be reasonably sure that the next iteration will // also be <25ms, keeping us within 50ms even in the worst case. do { incrementalCollect(false); } while (concurrentPhase_ == initialPhase && ygCollectionStats_->getElapsedTime().count() < kYGIncrementalCollectBudget); } else if (concurrentPhase_ == Phase::CompleteMarking) { incrementalCollect(false); collectOGInBackground(); } } size_t HadesGC::getDrainRate() { // Concurrent collections don't need to use the drain rate because they // only yield the lock periodically to be interrupted, but otherwise will // continuously churn through work regardless of the rate. // Non-concurrent collections, on the other hand, can only make progress // at YG collection intervals, so they need to be configured to mark the // OG faster than it fills up. assert(!kConcurrentGC); // We want to make progress so that we are able to complete marking over all // YG collections before OG fills up. uint64_t totalAllocated = oldGen_.allocatedBytes() + oldGen_.externalBytes(); // Must be >0 to avoid division by zero below. uint64_t bytesToFill = std::max(oldGen_.targetSizeBytes(), totalAllocated + 1) - totalAllocated; uint64_t preAllocated = ogCollectionStats_->beforeAllocatedBytes(); uint64_t markedBytes = oldGenMarker_->markedBytes(); assert( markedBytes <= preAllocated && "Cannot mark more bytes than were initially allocated"); uint64_t bytesToMark = preAllocated - markedBytes; // The drain rate is calculated from: // bytesToMark / (collections until full) // = bytesToMark / (bytesToFill / ygAverageSurvivalBytes_) uint64_t drainRate = bytesToMark * ygAverageSurvivalBytes_ / bytesToFill; // If any of the above calculations end up being a tiny drain rate, make // the lower limit at least 8 KB, to ensure collections eventually end. constexpr uint64_t byteDrainRateMin = 8192; return std::max(drainRate, byteDrainRateMin); } void HadesGC::addSegmentExtentToCrashManager( const HeapSegment &seg, const std::string &extraName) { assert(!extraName.empty() && "extraName can't be empty"); if (!crashMgr_) { return; } const std::string segmentName = name_ + ":HeapSegment:" + extraName; // Pointers need at most 18 characters for 0x + 16 digits for a 64-bit // pointer. constexpr unsigned kAddressMaxSize = 18; char segmentAddressBuffer[kAddressMaxSize]; snprintf(segmentAddressBuffer, kAddressMaxSize, "%p", seg.lowLim()); crashMgr_->setContextualCustomData(segmentName.c_str(), segmentAddressBuffer); #ifdef HERMESVM_PLATFORM_LOGGING hermesLog( "HermesGC", "Added segment extent: %s = %s", segmentName.c_str(), segmentAddressBuffer); #endif } void HadesGC::removeSegmentExtentFromCrashManager( const std::string &extraName) { assert(!extraName.empty() && "extraName can't be empty"); if (!crashMgr_) { return; } const std::string segmentName = name_ + ":HeapSegment:" + extraName; crashMgr_->removeContextualCustomData(segmentName.c_str()); } #ifdef HERMES_SLOW_DEBUG void HadesGC::checkWellFormed() { WeakRefLock lk{weakRefMutex()}; CheckHeapWellFormedAcceptor acceptor(*this); { DroppingAcceptor<CheckHeapWellFormedAcceptor> nameAcceptor{acceptor}; markRoots(nameAcceptor, true); } markWeakRoots(acceptor, /*markLongLived*/ true); forAllObjs([this, &acceptor](GCCell *cell) { assert(cell->isValid() && "Invalid cell encountered in heap"); markCell(cell, acceptor); }); } void HadesGC::verifyCardTable() { assert(inGC() && "Must be in GC to call verifyCardTable"); struct VerifyCardDirtyAcceptor final : public SlotAcceptor { HadesGC &gc; explicit VerifyCardDirtyAcceptor(HadesGC &gc) : gc(gc) {} void acceptHelper(void *valuePtr, void *locPtr) { const bool crossRegionCompacteePtr = !gc.compactee_.evacContains(locPtr) && gc.compactee_.evacContains(valuePtr); if (!gc.inYoungGen(locPtr) && (gc.inYoungGen(valuePtr) || crossRegionCompacteePtr)) { assert(HeapSegment::cardTableCovering(locPtr)->isCardForAddressDirty( locPtr)); } } void accept(GCPointerBase &ptr) override { acceptHelper(ptr.get(gc.getPointerBase()), &ptr); } void accept(GCHermesValue &hv) override { if (hv.isPointer()) acceptHelper(hv.getPointer(), &hv); } void accept(GCSmallHermesValue &hv) override { if (hv.isPointer()) acceptHelper(hv.getPointer(gc.getPointerBase()), &hv); } void accept(const GCSymbolID &hv) override {} }; VerifyCardDirtyAcceptor acceptor{*this}; forAllObjs([this, &acceptor](GCCell *cell) { markCell(cell, acceptor); }); for (const HeapSegment &seg : oldGen_) { seg.cardTable().verifyBoundaries(seg.start(), seg.level()); } } #endif } // namespace vm } // namespace hermes