lib/VM/GCBase.cpp (1,515 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.
*/
#define DEBUG_TYPE "gc"
#include "hermes/VM/GC.h"
#include "hermes/Platform/Logging.h"
#include "hermes/Support/ErrorHandling.h"
#include "hermes/Support/OSCompat.h"
#include "hermes/VM/CellKind.h"
#include "hermes/VM/JSWeakMapImpl.h"
#include "hermes/VM/RootAndSlotAcceptorDefault.h"
#include "hermes/VM/Runtime.h"
#include "hermes/VM/SmallHermesValue-inline.h"
#include "hermes/VM/VTable.h"
#include "llvh/Support/Debug.h"
#include "llvh/Support/FileSystem.h"
#include "llvh/Support/Format.h"
#include "llvh/Support/raw_os_ostream.h"
#include "llvh/Support/raw_ostream.h"
#include <inttypes.h>
#include <clocale>
#include <stdexcept>
#include <system_error>
using llvh::dbgs;
using llvh::format;
namespace hermes {
namespace vm {
const char GCBase::kNaturalCauseForAnalytics[] = "natural";
const char GCBase::kHandleSanCauseForAnalytics[] = "handle-san";
GCBase::GCBase(
GCCallbacks &gcCallbacks,
PointerBase &pointerBase,
const GCConfig &gcConfig,
std::shared_ptr<CrashManager> crashMgr,
HeapKind kind)
: gcCallbacks_(gcCallbacks),
pointerBase_(pointerBase),
crashMgr_(crashMgr),
heapKind_(kind),
analyticsCallback_(gcConfig.getAnalyticsCallback()),
recordGcStats_(gcConfig.getShouldRecordStats()),
// Start off not in GC.
inGC_(false),
name_(gcConfig.getName()),
allocationLocationTracker_(this),
samplingAllocationTracker_(this),
#ifdef HERMESVM_SANITIZE_HANDLES
sanitizeRate_(gcConfig.getSanitizeConfig().getSanitizeRate()),
#endif
tripwireCallback_(gcConfig.getTripwireConfig().getCallback()),
tripwireLimit_(gcConfig.getTripwireConfig().getLimit())
#ifndef NDEBUG
,
randomizeAllocSpace_(gcConfig.getShouldRandomizeAllocSpace())
#endif
{
buildMetadataTable();
#ifdef HERMESVM_PLATFORM_LOGGING
hermesLog(
"HermesGC",
"Initialisation (Init: %dMB, Max: %dMB, Tripwire: %dMB)",
gcConfig.getInitHeapSize() >> 20,
gcConfig.getMaxHeapSize() >> 20,
gcConfig.getTripwireConfig().getLimit() >> 20);
#endif // HERMESVM_PLATFORM_LOGGING
#ifdef HERMESVM_SANITIZE_HANDLES
const std::minstd_rand::result_type seed =
gcConfig.getSanitizeConfig().getRandomSeed() >= 0
? gcConfig.getSanitizeConfig().getRandomSeed()
: std::random_device()();
if (sanitizeRate_ > 0.0 && sanitizeRate_ < 1.0) {
llvh::errs()
<< "Warning: you are using handle sanitization with random sampling.\n"
<< "Sanitize Rate: ";
llvh::write_double(llvh::errs(), sanitizeRate_, llvh::FloatStyle::Percent);
llvh::errs() << "\n"
<< "Sanitize Rate Seed: " << seed << "\n"
<< "Re-run with -gc-sanitize-handles-random-seed=" << seed
<< " for deterministic crashes.\n";
}
randomEngine_.seed(seed);
#endif
}
GCBase::GCCycle::GCCycle(
GCBase *gc,
OptValue<GCCallbacks *> gcCallbacksOpt,
std::string extraInfo)
: gc_(gc),
gcCallbacksOpt_(gcCallbacksOpt),
extraInfo_(std::move(extraInfo)),
previousInGC_(gc_->inGC_) {
if (!previousInGC_) {
if (gcCallbacksOpt_.hasValue()) {
gcCallbacksOpt_.getValue()->onGCEvent(
GCEventKind::CollectionStart, extraInfo_);
}
gc_->inGC_ = true;
}
}
GCBase::GCCycle::~GCCycle() {
if (!previousInGC_) {
gc_->inGC_ = false;
if (gcCallbacksOpt_.hasValue()) {
gcCallbacksOpt_.getValue()->onGCEvent(
GCEventKind::CollectionEnd, extraInfo_);
}
}
}
void GCBase::runtimeWillExecute() {
if (recordGcStats_ && !execStartTimeRecorded_) {
execStartTime_ = std::chrono::steady_clock::now();
execStartCPUTime_ = oscompat::thread_cpu_time();
oscompat::num_context_switches(
startNumVoluntaryContextSwitches_, startNumInvoluntaryContextSwitches_);
execStartTimeRecorded_ = true;
}
}
std::error_code GCBase::createSnapshotToFile(const std::string &fileName) {
std::error_code code;
llvh::raw_fd_ostream os(fileName, code, llvh::sys::fs::FileAccess::FA_Write);
if (code) {
return code;
}
createSnapshot(os);
return std::error_code{};
}
namespace {
constexpr HeapSnapshot::NodeID objectIDForRootSection(
RootAcceptor::Section section) {
// Since root sections start at zero, and in IDTracker the root sections
// start one past the reserved GC root, this number can be added to
// do conversions.
return GCBase::IDTracker::reserved(
static_cast<GCBase::IDTracker::ReservedObjectID>(
static_cast<HeapSnapshot::NodeID>(
GCBase::IDTracker::ReservedObjectID::GCRoots) +
1 + static_cast<HeapSnapshot::NodeID>(section)));
}
// Abstract base class for all snapshot acceptors.
struct SnapshotAcceptor : public RootAndSlotAcceptorWithNamesDefault {
using RootAndSlotAcceptorWithNamesDefault::accept;
SnapshotAcceptor(PointerBase &base, HeapSnapshot &snap)
: RootAndSlotAcceptorWithNamesDefault(base), snap_(snap) {}
void acceptHV(HermesValue &hv, const char *name) override {
if (hv.isPointer()) {
GCCell *ptr = static_cast<GCCell *>(hv.getPointer());
accept(ptr, name);
}
}
void acceptSHV(SmallHermesValue &hv, const char *name) override {
if (hv.isPointer()) {
GCCell *ptr = static_cast<GCCell *>(hv.getPointer(pointerBase_));
accept(ptr, name);
}
}
protected:
HeapSnapshot &snap_;
};
struct PrimitiveNodeAcceptor : public SnapshotAcceptor {
using SnapshotAcceptor::accept;
PrimitiveNodeAcceptor(
PointerBase &base,
HeapSnapshot &snap,
GCBase::IDTracker &tracker)
: SnapshotAcceptor(base, snap), tracker_(tracker) {}
// Do nothing for any value except a number.
void accept(GCCell *&ptr, const char *name) override {}
void acceptHV(HermesValue &hv, const char *) override {
if (hv.isNumber()) {
seenNumbers_.insert(hv.getNumber());
}
}
void acceptSHV(SmallHermesValue &hv, const char *) override {
if (hv.isNumber()) {
seenNumbers_.insert(hv.getNumber(pointerBase_));
}
}
void writeAllNodes() {
// Always write out the nodes for singletons.
snap_.beginNode();
snap_.endNode(
HeapSnapshot::NodeType::Object,
"undefined",
GCBase::IDTracker::reserved(
GCBase::IDTracker::ReservedObjectID::Undefined),
0,
0);
snap_.beginNode();
snap_.endNode(
HeapSnapshot::NodeType::Object,
"null",
GCBase::IDTracker::reserved(GCBase::IDTracker::ReservedObjectID::Null),
0,
0);
snap_.beginNode();
snap_.endNode(
HeapSnapshot::NodeType::Object,
"true",
GCBase::IDTracker::reserved(GCBase::IDTracker::ReservedObjectID::True),
0,
0);
snap_.beginNode();
snap_.endNode(
HeapSnapshot::NodeType::Object,
"false",
GCBase::IDTracker::reserved(GCBase::IDTracker::ReservedObjectID::False),
0,
0);
for (double num : seenNumbers_) {
// A number never has any edges, so just make a node for it.
snap_.beginNode();
// Convert the number value to a string, according to the JS conversion
// routines.
char buf[hermes::NUMBER_TO_STRING_BUF_SIZE];
size_t len = hermes::numberToString(num, buf, sizeof(buf));
snap_.endNode(
HeapSnapshot::NodeType::Number,
llvh::StringRef{buf, len},
tracker_.getNumberID(num),
// Numbers are zero-sized in the heap because they're stored inline.
0,
0);
}
}
private:
GCBase::IDTracker &tracker_;
// Track all numbers that are seen in a heap pass, and only emit one node for
// each of them.
llvh::DenseSet<double, GCBase::IDTracker::DoubleComparator> seenNumbers_;
};
struct EdgeAddingAcceptor : public SnapshotAcceptor, public WeakRefAcceptor {
using SnapshotAcceptor::accept;
EdgeAddingAcceptor(GCBase &gc, HeapSnapshot &snap)
: SnapshotAcceptor(gc.getPointerBase(), snap), gc_(gc) {}
void accept(GCCell *&ptr, const char *name) override {
if (!ptr) {
return;
}
snap_.addNamedEdge(
HeapSnapshot::EdgeType::Internal,
llvh::StringRef::withNullAsEmpty(name),
gc_.getObjectID(ptr));
}
void acceptHV(HermesValue &hv, const char *name) override {
if (auto id = gc_.getSnapshotID(hv)) {
snap_.addNamedEdge(
HeapSnapshot::EdgeType::Internal,
llvh::StringRef::withNullAsEmpty(name),
id.getValue());
}
}
void acceptSHV(SmallHermesValue &shv, const char *name) override {
HermesValue hv = shv.toHV(pointerBase_);
acceptHV(hv, name);
}
void accept(WeakRefBase &wr) override {
WeakRefSlot *slot = wr.unsafeGetSlot();
if (slot->state() == WeakSlotState::Free) {
// If the slot is free, there's no edge to add.
return;
}
if (!slot->hasPointer()) {
// Filter out empty refs from adding edges.
return;
}
// Assume all weak pointers have no names, and are stored in an array-like
// structure.
std::string indexName = std::to_string(nextEdge_++);
snap_.addNamedEdge(
HeapSnapshot::EdgeType::Weak,
indexName,
gc_.getObjectID(slot->getPointer()));
}
void acceptSym(SymbolID sym, const char *name) override {
if (sym.isInvalid()) {
return;
}
snap_.addNamedEdge(
HeapSnapshot::EdgeType::Internal,
llvh::StringRef::withNullAsEmpty(name),
gc_.getObjectID(sym));
}
private:
GCBase &gc_;
// For unnamed edges, use indices instead.
unsigned nextEdge_{0};
};
struct SnapshotRootSectionAcceptor : public SnapshotAcceptor,
public WeakAcceptorDefault {
using SnapshotAcceptor::accept;
using WeakRootAcceptor::acceptWeak;
SnapshotRootSectionAcceptor(PointerBase &base, HeapSnapshot &snap)
: SnapshotAcceptor(base, snap), WeakAcceptorDefault(base) {}
void accept(GCCell *&, const char *) override {
// While adding edges to root sections, there's no need to do anything for
// pointers.
}
void accept(WeakRefBase &wr) override {
// Same goes for weak refs.
}
void acceptWeak(GCCell *&ptr) override {
// Same goes for weak pointers.
}
void beginRootSection(Section section) override {
// Make an element edge from the super root to each root section.
snap_.addIndexedEdge(
HeapSnapshot::EdgeType::Element,
rootSectionNum_++,
objectIDForRootSection(section));
}
void endRootSection() override {
// Do nothing for the end of the root section.
}
private:
// v8's roots start numbering at 1.
int rootSectionNum_{1};
};
struct SnapshotRootAcceptor : public SnapshotAcceptor,
public WeakAcceptorDefault {
using SnapshotAcceptor::accept;
using WeakRootAcceptor::acceptWeak;
SnapshotRootAcceptor(GCBase &gc, HeapSnapshot &snap)
: SnapshotAcceptor(gc.getPointerBase(), snap),
WeakAcceptorDefault(gc.getPointerBase()),
gc_(gc) {}
void accept(GCCell *&ptr, const char *name) override {
pointerAccept(ptr, name, false);
}
void acceptWeak(GCCell *&ptr) override {
pointerAccept(ptr, nullptr, true);
}
void accept(WeakRefBase &wr) override {
WeakRefSlot *slot = wr.unsafeGetSlot();
if (slot->state() == WeakSlotState::Free) {
// If the slot is free, there's no edge to add.
return;
}
if (!slot->hasPointer()) {
// Filter out empty refs from adding edges.
return;
}
pointerAccept(slot->getPointer(), nullptr, true);
}
void acceptSym(SymbolID sym, const char *name) override {
if (sym.isInvalid()) {
return;
}
auto nameRef = llvh::StringRef::withNullAsEmpty(name);
const auto id = gc_.getObjectID(sym);
if (!nameRef.empty()) {
snap_.addNamedEdge(HeapSnapshot::EdgeType::Internal, nameRef, id);
} else {
// Unnamed edges get indices.
snap_.addIndexedEdge(HeapSnapshot::EdgeType::Element, nextEdge_++, id);
}
}
void provideSnapshot(
const std::function<void(HeapSnapshot &)> &func) override {
func(snap_);
}
void beginRootSection(Section section) override {
assert(
currentSection_ == Section::InvalidSection &&
"beginRootSection called while previous section is open");
snap_.beginNode();
currentSection_ = section;
}
void endRootSection() override {
// A root section creates a synthetic node with that name and makes edges
// come from that root.
static const char *rootNames[] = {
// Parentheses around the name is adopted from V8's roots.
#define ROOT_SECTION(name) "(" #name ")",
#include "hermes/VM/RootSections.def"
};
snap_.endNode(
HeapSnapshot::NodeType::Synthetic,
rootNames[static_cast<unsigned>(currentSection_)],
objectIDForRootSection(currentSection_),
// The heap visualizer doesn't like it when these synthetic nodes have a
// size (it describes them as living in the heap).
0,
0);
currentSection_ = Section::InvalidSection;
// Reset the edge counter, so each root section's unnamed edges start at
// zero.
nextEdge_ = 0;
}
private:
GCBase &gc_;
llvh::DenseSet<HeapSnapshot::NodeID> seenIDs_;
// For unnamed edges, use indices instead.
unsigned nextEdge_{0};
Section currentSection_{Section::InvalidSection};
void pointerAccept(GCCell *ptr, const char *name, bool weak) {
assert(
currentSection_ != Section::InvalidSection &&
"accept called outside of begin/end root section pair");
if (!ptr) {
return;
}
const auto id = gc_.getObjectID(ptr);
if (!seenIDs_.insert(id).second) {
// Already seen this node, don't add another edge.
return;
}
auto nameRef = llvh::StringRef::withNullAsEmpty(name);
if (!nameRef.empty()) {
snap_.addNamedEdge(
weak ? HeapSnapshot::EdgeType::Weak
: HeapSnapshot::EdgeType::Internal,
nameRef,
id);
} else if (weak) {
std::string numericName = std::to_string(nextEdge_++);
snap_.addNamedEdge(HeapSnapshot::EdgeType::Weak, numericName.c_str(), id);
} else {
// Unnamed edges get indices.
snap_.addIndexedEdge(HeapSnapshot::EdgeType::Element, nextEdge_++, id);
}
}
};
} // namespace
void GCBase::createSnapshot(GC *gc, llvh::raw_ostream &os) {
JSONEmitter json(os);
HeapSnapshot snap(json, gcCallbacks_.getStackTracesTree());
const auto rootScan = [gc, &snap, this]() {
{
// Make the super root node and add edges to each root section.
SnapshotRootSectionAcceptor rootSectionAcceptor(getPointerBase(), snap);
// The super root has a single element pointing to the "(GC roots)"
// synthetic node. v8 also has some "shortcut" edges to things like the
// global object, but those don't seem necessary for correctness.
snap.beginNode();
snap.addIndexedEdge(
HeapSnapshot::EdgeType::Element,
1,
IDTracker::reserved(IDTracker::ReservedObjectID::GCRoots));
snap.endNode(
HeapSnapshot::NodeType::Synthetic,
"",
IDTracker::reserved(IDTracker::ReservedObjectID::SuperRoot),
0,
0);
snapshotAddGCNativeNodes(snap);
snap.beginNode();
markRoots(rootSectionAcceptor, true);
markWeakRoots(rootSectionAcceptor, /*markLongLived*/ true);
snapshotAddGCNativeEdges(snap);
snap.endNode(
HeapSnapshot::NodeType::Synthetic,
"(GC roots)",
static_cast<HeapSnapshot::NodeID>(
IDTracker::reserved(IDTracker::ReservedObjectID::GCRoots)),
0,
0);
}
{
// Make a node for each root section and add edges into the actual heap.
// Within a root section, there might be duplicates. The root acceptor
// filters out duplicate edges because there cannot be duplicate edges to
// nodes reachable from the super root.
SnapshotRootAcceptor rootAcceptor(*gc, snap);
markRoots(rootAcceptor, true);
markWeakRoots(rootAcceptor, /*markLongLived*/ true);
}
gcCallbacks_.visitIdentifiers([&snap, this](
SymbolID sym,
const StringPrimitive *str) {
snap.beginNode();
if (str) {
snap.addNamedEdge(
HeapSnapshot::EdgeType::Internal, "description", getObjectID(str));
}
snap.endNode(
HeapSnapshot::NodeType::Symbol,
convertSymbolToUTF8(sym),
idTracker_.getObjectID(sym),
sizeof(SymbolID),
0);
});
};
snap.beginSection(HeapSnapshot::Section::Nodes);
rootScan();
// Add all primitive values as nodes if they weren't added before.
// This must be done as a step before adding any edges to these nodes.
// In particular, custom edge adders might try to add edges to primitives that
// haven't been recorded yet.
// The acceptor is recording some state between objects, so define it outside
// the loop.
PrimitiveNodeAcceptor primitiveAcceptor(
getPointerBase(), snap, getIDTracker());
SlotVisitorWithNames<PrimitiveNodeAcceptor> primitiveVisitor{
primitiveAcceptor};
// Add a node for each object in the heap.
const auto snapshotForObject =
[&snap, &primitiveVisitor, gc, this](GCCell *cell) {
auto &allocationLocationTracker = getAllocationLocationTracker();
// First add primitive nodes.
markCellWithNames(primitiveVisitor, cell);
EdgeAddingAcceptor acceptor(*gc, snap);
SlotVisitorWithNames<EdgeAddingAcceptor> visitor(acceptor);
// Allow nodes to add extra nodes not in the JS heap.
cell->getVT()->snapshotMetaData.addNodes(cell, gc, snap);
snap.beginNode();
// Add all internal edges first.
markCellWithNames(visitor, cell);
// Allow nodes to add custom edges not represented by metadata.
cell->getVT()->snapshotMetaData.addEdges(cell, gc, snap);
auto stackTracesTreeNode =
allocationLocationTracker.getStackTracesTreeNodeForAlloc(
gc->getObjectID(cell));
snap.endNode(
cell->getVT()->snapshotMetaData.nodeType(),
cell->getVT()->snapshotMetaData.nameForNode(cell, gc),
gc->getObjectID(cell),
cell->getAllocatedSize(),
stackTracesTreeNode ? stackTracesTreeNode->id : 0);
};
gc->forAllObjs(snapshotForObject);
// Write the singleton number nodes into the snapshot.
primitiveAcceptor.writeAllNodes();
snap.endSection(HeapSnapshot::Section::Nodes);
snap.beginSection(HeapSnapshot::Section::Edges);
rootScan();
// No need to run the primitive scan again, as it only adds nodes, not edges.
// Add edges between objects in the heap.
forAllObjs(snapshotForObject);
snap.endSection(HeapSnapshot::Section::Edges);
snap.emitAllocationTraceInfo();
snap.beginSection(HeapSnapshot::Section::Samples);
getAllocationLocationTracker().addSamplesToSnapshot(snap);
snap.endSection(HeapSnapshot::Section::Samples);
snap.beginSection(HeapSnapshot::Section::Locations);
forAllObjs([&snap, gc](GCCell *cell) {
cell->getVT()->snapshotMetaData.addLocations(cell, gc, snap);
});
snap.endSection(HeapSnapshot::Section::Locations);
}
void GCBase::snapshotAddGCNativeNodes(HeapSnapshot &snap) {
snap.beginNode();
snap.endNode(
HeapSnapshot::NodeType::Native,
"std::deque<WeakRefSlot>",
IDTracker::reserved(IDTracker::ReservedObjectID::WeakRefSlotStorage),
weakSlots_.size() * sizeof(decltype(weakSlots_)::value_type),
0);
}
void GCBase::snapshotAddGCNativeEdges(HeapSnapshot &snap) {
snap.addNamedEdge(
HeapSnapshot::EdgeType::Internal,
"weakRefSlots",
IDTracker::reserved(IDTracker::ReservedObjectID::WeakRefSlotStorage));
}
void GCBase::enableHeapProfiler(
std::function<void(
uint64_t,
std::chrono::microseconds,
std::vector<GCBase::AllocationLocationTracker::HeapStatsUpdate>)>
fragmentCallback) {
getAllocationLocationTracker().enable(std::move(fragmentCallback));
}
void GCBase::disableHeapProfiler() {
getAllocationLocationTracker().disable();
}
void GCBase::enableSamplingHeapProfiler(size_t samplingInterval, int64_t seed) {
getSamplingAllocationTracker().enable(samplingInterval, seed);
}
void GCBase::disableSamplingHeapProfiler(llvh::raw_ostream &os) {
getSamplingAllocationTracker().disable(os);
}
void GCBase::checkTripwire(size_t dataSize) {
if (LLVM_LIKELY(!tripwireCallback_) ||
LLVM_LIKELY(dataSize < tripwireLimit_) || tripwireCalled_) {
return;
}
class Ctx : public GCTripwireContext {
public:
Ctx(GCBase *gc) : gc_(gc) {}
std::error_code createSnapshotToFile(const std::string &path) override {
return gc_->createSnapshotToFile(path);
}
std::error_code createSnapshot(std::ostream &os) override {
llvh::raw_os_ostream ros(os);
gc_->createSnapshot(ros);
return std::error_code{};
}
private:
GCBase *gc_;
} ctx(this);
tripwireCalled_ = true;
tripwireCallback_(ctx);
}
void GCBase::printAllCollectedStats(llvh::raw_ostream &os) {
if (!recordGcStats_)
return;
dump(os);
os << "GC stats:\n";
JSONEmitter json{os, /*pretty*/ true};
json.openDict();
printStats(json);
json.closeDict();
os << "\n";
}
void GCBase::getHeapInfo(HeapInfo &info) {
info.numCollections = cumStats_.numCollections;
}
void GCBase::getHeapInfoWithMallocSize(HeapInfo &info) {
// Assign to overwrite anything previously in the heap info.
// A deque doesn't have a capacity, so the size is the lower bound.
info.mallocSizeEstimate =
weakSlots_.size() * sizeof(decltype(weakSlots_)::value_type);
}
#ifndef NDEBUG
void GCBase::getDebugHeapInfo(DebugHeapInfo &info) {
recordNumAllocatedObjects();
info.numAllocatedObjects = numAllocatedObjects_;
info.numReachableObjects = numReachableObjects_;
info.numCollectedObjects = numCollectedObjects_;
info.numFinalizedObjects = numFinalizedObjects_;
info.numMarkedSymbols = numMarkedSymbols_;
info.numHiddenClasses = numHiddenClasses_;
info.numLeafHiddenClasses = numLeafHiddenClasses_;
}
size_t GCBase::countUsedWeakRefs() const {
size_t count = 0;
for (auto &slot : weakSlots_) {
if (slot.state() != WeakSlotState::Free) {
++count;
}
}
return count;
}
#endif
#ifndef NDEBUG
void GCBase::DebugHeapInfo::assertInvariants() const {
// The number of allocated objects at any time is at least the number
// found reachable in the last collection.
assert(numAllocatedObjects >= numReachableObjects);
// The number of objects finalized in the last collection is at most the
// number of objects collected.
assert(numCollectedObjects >= numFinalizedObjects);
}
#endif
void GCBase::dump(llvh::raw_ostream &, bool) { /* nop */
}
void GCBase::printStats(JSONEmitter &json) {
json.emitKeyValue("type", "hermes");
json.emitKeyValue("version", 0);
gcCallbacks_.printRuntimeGCStats(json);
std::chrono::duration<double> elapsedTime =
std::chrono::steady_clock::now() - execStartTime_;
auto elapsedCPUSeconds =
std::chrono::duration_cast<std::chrono::duration<double>>(
oscompat::thread_cpu_time())
.count() -
std::chrono::duration_cast<std::chrono::duration<double>>(
execStartCPUTime_)
.count();
HeapInfo info;
getHeapInfoWithMallocSize(info);
getHeapInfo(info);
#ifndef NDEBUG
DebugHeapInfo debugInfo;
getDebugHeapInfo(debugInfo);
#endif
json.emitKey("heapInfo");
json.openDict();
#ifndef NDEBUG
json.emitKeyValue("Num allocated cells", debugInfo.numAllocatedObjects);
json.emitKeyValue("Num reachable cells", debugInfo.numReachableObjects);
json.emitKeyValue("Num collected cells", debugInfo.numCollectedObjects);
json.emitKeyValue("Num finalized cells", debugInfo.numFinalizedObjects);
json.emitKeyValue("Num marked symbols", debugInfo.numMarkedSymbols);
json.emitKeyValue("Num hidden classes", debugInfo.numHiddenClasses);
json.emitKeyValue("Num leaf classes", debugInfo.numLeafHiddenClasses);
json.emitKeyValue("Num weak references", ((GC *)this)->countUsedWeakRefs());
#endif
json.emitKeyValue("Peak RSS", oscompat::peak_rss());
json.emitKeyValue("Current RSS", oscompat::current_rss());
json.emitKeyValue("Current Dirty", oscompat::current_private_dirty());
json.emitKeyValue("Heap size", info.heapSize);
json.emitKeyValue("Allocated bytes", info.allocatedBytes);
json.emitKeyValue("Num collections", info.numCollections);
json.emitKeyValue("Malloc size", info.mallocSizeEstimate);
json.closeDict();
long vol = -1;
long invol = -1;
if (oscompat::num_context_switches(vol, invol)) {
vol -= startNumVoluntaryContextSwitches_;
invol -= startNumInvoluntaryContextSwitches_;
}
json.emitKey("general");
json.openDict();
json.emitKeyValue("numCollections", cumStats_.numCollections);
json.emitKeyValue("totalTime", elapsedTime.count());
json.emitKeyValue("totalCPUTime", elapsedCPUSeconds);
json.emitKeyValue("totalGCTime", formatSecs(cumStats_.gcWallTime.sum()).secs);
json.emitKeyValue("volCtxSwitch", vol);
json.emitKeyValue("involCtxSwitch", invol);
json.emitKeyValue(
"avgGCPause", formatSecs(cumStats_.gcWallTime.average()).secs);
json.emitKeyValue("maxGCPause", formatSecs(cumStats_.gcWallTime.max()).secs);
json.emitKeyValue(
"totalGCCPUTime", formatSecs(cumStats_.gcCPUTime.sum()).secs);
json.emitKeyValue(
"avgGCCPUPause", formatSecs(cumStats_.gcCPUTime.average()).secs);
json.emitKeyValue(
"maxGCCPUPause", formatSecs(cumStats_.gcCPUTime.max()).secs);
json.emitKeyValue("finalHeapSize", formatSize(cumStats_.finalHeapSize).bytes);
json.emitKeyValue(
"peakAllocatedBytes", formatSize(getPeakAllocatedBytes()).bytes);
json.emitKeyValue("peakLiveAfterGC", formatSize(getPeakLiveAfterGC()).bytes);
json.emitKeyValue(
"totalAllocatedBytes", formatSize(info.totalAllocatedBytes).bytes);
json.closeDict();
json.emitKey("collections");
json.openArray();
for (const auto &event : analyticsEvents_) {
json.openDict();
json.emitKeyValue("runtimeDescription", event.runtimeDescription);
json.emitKeyValue("gcKind", event.gcKind);
json.emitKeyValue("collectionType", event.collectionType);
json.emitKeyValue("cause", event.cause);
json.emitKeyValue("duration", event.duration.count());
json.emitKeyValue("cpuDuration", event.cpuDuration.count());
json.emitKeyValue("preAllocated", event.allocated.before);
json.emitKeyValue("postAllocated", event.allocated.after);
json.emitKeyValue("preSize", event.size.before);
json.emitKeyValue("postSize", event.size.after);
json.emitKeyValue("preExternal", event.external.before);
json.emitKeyValue("postExternal", event.external.after);
json.emitKeyValue("survivalRatio", event.survivalRatio);
json.emitKey("tags");
json.openArray();
for (const auto &tag : event.tags) {
json.emitValue(tag);
}
json.closeArray();
json.closeDict();
}
json.closeArray();
}
void GCBase::recordGCStats(
const GCAnalyticsEvent &event,
CumulativeHeapStats *stats,
bool onMutator) {
// Hades OG collections do not block the mutator, and so do not contribute to
// the max pause time or the total execution time.
if (onMutator)
stats->gcWallTime.record(
std::chrono::duration<double>(event.duration).count());
stats->gcCPUTime.record(
std::chrono::duration<double>(event.cpuDuration).count());
stats->finalHeapSize = event.size.after;
stats->usedBefore.record(event.allocated.before);
stats->usedAfter.record(event.allocated.after);
stats->numCollections++;
}
void GCBase::recordGCStats(const GCAnalyticsEvent &event, bool onMutator) {
if (analyticsCallback_) {
analyticsCallback_(event);
}
if (recordGcStats_) {
analyticsEvents_.push_back(event);
}
recordGCStats(event, &cumStats_, onMutator);
}
void GCBase::oom(std::error_code reason) {
char detailBuffer[400];
oomDetail(detailBuffer, reason);
#ifdef HERMESVM_EXCEPTION_ON_OOM
// No need to run finalizeAll, the exception will propagate and eventually run
// ~Runtime.
throw JSOutOfMemoryError(
std::string(detailBuffer) + "\ncall stack:\n" +
gcCallbacks_.getCallStackNoAlloc());
#else
hermesLog("HermesGC", "OOM: %s.", detailBuffer);
// Record the OOM custom data with the crash manager.
crashMgr_->setCustomData("HermesGCOOMDetailBasic", detailBuffer);
hermes_fatal("OOM", reason);
#endif
}
void GCBase::oomDetail(
llvh::MutableArrayRef<char> detailBuffer,
std::error_code reason) {
HeapInfo heapInfo;
getHeapInfo(heapInfo);
snprintf(
detailBuffer.data(),
detailBuffer.size(),
"[%.20s] reason = %.150s (%d from category: %.50s), numCollections = %u, heapSize = %" PRIu64
", allocated = %" PRIu64 ", va = %" PRIu64 ", external = %" PRIu64,
name_.c_str(),
reason.message().c_str(),
reason.value(),
reason.category().name(),
heapInfo.numCollections,
heapInfo.heapSize,
heapInfo.allocatedBytes,
heapInfo.va,
heapInfo.externalBytes);
}
#ifdef HERMESVM_SANITIZE_HANDLES
bool GCBase::shouldSanitizeHandles() {
static std::uniform_real_distribution<> dist(0.0, 1.0);
return dist(randomEngine_) < sanitizeRate_;
}
#endif
#ifdef HERMESVM_GC_RUNTIME
#define GCBASE_BARRIER_1(name, type1) \
void GCBase::name(type1 arg1) { \
runtimeGCDispatch([&](auto *gc) { gc->name(arg1); }); \
}
#define GCBASE_BARRIER_2(name, type1, type2) \
void GCBase::name(type1 arg1, type2 arg2) { \
runtimeGCDispatch([&](auto *gc) { gc->name(arg1, arg2); }); \
}
GCBASE_BARRIER_2(writeBarrier, const GCHermesValue *, HermesValue);
GCBASE_BARRIER_2(writeBarrier, const GCSmallHermesValue *, SmallHermesValue);
GCBASE_BARRIER_2(writeBarrier, const GCPointerBase *, const GCCell *);
GCBASE_BARRIER_2(constructorWriteBarrier, const GCHermesValue *, HermesValue);
GCBASE_BARRIER_2(
constructorWriteBarrier,
const GCSmallHermesValue *,
SmallHermesValue);
GCBASE_BARRIER_2(
constructorWriteBarrier,
const GCPointerBase *,
const GCCell *);
GCBASE_BARRIER_2(writeBarrierRange, const GCHermesValue *, uint32_t);
GCBASE_BARRIER_2(writeBarrierRange, const GCSmallHermesValue *, uint32_t);
GCBASE_BARRIER_2(constructorWriteBarrierRange, const GCHermesValue *, uint32_t);
GCBASE_BARRIER_2(
constructorWriteBarrierRange,
const GCSmallHermesValue *,
uint32_t);
GCBASE_BARRIER_1(snapshotWriteBarrier, const GCHermesValue *);
GCBASE_BARRIER_1(snapshotWriteBarrier, const GCSmallHermesValue *);
GCBASE_BARRIER_1(snapshotWriteBarrier, const GCPointerBase *);
GCBASE_BARRIER_1(snapshotWriteBarrier, const GCSymbolID *);
GCBASE_BARRIER_2(snapshotWriteBarrierRange, const GCHermesValue *, uint32_t);
GCBASE_BARRIER_2(
snapshotWriteBarrierRange,
const GCSmallHermesValue *,
uint32_t);
GCBASE_BARRIER_1(weakRefReadBarrier, GCCell *);
GCBASE_BARRIER_1(weakRefReadBarrier, HermesValue);
#undef GCBASE_BARRIER_1
#undef GCBASE_BARRIER_2
#endif
/*static*/
std::vector<detail::WeakRefKey *> GCBase::buildKeyList(
GC *gc,
JSWeakMap *weakMap) {
std::vector<detail::WeakRefKey *> res;
for (auto iter = weakMap->keys_begin(), end = weakMap->keys_end();
iter != end;
iter++) {
if (iter->getObjectInGC(gc)) {
res.push_back(&(*iter));
}
}
return res;
}
HeapSnapshot::NodeID GCBase::getObjectID(const GCCell *cell) {
assert(cell && "Called getObjectID on a null pointer");
return getObjectID(CompressedPointer::encodeNonNull(
const_cast<GCCell *>(cell), pointerBase_));
}
HeapSnapshot::NodeID GCBase::getObjectIDMustExist(const GCCell *cell) {
assert(cell && "Called getObjectID on a null pointer");
return idTracker_.getObjectIDMustExist(CompressedPointer::encodeNonNull(
const_cast<GCCell *>(cell), pointerBase_));
}
HeapSnapshot::NodeID GCBase::getObjectID(CompressedPointer cell) {
assert(cell && "Called getObjectID on a null pointer");
return idTracker_.getObjectID(cell);
}
HeapSnapshot::NodeID GCBase::getObjectID(SymbolID sym) {
return idTracker_.getObjectID(sym);
}
HeapSnapshot::NodeID GCBase::getNativeID(const void *mem) {
assert(mem && "Called getNativeID on a null pointer");
return idTracker_.getNativeID(mem);
}
bool GCBase::hasObjectID(const GCCell *cell) {
assert(cell && "Called hasObjectID on a null pointer");
return idTracker_.hasObjectID(CompressedPointer::encodeNonNull(
const_cast<GCCell *>(cell), pointerBase_));
}
void GCBase::newAlloc(const GCCell *ptr, uint32_t sz) {
allocationLocationTracker_.newAlloc(ptr, sz);
samplingAllocationTracker_.newAlloc(ptr, sz);
}
void GCBase::moveObject(
const GCCell *oldPtr,
uint32_t oldSize,
const GCCell *newPtr,
uint32_t newSize) {
idTracker_.moveObject(
CompressedPointer::encodeNonNull(
const_cast<GCCell *>(oldPtr), pointerBase_),
CompressedPointer::encodeNonNull(
const_cast<GCCell *>(newPtr), pointerBase_));
// Use newPtr here because the idTracker_ just moved it.
allocationLocationTracker_.updateSize(newPtr, oldSize, newSize);
samplingAllocationTracker_.updateSize(newPtr, oldSize, newSize);
}
void GCBase::untrackObject(const GCCell *cell, uint32_t sz) {
assert(cell && "Called untrackObject on a null pointer");
// The allocation tracker needs to use the ID, so this needs to come
// before untrackObject.
getAllocationLocationTracker().freeAlloc(cell, sz);
getSamplingAllocationTracker().freeAlloc(cell, sz);
idTracker_.untrackObject(CompressedPointer::encodeNonNull(
const_cast<GCCell *>(cell), pointerBase_));
}
#ifndef NDEBUG
uint64_t GCBase::nextObjectID() {
return debugAllocationCounter_++;
}
#endif
const GCExecTrace &GCBase::getGCExecTrace() const {
return execTrace_;
}
/*static*/
double GCBase::clockDiffSeconds(TimePoint start, TimePoint end) {
std::chrono::duration<double> elapsed = (end - start);
return elapsed.count();
}
/*static*/
double GCBase::clockDiffSeconds(
std::chrono::microseconds start,
std::chrono::microseconds end) {
std::chrono::duration<double> elapsed = (end - start);
return elapsed.count();
}
llvh::raw_ostream &operator<<(
llvh::raw_ostream &os,
const DurationFormatObj &dfo) {
if (dfo.secs >= 1.0) {
os << format("%5.3f", dfo.secs) << " s";
} else if (dfo.secs >= 0.001) {
os << format("%5.3f", dfo.secs * 1000.0) << " ms";
} else {
os << format("%5.3f", dfo.secs * 1000000.0) << " us";
}
return os;
}
llvh::raw_ostream &operator<<(llvh::raw_ostream &os, const SizeFormatObj &sfo) {
double dblsize = static_cast<double>(sfo.bytes);
if (sfo.bytes >= (1024 * 1024 * 1024)) {
double gbs = dblsize / (1024.0 * 1024.0 * 1024.0);
os << format("%0.3f GiB", gbs);
} else if (sfo.bytes >= (1024 * 1024)) {
double mbs = dblsize / (1024.0 * 1024.0);
os << format("%0.3f MiB", mbs);
} else if (sfo.bytes >= 1024) {
double kbs = dblsize / 1024.0;
os << format("%0.3f KiB", kbs);
} else {
os << sfo.bytes << " B";
}
return os;
}
GCBase::GCCallbacks::~GCCallbacks() {}
GCBase::IDTracker::IDTracker() {
assert(lastID_ % 2 == 1 && "First JS object ID isn't odd");
}
void GCBase::IDTracker::moveObject(
CompressedPointer oldLocation,
CompressedPointer newLocation) {
if (oldLocation == newLocation) {
// Don't need to do anything if the object isn't moving anywhere. This can
// happen in old generations where it is compacted to the same location.
return;
}
std::lock_guard<Mutex> lk{mtx_};
auto old = objectIDMap_.find(oldLocation.getRaw());
if (old == objectIDMap_.end()) {
// Avoid making new keys for objects that don't need to be tracked.
return;
}
const auto oldID = old->second;
assert(
objectIDMap_.count(newLocation.getRaw()) == 0 &&
"Moving to a location that is already tracked");
// Have to erase first, because any other access can invalidate the iterator.
objectIDMap_.erase(old);
objectIDMap_[newLocation.getRaw()] = oldID;
// Update the reverse map entry if it exists.
auto reverseMappingIt = idObjectMap_.find(oldID);
if (reverseMappingIt != idObjectMap_.end()) {
assert(
reverseMappingIt->second == oldLocation.getRaw() &&
"The reverse mapping should have the old address");
reverseMappingIt->second = newLocation.getRaw();
}
}
llvh::SmallVector<HeapSnapshot::NodeID, 1>
&GCBase::IDTracker::getExtraNativeIDs(HeapSnapshot::NodeID node) {
std::lock_guard<Mutex> lk{mtx_};
// The operator[] will default construct the vector to be empty if it doesn't
// exist.
return extraNativeIDs_[node];
}
HeapSnapshot::NodeID GCBase::IDTracker::getNumberID(double num) {
std::lock_guard<Mutex> lk{mtx_};
auto &numberRef = numberIDMap_[num];
// If the entry didn't exist, the value was initialized to 0.
if (numberRef != 0) {
return numberRef;
}
// Else, it is a number that hasn't been seen before.
return numberRef = nextNumberID();
}
llvh::Optional<CompressedPointer> GCBase::IDTracker::getObjectForID(
HeapSnapshot::NodeID id) {
std::lock_guard<Mutex> lk{mtx_};
auto it = idObjectMap_.find(id);
if (it != idObjectMap_.end()) {
return CompressedPointer::fromRaw(it->second);
}
// Do an O(N) search through the map, then cache the result.
// This trades time for memory, since this is a rare operation.
for (const auto &p : objectIDMap_) {
if (p.second == id) {
// Cache the result so repeated lookups are fast.
// This cache is unlikely to grow that large, unless someone hovers over
// every single object in a snapshot in Chrome.
auto itAndDidInsert = idObjectMap_.try_emplace(p.second, p.first);
assert(itAndDidInsert.second);
return CompressedPointer::fromRaw(itAndDidInsert.first->second);
}
}
// ID not found in the map, wasn't an object to begin with.
return llvh::None;
}
bool GCBase::IDTracker::hasNativeIDs() {
std::lock_guard<Mutex> lk{mtx_};
return !nativeIDMap_.empty();
}
bool GCBase::IDTracker::isTrackingIDs() {
std::lock_guard<Mutex> lk{mtx_};
return !objectIDMap_.empty();
}
HeapSnapshot::NodeID GCBase::IDTracker::getObjectID(CompressedPointer cell) {
std::lock_guard<Mutex> lk{mtx_};
auto iter = objectIDMap_.find(cell.getRaw());
if (iter != objectIDMap_.end()) {
return iter->second;
}
// Else, assume it is an object that needs to be tracked and give it a new ID.
const auto objID = nextObjectID();
objectIDMap_[cell.getRaw()] = objID;
return objID;
}
bool GCBase::IDTracker::hasObjectID(CompressedPointer cell) {
std::lock_guard<Mutex> lk{mtx_};
return objectIDMap_.count(cell.getRaw());
}
HeapSnapshot::NodeID GCBase::IDTracker::getObjectIDMustExist(
CompressedPointer cell) {
std::lock_guard<Mutex> lk{mtx_};
auto iter = objectIDMap_.find(cell.getRaw());
assert(iter != objectIDMap_.end() && "cell must already have an ID");
return iter->second;
}
HeapSnapshot::NodeID GCBase::IDTracker::getObjectID(SymbolID sym) {
std::lock_guard<Mutex> lk{mtx_};
auto iter = symbolIDMap_.find(sym.unsafeGetIndex());
if (iter != symbolIDMap_.end()) {
return iter->second;
}
// Else, assume it is a symbol that needs to be tracked and give it a new ID.
const auto symID = nextObjectID();
symbolIDMap_[sym.unsafeGetIndex()] = symID;
return symID;
}
HeapSnapshot::NodeID GCBase::IDTracker::getNativeID(const void *mem) {
std::lock_guard<Mutex> lk{mtx_};
auto iter = nativeIDMap_.find(mem);
if (iter != nativeIDMap_.end()) {
return iter->second;
}
// Else, assume it is a piece of native memory that needs to be tracked and
// give it a new ID.
const auto objID = nextNativeID();
nativeIDMap_[mem] = objID;
return objID;
}
void GCBase::IDTracker::untrackObject(CompressedPointer cell) {
std::lock_guard<Mutex> lk{mtx_};
// It's ok if this didn't exist before, since erase will remove it anyway, and
// the default constructed zero ID won't be present in extraNativeIDs_.
const auto id = objectIDMap_[cell.getRaw()];
objectIDMap_.erase(cell.getRaw());
extraNativeIDs_.erase(id);
// Erase the reverse mapping entry if it exists.
idObjectMap_.erase(id);
}
void GCBase::IDTracker::untrackNative(const void *mem) {
std::lock_guard<Mutex> lk{mtx_};
nativeIDMap_.erase(mem);
}
void GCBase::IDTracker::untrackSymbol(uint32_t symIdx) {
std::lock_guard<Mutex> lk{mtx_};
symbolIDMap_.erase(symIdx);
}
HeapSnapshot::NodeID GCBase::IDTracker::lastID() const {
return lastID_;
}
HeapSnapshot::NodeID GCBase::IDTracker::nextObjectID() {
// This must be unique for most features that rely on it, check for overflow.
if (LLVM_UNLIKELY(
lastID_ >=
std::numeric_limits<HeapSnapshot::NodeID>::max() - kIDStep)) {
hermes_fatal("Ran out of object IDs");
}
return lastID_ += kIDStep;
}
HeapSnapshot::NodeID GCBase::IDTracker::nextNativeID() {
// Calling nextObjectID effectively allocates two new IDs, one even
// and one odd, returning the latter. For native objects, we want the former.
HeapSnapshot::NodeID id = nextObjectID();
assert(id > 0 && "nextObjectID should check for overflow");
return id - 1;
}
HeapSnapshot::NodeID GCBase::IDTracker::nextNumberID() {
// Numbers will all be considered JS memory, not native memory.
return nextObjectID();
}
GCBase::AllocationLocationTracker::AllocationLocationTracker(GCBase *gc)
: gc_(gc) {}
bool GCBase::AllocationLocationTracker::isEnabled() const {
return enabled_;
}
StackTracesTreeNode *
GCBase::AllocationLocationTracker::getStackTracesTreeNodeForAlloc(
HeapSnapshot::NodeID id) const {
auto mapIt = stackMap_.find(id);
return mapIt == stackMap_.end() ? nullptr : mapIt->second;
}
void GCBase::AllocationLocationTracker::enable(
std::function<
void(uint64_t, std::chrono::microseconds, std::vector<HeapStatsUpdate>)>
callback) {
assert(!enabled_ && "Shouldn't enable twice");
enabled_ = true;
std::lock_guard<Mutex> lk{mtx_};
// For correct visualization of the allocation timeline, it's necessary that
// objects in the heap snapshot that existed before sampling was enabled have
// numerically lower IDs than those allocated during sampling. We ensure this
// by assigning IDs to everything here.
uint64_t numObjects = 0;
uint64_t numBytes = 0;
gc_->forAllObjs([&numObjects, &numBytes, this](GCCell *cell) {
numObjects++;
numBytes += cell->getAllocatedSize();
gc_->getObjectID(cell);
});
fragmentCallback_ = std::move(callback);
startTime_ = std::chrono::steady_clock::now();
fragments_.clear();
// The first fragment has all objects that were live before the profiler was
// enabled.
// The ID and timestamp will be filled out via flushCallback.
fragments_.emplace_back(Fragment{
IDTracker::kInvalidNode,
std::chrono::microseconds(),
numObjects,
numBytes,
// Say the fragment is touched here so it is written out
// automatically by flushCallback.
true});
// Immediately flush the first fragment.
flushCallback();
}
void GCBase::AllocationLocationTracker::disable() {
std::lock_guard<Mutex> lk{mtx_};
flushCallback();
enabled_ = false;
fragmentCallback_ = nullptr;
}
void GCBase::AllocationLocationTracker::newAlloc(
const GCCell *ptr,
uint32_t sz) {
// Note we always get the current IP even if allocation tracking is not
// enabled as it allows us to assert this feature works across many tests.
// Note it's not very slow, it's slower than the non-virtual version
// in Runtime though.
const auto *ip = gc_->gcCallbacks_.getCurrentIPSlow();
if (!enabled_) {
return;
}
std::lock_guard<Mutex> lk{mtx_};
// This is stateful and causes the object to have an ID assigned.
const auto id = gc_->getObjectID(ptr);
HERMES_SLOW_ASSERT(
&findFragmentForID(id) == &fragments_.back() &&
"Should only ever be allocating into the newest fragment");
Fragment &lastFrag = fragments_.back();
assert(
lastFrag.lastSeenObjectID_ == IDTracker::kInvalidNode &&
"Last fragment should not have an ID assigned yet");
lastFrag.numObjects_++;
lastFrag.numBytes_ += sz;
lastFrag.touchedSinceLastFlush_ = true;
if (lastFrag.numBytes_ >= kFlushThreshold) {
flushCallback();
}
if (auto node = gc_->gcCallbacks_.getCurrentStackTracesTreeNode(ip)) {
auto itAndDidInsert = stackMap_.try_emplace(id, node);
assert(itAndDidInsert.second && "Failed to create a new node");
(void)itAndDidInsert;
}
}
void GCBase::AllocationLocationTracker::updateSize(
const GCCell *ptr,
uint32_t oldSize,
uint32_t newSize) {
int32_t delta = static_cast<int32_t>(newSize) - static_cast<int32_t>(oldSize);
if (!delta || !enabled_) {
// Nothing to update.
return;
}
std::lock_guard<Mutex> lk{mtx_};
const auto id = gc_->getObjectIDMustExist(ptr);
Fragment &frag = findFragmentForID(id);
frag.numBytes_ += delta;
frag.touchedSinceLastFlush_ = true;
}
void GCBase::AllocationLocationTracker::freeAlloc(
const GCCell *ptr,
uint32_t sz) {
if (!enabled_) {
// Fragments won't exist if the heap profiler isn't enabled.
return;
}
// Hold a lock during freeAlloc because concurrent Hades might be creating an
// alloc (newAlloc) at the same time.
std::lock_guard<Mutex> lk{mtx_};
// The ID must exist here since the memory profiler guarantees everything has
// an ID (it does a heap pass at the beginning to assign them all).
const auto id = gc_->getObjectIDMustExist(ptr);
stackMap_.erase(id);
Fragment &frag = findFragmentForID(id);
assert(
frag.numObjects_ >= 1 && "Num objects decremented too much for fragment");
frag.numObjects_--;
assert(frag.numBytes_ >= sz && "Num bytes decremented too much for fragment");
frag.numBytes_ -= sz;
frag.touchedSinceLastFlush_ = true;
}
GCBase::AllocationLocationTracker::Fragment &
GCBase::AllocationLocationTracker::findFragmentForID(HeapSnapshot::NodeID id) {
assert(fragments_.size() >= 1 && "Must have at least one fragment available");
for (auto it = fragments_.begin(); it != fragments_.end() - 1; ++it) {
if (it->lastSeenObjectID_ >= id) {
return *it;
}
}
// Since no previous fragments matched, it must be the last fragment.
return fragments_.back();
}
void GCBase::AllocationLocationTracker::flushCallback() {
Fragment &lastFrag = fragments_.back();
const auto lastID = gc_->getIDTracker().lastID();
const auto duration = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - startTime_);
assert(
lastFrag.lastSeenObjectID_ == IDTracker::kInvalidNode &&
"Last fragment should not have an ID assigned yet");
// In case a flush happens without any allocations occurring, don't add a new
// fragment.
if (lastFrag.touchedSinceLastFlush_) {
lastFrag.lastSeenObjectID_ = lastID;
lastFrag.timestamp_ = duration;
// Place an empty fragment at the end, for any new allocs.
fragments_.emplace_back(Fragment{
IDTracker::kInvalidNode, std::chrono::microseconds(), 0, 0, false});
}
if (fragmentCallback_) {
std::vector<HeapStatsUpdate> updatedFragments;
// Don't include the last fragment, which is newly created (or has no
// objects in it).
for (size_t i = 0; i < fragments_.size() - 1; ++i) {
auto &fragment = fragments_[i];
if (fragment.touchedSinceLastFlush_) {
updatedFragments.emplace_back(
i, fragment.numObjects_, fragment.numBytes_);
fragment.touchedSinceLastFlush_ = false;
}
}
fragmentCallback_(lastID, duration, std::move(updatedFragments));
}
}
void GCBase::AllocationLocationTracker::addSamplesToSnapshot(
HeapSnapshot &snap) {
std::lock_guard<Mutex> lk{mtx_};
if (enabled_) {
flushCallback();
}
// There might not be fragments if tracking has never been enabled. If there
// are, the last one is always invalid.
assert(
(fragments_.empty() ||
fragments_.back().lastSeenObjectID_ == IDTracker::kInvalidNode) &&
"Last fragment should not have an ID assigned yet");
// Loop over the fragments if we have any, and always skip the last one.
for (size_t i = 0, e = fragments_.size(); i + 1 < e; ++i) {
const auto &fragment = fragments_[i];
snap.addSample(fragment.timestamp_, fragment.lastSeenObjectID_);
}
}
void GCBase::SamplingAllocationLocationTracker::enable(
size_t samplingInterval,
int64_t seed) {
if (seed < 0) {
seed = std::random_device()();
}
randomEngine_.seed(seed);
dist_ = llvh::make_unique<std::poisson_distribution<>>(samplingInterval);
limit_ = nextSample();
}
void GCBase::SamplingAllocationLocationTracker::disable(llvh::raw_ostream &os) {
JSONEmitter json{os};
ChromeSamplingMemoryProfile profile{json};
std::lock_guard<Mutex> lk{mtx_};
// Track a map of size -> count for each stack tree node.
llvh::DenseMap<StackTracesTreeNode *, llvh::DenseMap<size_t, size_t>>
sizesToCounts;
// Do a pre-pass to compute sizesToCounts.
for (const auto &s : samples_) {
const Sample &sample = s.second;
sizesToCounts[sample.node][sample.size]++;
}
// Have to emit the tree of stack frames before emitting samples, Chrome
// requires the tree emitted first.
profile.emitTree(gc_->gcCallbacks_.getStackTracesTree(), sizesToCounts);
profile.beginSamples();
for (const auto &s : samples_) {
const Sample &sample = s.second;
profile.emitSample(sample.size, sample.node, sample.id);
}
profile.endSamples();
dist_.reset();
samples_.clear();
limit_ = 0;
}
void GCBase::SamplingAllocationLocationTracker::newAlloc(
const GCCell *ptr,
uint32_t sz) {
// If the sampling profiler isn't enabled, don't check anything else.
if (!isEnabled()) {
return;
}
if (sz <= limit_) {
// Exit if it's not time for a sample yet.
limit_ -= sz;
return;
}
const auto *ip = gc_->gcCallbacks_.getCurrentIPSlow();
// This is stateful and causes the object to have an ID assigned.
const auto id = gc_->getObjectID(ptr);
if (StackTracesTreeNode *node =
gc_->gcCallbacks_.getCurrentStackTracesTreeNode(ip)) {
// Hold a lock while modifying samples_.
std::lock_guard<Mutex> lk{mtx_};
auto sampleItAndDidInsert =
samples_.try_emplace(id, Sample{sz, node, nextSampleID_++});
assert(sampleItAndDidInsert.second && "Failed to create a sample");
(void)sampleItAndDidInsert;
}
// Reset the limit.
limit_ = nextSample();
}
void GCBase::SamplingAllocationLocationTracker::freeAlloc(
const GCCell *ptr,
uint32_t sz) {
// If the sampling profiler isn't enabled, don't check anything else.
if (!isEnabled()) {
return;
}
if (!gc_->hasObjectID(ptr)) {
// This object's lifetime isn't being tracked.
return;
}
const auto id = gc_->getObjectIDMustExist(ptr);
// Hold a lock while modifying samples_.
std::lock_guard<Mutex> lk{mtx_};
samples_.erase(id);
}
void GCBase::SamplingAllocationLocationTracker::updateSize(
const GCCell *ptr,
uint32_t oldSize,
uint32_t newSize) {
int32_t delta = static_cast<int32_t>(newSize) - static_cast<int32_t>(oldSize);
if (!delta || !isEnabled() || !gc_->hasObjectID(ptr)) {
// Nothing to update.
return;
}
const auto id = gc_->getObjectIDMustExist(ptr);
// Hold a lock while modifying samples_.
std::lock_guard<Mutex> lk{mtx_};
const auto it = samples_.find(id);
if (it == samples_.end()) {
return;
}
Sample &sample = it->second;
// Update the size stored in the sample.
sample.size = newSize;
}
size_t GCBase::SamplingAllocationLocationTracker::nextSample() {
return (*dist_)(randomEngine_);
}
llvh::Optional<HeapSnapshot::NodeID> GCBase::getSnapshotID(HermesValue val) {
if (val.isPointer() && val.getPointer()) {
// Make nullptr HermesValue look like a JS null.
// This should be rare, but is occasionally used by some parts of the VM.
return val.getPointer()
? getObjectID(static_cast<GCCell *>(val.getPointer()))
: IDTracker::reserved(IDTracker::ReservedObjectID::Null);
} else if (val.isNumber()) {
return idTracker_.getNumberID(val.getNumber());
} else if (val.isSymbol() && val.getSymbol().isValid()) {
return idTracker_.getObjectID(val.getSymbol());
} else if (val.isUndefined()) {
return IDTracker::reserved(IDTracker::ReservedObjectID::Undefined);
} else if (val.isNull()) {
return static_cast<HeapSnapshot::NodeID>(
IDTracker::reserved(IDTracker::ReservedObjectID::Null));
} else if (val.isBool()) {
return static_cast<HeapSnapshot::NodeID>(
val.getBool()
? IDTracker::reserved(IDTracker::ReservedObjectID::True)
: IDTracker::reserved(IDTracker::ReservedObjectID::False));
} else {
return llvh::None;
}
}
void *GCBase::getObjectForID(HeapSnapshot::NodeID id) {
if (llvh::Optional<CompressedPointer> ptr = idTracker_.getObjectForID(id)) {
return ptr->get(pointerBase_);
}
return nullptr;
}
void GCBase::sizeDiagnosticCensus(size_t allocatedBytes) {
struct DiagnosticStat {
uint64_t count{0};
uint64_t size{0};
std::map<std::string, DiagnosticStat> breakdown;
static constexpr double getPercent(double numer, double denom) {
return denom != 0 ? 100 * numer / denom : 0.0;
}
void printBreakdown(size_t depth) const {
if (breakdown.empty())
return;
static const char *fmtBase =
"%-25s : %'10" PRIu64 " [%'10" PRIu64 " B | %4.1f%%]";
const std::string fmtStr = std::string(depth, '\t') + fmtBase;
size_t totalSize = 0;
size_t totalCount = 0;
for (const auto &stat : breakdown) {
hermesLog(
"HermesGC",
fmtStr.c_str(),
stat.first.c_str(),
stat.second.count,
stat.second.size,
getPercent(stat.second.size, size));
stat.second.printBreakdown(depth + 1);
totalSize += stat.second.size;
totalCount += stat.second.count;
}
if (size_t other = size - totalSize)
hermesLog(
"HermesGC",
fmtStr.c_str(),
"Other",
count - totalCount,
other,
getPercent(other, size));
}
};
struct HeapSizeDiagnostic {
uint64_t numCell = 0;
uint64_t numVariableSizedObject = 0;
DiagnosticStat stats;
void rootsDiagnosticFrame() const {
// Use this to print commas on large numbers
char *currentLocale = std::setlocale(LC_NUMERIC, nullptr);
std::setlocale(LC_NUMERIC, "");
hermesLog("HermesGC", "Root size: %'7" PRIu64 " B", stats.size);
stats.printBreakdown(1);
std::setlocale(LC_NUMERIC, currentLocale);
}
void sizeDiagnosticFrame() const {
// Use this to print commas on large numbers
char *currentLocale = std::setlocale(LC_NUMERIC, nullptr);
std::setlocale(LC_NUMERIC, "");
hermesLog("HermesGC", "Heap size: %'7" PRIu64 " B", stats.size);
hermesLog("HermesGC", "\tTotal cells: %'7" PRIu64, numCell);
hermesLog(
"HermesGC",
"\tNum variable size cells: %'7" PRIu64,
numVariableSizedObject);
stats.printBreakdown(1);
std::setlocale(LC_NUMERIC, currentLocale);
}
};
struct HeapSizeDiagnosticAcceptor final : public RootAndSlotAcceptor {
// Can't be static in a local class.
const int64_t HINT8_MIN = -(1 << 7);
const int64_t HINT8_MAX = (1 << 7) - 1;
const int64_t HINT16_MIN = -(1 << 15);
const int64_t HINT16_MAX = (1 << 15) - 1;
const int64_t HINT24_MIN = -(1 << 23);
const int64_t HINT24_MAX = (1 << 23) - 1;
const int64_t HINT32_MIN = -(1LL << 31);
const int64_t HINT32_MAX = (1LL << 31) - 1;
HeapSizeDiagnostic diagnostic;
PointerBase &pointerBase_;
HeapSizeDiagnosticAcceptor(PointerBase &pb) : pointerBase_{pb} {}
using SlotAcceptor::accept;
void accept(GCCell *&ptr) override {
diagnostic.stats.breakdown["Pointer"].count++;
diagnostic.stats.breakdown["Pointer"].size += sizeof(GCCell *);
}
void accept(GCPointerBase &ptr) override {
diagnostic.stats.breakdown["GCPointer"].count++;
diagnostic.stats.breakdown["GCPointer"].size += sizeof(GCPointerBase);
}
void accept(PinnedHermesValue &hv) override {
acceptNullable(hv);
}
void acceptNullable(PinnedHermesValue &hv) override {
acceptHV(
hv,
diagnostic.stats.breakdown["HermesValue"],
sizeof(PinnedHermesValue));
}
void accept(GCHermesValue &hv) override {
acceptHV(
hv, diagnostic.stats.breakdown["HermesValue"], sizeof(GCHermesValue));
}
void accept(GCSmallHermesValue &shv) override {
acceptHV(
shv.toHV(pointerBase_),
diagnostic.stats.breakdown["SmallHermesValue"],
sizeof(GCSmallHermesValue));
}
void acceptHV(
const HermesValue &hv,
DiagnosticStat &diag,
const size_t hvBytes) {
diag.count++;
diag.size += hvBytes;
llvh::StringRef hvType;
if (hv.isBool()) {
hvType = "Bool";
} else if (hv.isNumber()) {
hvType = "Number";
double val = hv.getNumber();
double intpart;
llvh::StringRef numType = "Doubles";
if (std::modf(val, &intpart) == 0.0) {
if (val >= static_cast<double>(HINT8_MIN) &&
val <= static_cast<double>(HINT8_MAX)) {
numType = "Int8";
} else if (
val >= static_cast<double>(HINT16_MIN) &&
val <= static_cast<double>(HINT16_MAX)) {
numType = "Int16";
} else if (
val >= static_cast<double>(HINT24_MIN) &&
val <= static_cast<double>(HINT24_MAX)) {
numType = "Int24";
} else if (
val >= static_cast<double>(HINT32_MIN) &&
val <= static_cast<double>(HINT32_MAX)) {
numType = "Int32";
}
}
diag.breakdown["Number"].breakdown[numType].count++;
diag.breakdown["Number"].breakdown[numType].size += hvBytes;
} else if (hv.isString()) {
hvType = "StringPointer";
} else if (hv.isSymbol()) {
hvType = "Symbol";
} else if (hv.isObject()) {
hvType = "ObjectPointer";
} else if (hv.isNull()) {
hvType = "Null";
} else if (hv.isUndefined()) {
hvType = "Undefined";
} else if (hv.isEmpty()) {
hvType = "Empty";
} else if (hv.isNativeValue()) {
hvType = "NativeValue";
} else {
assert(false && "Should be no other hermes values");
}
diag.breakdown[hvType].count++;
diag.breakdown[hvType].size += hvBytes;
}
void accept(const RootSymbolID &sym) override {
acceptSym(sym);
}
void accept(const GCSymbolID &sym) override {
acceptSym(sym);
}
void acceptSym(SymbolID sym) {
diagnostic.stats.breakdown["Symbol"].count++;
diagnostic.stats.breakdown["Symbol"].size += sizeof(SymbolID);
}
};
hermesLog("HermesGC", "%s:", "Roots");
HeapSizeDiagnosticAcceptor rootAcceptor{getPointerBase()};
DroppingAcceptor<HeapSizeDiagnosticAcceptor> namedRootAcceptor{rootAcceptor};
markRoots(namedRootAcceptor, /* markLongLived */ true);
// For roots, compute the overall size and counts from the breakdown.
for (const auto &substat : rootAcceptor.diagnostic.stats.breakdown) {
rootAcceptor.diagnostic.stats.count += substat.second.count;
rootAcceptor.diagnostic.stats.size += substat.second.size;
}
rootAcceptor.diagnostic.rootsDiagnosticFrame();
hermesLog("HermesGC", "%s:", "Heap contents");
HeapSizeDiagnosticAcceptor acceptor{getPointerBase()};
forAllObjs([&acceptor, this](GCCell *cell) {
markCell(cell, acceptor);
acceptor.diagnostic.numCell++;
if (cell->isVariableSize()) {
acceptor.diagnostic.numVariableSizedObject++;
// In theory should use sizeof(VariableSizeRuntimeCell), but that includes
// padding sometimes. To be conservative, use the field it contains
// directly instead.
acceptor.diagnostic.stats.breakdown["Cell headers"].size +=
(sizeof(GCCell) + sizeof(uint32_t));
} else {
acceptor.diagnostic.stats.breakdown["Cell headers"].size +=
sizeof(GCCell);
}
// We include ExternalStringPrimitives because we're including external
// memory in the overall heap size. We do not include
// BufferedStringPrimitives because they just store a pointer to an
// ExternalStringPrimitive (which is already tracked).
auto *strprim = dyn_vmcast<StringPrimitive>(cell);
if (strprim && !isBufferedStringPrimitive(cell)) {
auto &stat = strprim->isASCII()
? acceptor.diagnostic.stats.breakdown["StringPrimitive (ASCII)"]
: acceptor.diagnostic.stats.breakdown["StringPrimitive (UTF-16)"];
stat.count++;
const size_t len = strprim->getStringLength();
// If the string is UTF-16 then the length is in terms of 16 bit
// characters.
const size_t sz = strprim->isASCII() ? len : len * 2;
stat.size += sz;
if (len < 8) {
auto &subStat =
stat.breakdown
["StringPrimitive (size " + std::to_string(len) + ")"];
subStat.count++;
subStat.size += sz;
}
}
});
assert(
acceptor.diagnostic.stats.size == 0 &&
acceptor.diagnostic.stats.count == 0 &&
"Should not be setting overall stats during heap scan.");
for (const auto &substat : acceptor.diagnostic.stats.breakdown)
acceptor.diagnostic.stats.count += substat.second.count;
acceptor.diagnostic.stats.size = allocatedBytes;
acceptor.diagnostic.sizeDiagnosticFrame();
}
} // namespace vm
} // namespace hermes
#undef DEBUG_TYPE