lib/VM/gcs/MallocGC.cpp (469 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 "GCBase-WeakMap.h"
#include "hermes/Support/CheckedMalloc.h"
#include "hermes/Support/ErrorHandling.h"
#include "hermes/Support/SlowAssert.h"
#include "hermes/VM/CheckHeapWellFormedAcceptor.h"
#include "hermes/VM/GC.h"
#include "hermes/VM/GCBase-inline.h"
#include "hermes/VM/HiddenClass.h"
#include "hermes/VM/JSWeakMapImpl.h"
#include "hermes/VM/RootAndSlotAcceptorDefault.h"
#include "hermes/VM/SmallHermesValue-inline.h"
#include "llvh/Support/Debug.h"
#include <algorithm>
#define DEBUG_TYPE "gc"
namespace hermes {
namespace vm {
static const char *kGCName = "malloc";
struct MallocGC::MarkingAcceptor final : public RootAndSlotAcceptorDefault,
public WeakAcceptorDefault {
MallocGC &gc;
std::vector<CellHeader *> worklist_;
/// 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. At the end of the collection, it is guaranteed that
/// the falses are garbage.
llvh::BitVector markedSymbols_;
MarkingAcceptor(MallocGC &gc)
: RootAndSlotAcceptorDefault(gc.getPointerBase()),
WeakAcceptorDefault(gc.getPointerBase()),
gc(gc),
markedSymbols_(gc.gcCallbacks_.getSymbolsEnd()) {}
using RootAndSlotAcceptorDefault::accept;
void accept(GCCell *&cell) override {
if (!cell) {
return;
}
HERMES_SLOW_ASSERT(
gc.validPointer(cell) &&
"Marked a pointer that the GC didn't allocate");
CellHeader *header = CellHeader::from(cell);
#ifdef HERMESVM_SANITIZE_HANDLES
/// Make the acceptor idempotent: allow it to be called multiple
/// times on the same slot during a collection. Do this by
/// recognizing when the pointer is already a "new" pointer.
if (gc.newPointers_.count(header)) {
return;
}
// With handle-san on, handle moving pointers here.
if (header->isMarked()) {
cell = header->getForwardingPointer()->data();
} else {
// It hasn't been seen before, move it.
// At this point, also trim the object.
const gcheapsize_t origSize = cell->getAllocatedSize();
const gcheapsize_t trimmedSize =
cell->getVT()->getTrimmedSize(cell, origSize);
auto *newLocation =
new (checkedMalloc(trimmedSize + sizeof(CellHeader))) CellHeader();
newLocation->mark();
memcpy(newLocation->data(), cell, trimmedSize);
if (origSize != trimmedSize) {
auto *newVarCell =
reinterpret_cast<VariableSizeRuntimeCell *>(newLocation->data());
newVarCell->setSizeFromGC(trimmedSize);
}
// Make sure to put an element on the worklist that is at the updated
// location. Don't update the stale address that is about to be free'd.
header->markWithForwardingPointer(newLocation);
auto *newCell = newLocation->data();
if (vmisa<JSWeakMap>(newCell)) {
reachableWeakMaps_.push_back(vmcast<JSWeakMap>(newCell));
} else {
worklist_.push_back(newLocation);
}
gc.newPointers_.insert(newLocation);
if (gc.isTrackingIDs()) {
gc.moveObject(
cell, cell->getAllocatedSize(), newLocation->data(), trimmedSize);
}
cell = newLocation->data();
}
#else
if (!header->isMarked()) {
// Only add to the worklist if it hasn't been marked yet.
header->mark();
// Trim the cell. This is fine to do with malloc'ed memory because the
// original size is retained by malloc.
gcheapsize_t origSize = cell->getAllocatedSize();
gcheapsize_t newSize = cell->getVT()->getTrimmedSize(cell, origSize);
if (newSize != origSize) {
static_cast<VariableSizeRuntimeCell *>(cell)->setSizeFromGC(newSize);
}
if (vmisa<JSWeakMap>(cell)) {
reachableWeakMaps_.push_back(vmcast<JSWeakMap>(cell));
} else {
worklist_.push_back(header);
}
// Move the pointer from the old pointers to the new pointers.
gc.pointers_.erase(header);
gc.newPointers_.insert(header);
}
// Else the cell is already marked and either on the worklist or already
// visited entirely, do nothing.
#endif
}
void acceptWeak(GCCell *&ptr) override {
if (ptr == nullptr) {
return;
}
CellHeader *header = CellHeader::from(ptr);
// Reset weak root if target GCCell is dead.
#ifdef HERMESVM_SANITIZE_HANDLES
ptr = header->isMarked() ? header->getForwardingPointer()->data() : nullptr;
#else
ptr = header->isMarked() ? ptr : nullptr;
#endif
}
void acceptHV(HermesValue &hv) override {
if (hv.isPointer()) {
GCCell *ptr = static_cast<GCCell *>(hv.getPointer());
accept(ptr);
hv.setInGC(hv.updatePointer(ptr), &gc);
} else if (hv.isSymbol()) {
acceptSym(hv.getSymbol());
}
}
void acceptSHV(SmallHermesValue &hv) override {
if (hv.isPointer()) {
GCCell *ptr = static_cast<GCCell *>(hv.getPointer(pointerBase_));
accept(ptr);
hv.setInGC(hv.updatePointer(ptr, pointerBase_), &gc);
} else if (hv.isSymbol()) {
acceptSym(hv.getSymbol());
}
}
void acceptSym(SymbolID sym) override {
if (sym.isInvalid()) {
return;
}
assert(
sym.unsafeGetIndex() < markedSymbols_.size() &&
"Tried to mark a symbol not in range");
markedSymbols_.set(sym.unsafeGetIndex());
}
void accept(WeakRefBase &wr) override {
wr.unsafeGetSlot()->mark();
}
};
gcheapsize_t MallocGC::Size::storageFootprint() const {
// MallocGC uses no storage from the StorageProvider.
return 0;
}
gcheapsize_t MallocGC::Size::minStorageFootprint() const {
// MallocGC uses no storage from the StorageProvider.
return 0;
}
MallocGC::MallocGC(
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::MallocGC),
pointers_(),
maxSize_(Size(gcConfig).max()),
sizeLimit_(gcConfig.getInitHeapSize()) {
(void)vmExperimentFlags;
crashMgr_->setCustomData("HermesGC", kGCName);
}
MallocGC::~MallocGC() {
for (CellHeader *header : pointers_) {
free(header);
}
}
void MallocGC::collectBeforeAlloc(std::string cause, uint32_t size) {
const auto growSizeLimit = [this, size](gcheapsize_t sizeLimit) {
// Either double the size limit, or increase to size, at a max of maxSize_.
return std::min(maxSize_, std::max(sizeLimit * 2, size));
};
if (size > sizeLimit_) {
sizeLimit_ = growSizeLimit(sizeLimit_);
}
if (size > maxSize_) {
// No way to handle the allocation no matter what.
oom(make_error_code(OOMError::MaxHeapReached));
}
assert(
size <= sizeLimit_ &&
"Should be guaranteed not to be asking for more space than the heap can "
"provide");
// Check for memory pressure conditions to do a collection.
// Use subtraction to prevent overflow.
#ifndef HERMESVM_SANITIZE_HANDLES
if (allocatedBytes_ < sizeLimit_ - size) {
return;
}
#endif
// Do a collection if the sanitization of handles is requested or if there
// is memory pressure.
collect(std::move(cause));
// While we still can't fill the allocation, keep growing.
while (allocatedBytes_ >= sizeLimit_ - size) {
if (sizeLimit_ == maxSize_) {
// Can't grow memory any higher, OOM.
oom(make_error_code(OOMError::MaxHeapReached));
}
sizeLimit_ = growSizeLimit(sizeLimit_);
}
}
#ifdef HERMES_SLOW_DEBUG
void MallocGC::checkWellFormed() {
GCCycle cycle{this};
CheckHeapWellFormedAcceptor acceptor(*this);
DroppingAcceptor<CheckHeapWellFormedAcceptor> nameAcceptor{acceptor};
markRoots(nameAcceptor, true);
markWeakRoots(acceptor, /*markLongLived*/ true);
for (CellHeader *header : pointers_) {
GCCell *cell = header->data();
assert(cell->isValid() && "Invalid cell encountered in heap");
markCell(cell, acceptor);
}
}
void MallocGC::clearUnmarkedPropertyMaps() {
for (CellHeader *header : pointers_)
if (!header->isMarked())
if (auto hc = dyn_vmcast<HiddenClass>(header->data()))
hc->clearPropertyMap(this);
}
#endif
void MallocGC::collect(std::string cause, bool /*canEffectiveOOM*/) {
assert(noAllocLevel_ == 0 && "no GC allowed right now");
using std::chrono::steady_clock;
LLVM_DEBUG(llvh::dbgs() << "Beginning collection");
#ifdef HERMES_SLOW_DEBUG
checkWellFormed();
#endif
const auto wallStart = steady_clock::now();
const auto cpuStart = oscompat::thread_cpu_time();
const auto allocatedBefore = allocatedBytes_;
const auto externalBefore = externalBytes_;
resetStats();
// Begin the collection phases.
{
GCCycle cycle{this, &gcCallbacks_, "Full collection"};
MarkingAcceptor acceptor(*this);
DroppingAcceptor<MarkingAcceptor> nameAcceptor{acceptor};
markRoots(nameAcceptor, true);
#ifdef HERMES_SLOW_DEBUG
clearUnmarkedPropertyMaps();
#endif
drainMarkStack(acceptor);
// The marking loop above will have accumulated WeakMaps;
// find things reachable from values of reachable keys.
completeWeakMapMarking(acceptor);
// Update weak roots references.
markWeakRoots(acceptor, /*markLongLived*/ true);
// Update and remove weak references.
updateWeakReferences();
resetWeakReferences();
// Free the unused symbols.
gcCallbacks_.freeSymbols(acceptor.markedSymbols_);
// By the end of the marking loop, all pointers left in pointers_ are dead.
for (CellHeader *header : pointers_) {
#ifndef HERMESVM_SANITIZE_HANDLES
// If handle sanitization isn't on, these pointers should all be dead.
assert(!header->isMarked() && "Live pointer left in dead heap section");
#endif
GCCell *cell = header->data();
// Extract before running any potential finalizers.
const auto freedSize = cell->getAllocatedSize();
// Run the finalizer if it exists and the cell is actually dead.
if (!header->isMarked()) {
cell->getVT()->finalizeIfExists(cell, this);
#ifndef NDEBUG
// Update statistics.
if (cell->getVT()->finalize_) {
++numFinalizedObjects_;
}
#endif
// Pointers that aren't marked now weren't moved, and are dead instead.
if (isTrackingIDs()) {
untrackObject(cell, freedSize);
}
}
#ifndef NDEBUG
// Before free'ing, fill with a dead value for debugging
std::fill_n(reinterpret_cast<char *>(cell), freedSize, kInvalidHeapValue);
#endif
free(header);
}
#ifndef NDEBUG
#ifdef HERMESVM_SANITIZE_HANDLES
// If handle sanitization is on, pointers_ is unmodified from before the
// collection, and the number of collected objects is the difference between
// the pointers before, and the pointers after the collection.
assert(
pointers_.size() >= newPointers_.size() &&
"There cannot be more new pointers than there are old pointers");
numCollectedObjects_ = pointers_.size() - newPointers_.size();
#else
// If handle sanitization is not on, live pointers are removed from
// pointers_ so the number of collected objects is equal to the size of
// pointers_.
numCollectedObjects_ = pointers_.size();
#endif
numReachableObjects_ = newPointers_.size();
numAllocatedObjects_ = newPointers_.size();
#endif
pointers_ = std::move(newPointers_);
assert(
newPointers_.empty() &&
"newPointers_ should be empty between collections");
// Clear all the mark bits in pointers_.
for (CellHeader *header : pointers_) {
assert(header->isMarked() && "Should only be live pointers left");
header->unmark();
}
}
// End of the collection phases, begin cleanup and stat recording.
#ifdef HERMES_SLOW_DEBUG
checkWellFormed();
#endif
// Grow the size limit if the heap is still more than 75% full.
if (allocatedBytes_ >= sizeLimit_ * 3 / 4) {
sizeLimit_ = std::min(maxSize_, sizeLimit_ * 2);
}
const auto cpuEnd = oscompat::thread_cpu_time();
const auto wallEnd = steady_clock::now();
GCAnalyticsEvent event{
getName(),
kGCName,
"full",
std::move(cause),
std::chrono::duration_cast<std::chrono::milliseconds>(
wallEnd - wallStart),
std::chrono::duration_cast<std::chrono::milliseconds>(cpuEnd - cpuStart),
/*allocated*/ BeforeAndAfter{allocatedBefore, allocatedBytes_},
// MallocGC only allocates memory as it is used so there is no distinction
// between the allocated bytes and the heap size.
/*size*/ BeforeAndAfter{allocatedBefore, allocatedBytes_},
// TODO: MallocGC doesn't yet support credit/debit external memory, so
// it has no data for these numbers.
/*external*/ BeforeAndAfter{externalBefore, externalBytes_},
/*survivalRatio*/
allocatedBefore ? (allocatedBytes_ * 1.0) / allocatedBefore : 0,
/*tags*/ {}};
recordGCStats(event, /* onMutator */ true);
checkTripwire(allocatedBytes_ + externalBytes_);
}
void MallocGC::drainMarkStack(MarkingAcceptor &acceptor) {
while (!acceptor.worklist_.empty()) {
CellHeader *header = acceptor.worklist_.back();
acceptor.worklist_.pop_back();
assert(header->isMarked() && "Pointer on the worklist isn't marked");
GCCell *cell = header->data();
markCell(cell, acceptor);
allocatedBytes_ += cell->getAllocatedSize();
}
}
void MallocGC::completeWeakMapMarking(MarkingAcceptor &acceptor) {
gcheapsize_t weakMapAllocBytes = GCBase::completeWeakMapMarking(
this,
acceptor,
acceptor.reachableWeakMaps_,
/*objIsMarked*/
[](GCCell *cell) { return CellHeader::from(cell)->isMarked(); },
/*markFromVal*/
[this, &acceptor](GCCell *valCell, GCHermesValue &valRef) {
CellHeader *valHeader = CellHeader::from(valCell);
if (valHeader->isMarked()) {
#ifdef HERMESVM_SANITIZE_HANDLES
valRef.setInGC(
HermesValue::encodeObjectValue(
valHeader->getForwardingPointer()->data()),
this);
#endif
return false;
}
acceptor.accept(valRef);
drainMarkStack(acceptor);
return true;
},
/*drainMarkStack*/
[this](MarkingAcceptor &acceptor) { drainMarkStack(acceptor); },
/*checkMarkStackOverflow (MallocGC does not have mark stack overflow)*/
[]() { return false; });
acceptor.reachableWeakMaps_.clear();
// drainMarkStack will have added the size of every object popped
// from the mark stack. WeakMaps are never pushed on that stack,
// but the call above returns their total size. So add that.
allocatedBytes_ += weakMapAllocBytes;
}
void MallocGC::finalizeAll() {
for (CellHeader *header : pointers_) {
GCCell *cell = header->data();
cell->getVT()->finalizeIfExists(cell, this);
}
}
void MallocGC::printStats(JSONEmitter &json) {
GCBase::printStats(json);
json.emitKey("specific");
json.openDict();
json.emitKeyValue("collector", kGCName);
json.emitKey("stats");
json.openDict();
json.closeDict();
json.closeDict();
}
std::string MallocGC::getKindAsStr() const {
return kGCName;
}
void MallocGC::resetStats() {
#ifndef NDEBUG
numAllocatedObjects_ = 0;
numReachableObjects_ = 0;
numCollectedObjects_ = 0;
numMarkedSymbols_ = 0;
numHiddenClasses_ = 0;
numLeafHiddenClasses_ = 0;
#endif
allocatedBytes_ = 0;
numFinalizedObjects_ = 0;
}
void MallocGC::getHeapInfo(HeapInfo &info) {
GCBase::getHeapInfo(info);
info.allocatedBytes = allocatedBytes_;
// MallocGC does not have a heap size.
info.heapSize = 0;
info.externalBytes = externalBytes_;
}
void MallocGC::getHeapInfoWithMallocSize(HeapInfo &info) {
getHeapInfo(info);
GCBase::getHeapInfoWithMallocSize(info);
// Note that info.mallocSizeEstimate is initialized by the call to
// GCBase::getHeapInfoWithMallocSize.
for (CellHeader *header : pointers_) {
GCCell *cell = header->data();
info.mallocSizeEstimate += cell->getVT()->getMallocSize(cell);
}
}
void MallocGC::getCrashManagerHeapInfo(CrashManager::HeapInformation &info) {
info.used_ = allocatedBytes_;
// MallocGC does not have a heap size.
info.size_ = 0;
}
void MallocGC::forAllObjs(const std::function<void(GCCell *)> &callback) {
for (auto *ptr : pointers_) {
callback(ptr->data());
}
}
void MallocGC::resetWeakReferences() {
for (auto &slot : weakSlots_) {
// Set all allocated slots to unmarked.
if (slot.state() == WeakSlotState::Marked)
slot.unmark();
}
}
void MallocGC::updateWeakReferences() {
for (auto &slot : weakSlots_) {
switch (slot.state()) {
case WeakSlotState::Free:
break;
case WeakSlotState::Unmarked:
freeWeakSlot(&slot);
break;
case WeakSlotState::Marked:
// If it's not a pointer, nothing to do.
if (!slot.hasPointer()) {
break;
}
auto *cell = reinterpret_cast<GCCell *>(slot.getPointer());
HERMES_SLOW_ASSERT(
validPointer(cell) &&
"Got a pointer out of a weak reference slot that is not owned by "
"the GC");
CellHeader *header = CellHeader::from(cell);
if (!header->isMarked()) {
// This pointer is no longer live, zero it out
slot.clearPointer();
} else {
#ifdef HERMESVM_SANITIZE_HANDLES
// Update the value to point to the new location
GCCell *nextCell = header->getForwardingPointer()->data();
HERMES_SLOW_ASSERT(
validPointer(cell) &&
"Forwarding weak ref must be to a valid cell");
slot.setPointer(nextCell);
#endif
}
break;
}
}
}
WeakRefSlot *MallocGC::allocWeakSlot(HermesValue init) {
weakSlots_.push_back({init});
return &weakSlots_.back();
}
void MallocGC::freeWeakSlot(WeakRefSlot *slot) {
slot->free(nullptr);
}
#ifndef NDEBUG
bool MallocGC::validPointer(const void *p) const {
return dbgContains(p) && static_cast<const GCCell *>(p)->isValid();
}
bool MallocGC::dbgContains(const void *p) const {
auto *ptr = reinterpret_cast<GCCell *>(const_cast<void *>(p));
CellHeader *header = CellHeader::from(ptr);
bool isValid = pointers_.find(header) != pointers_.end();
isValid = isValid || newPointers_.find(header) != newPointers_.end();
return isValid;
}
#endif
void MallocGC::createSnapshot(llvh::raw_ostream &os) {
GCCycle cycle{this};
GCBase::createSnapshot(this, os);
}
void MallocGC::creditExternalMemory(GCCell *, uint32_t size) {
externalBytes_ += size;
}
void MallocGC::debitExternalMemory(GCCell *, uint32_t size) {
externalBytes_ -= size;
}
/// @name Forward instantiations
/// @{
template void *MallocGC::alloc</*FixedSize*/ true, HasFinalizer::Yes>(
uint32_t size);
template void *MallocGC::alloc</*FixedSize*/ false, HasFinalizer::Yes>(
uint32_t size);
template void *MallocGC::alloc</*FixedSize*/ true, HasFinalizer::No>(
uint32_t size);
template void *MallocGC::alloc</*FixedSize*/ false, HasFinalizer::No>(
uint32_t size);
/// @}
} // namespace vm
} // namespace hermes
#undef DEBUG_TYPE