Bug 1692221 - Update list of free committed arenas in TenuredChunk::decommitFreeArenasWithoutUnlocking r=sfink

This adds methods to verify the chunk metadata very GC in debug builds (held relocated arenas mess things up because they're mprotected so it's checked after we free those).

The code in question was hard to exercise and I had to update reportLargeAllocationFailure to take a byte count so we could avoid calling the shell's large allocation failure callback, which causes the shell to exit (see JSRuntime::onOutOfMemoryCanGC).

It's debateable whether it's worth handling OOM from MarkPagesUnusedSoft. If we assume pages have been been successfully marked unused regardless of failure I think the worst that happens is that we don't reuse them immediately... I ended up handling this anyway though, and rebuilding the list rather than clearing it entirely in decommitFreeArenasWithoutUnlocking.

Differential Revision: https://phabricator.services.mozilla.com/D104837
This commit is contained in:
Jon Coppeard 2021-02-16 11:59:16 +00:00
Родитель 9a0c3c6009
Коммит b8dbeb621f
7 изменённых файлов: 136 добавлений и 8 удалений

Просмотреть файл

@ -98,7 +98,7 @@ struct TenuredChunkInfo {
TenuredChunk* prev = nullptr;
public:
/* Free arenas are linked together with arena.next. */
/* List of free committed arenas, linked together with arena.next. */
Arena* freeArenasHead;
/*

Просмотреть файл

@ -4212,8 +4212,21 @@ static bool ThrowOutOfMemory(JSContext* cx, unsigned argc, Value* vp) {
static bool ReportLargeAllocationFailure(JSContext* cx, unsigned argc,
Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
void* buf = cx->runtime()->onOutOfMemoryCanGC(
AllocFunction::Malloc, js::MallocArena, JSRuntime::LARGE_ALLOCATION);
size_t bytes = JSRuntime::LARGE_ALLOCATION;
if (args.length() >= 1) {
if (!args[0].isInt32()) {
RootedObject callee(cx, &args.callee());
ReportUsageErrorASCII(cx, callee,
"First argument must be an integer if specified.");
return false;
}
bytes = args[0].toInt32();
}
void* buf = cx->runtime()->onOutOfMemoryCanGC(AllocFunction::Malloc,
js::MallocArena, bytes);
js_free(buf);
args.rval().setUndefined();
return true;
@ -6993,7 +7006,7 @@ gc::ZealModeHelpText),
" Throw out of memory exception, for OOM handling testing."),
JS_FN_HELP("reportLargeAllocationFailure", ReportLargeAllocationFailure, 0, 0,
"reportLargeAllocationFailure()",
"reportLargeAllocationFailure([bytes])",
" Call the large allocation failure callback, as though a large malloc call failed,\n"
" then return undefined. In Gecko, this sends a memory pressure notification, which\n"
" can free up some memory."),

Просмотреть файл

@ -872,6 +872,10 @@ void TenuredChunk::init(GCRuntime* gc) {
*/
decommitAllArenas();
#ifdef DEBUG
verify();
#endif
/* The rest of info fields are initialized in pickChunk. */
}

Просмотреть файл

@ -723,6 +723,7 @@ bool ChunkPool::isSorted() const {
}
#ifdef DEBUG
bool ChunkPool::contains(TenuredChunk* chunk) const {
verify();
for (TenuredChunk* cursor = head_; cursor; cursor = cursor->info.next) {
@ -744,6 +745,48 @@ bool ChunkPool::verify() const {
MOZ_ASSERT(count_ == count);
return true;
}
void GCRuntime::verifyAllChunks() {
AutoLockGC lock(this);
fullChunks(lock).verifyChunks();
availableChunks(lock).verifyChunks();
emptyChunks(lock).verifyChunks();
}
void ChunkPool::verifyChunks() const {
for (TenuredChunk* chunk = head_; chunk; chunk = chunk->info.next) {
chunk->verify();
}
}
void TenuredChunk::verify() const {
size_t freeCount = 0;
size_t freeCommittedCount = 0;
for (size_t i = 0; i < ArenasPerChunk; ++i) {
if (decommittedArenas[i]) {
// Free but not committed.
freeCount++;
continue;
}
if (!arenas[i].allocated()) {
// Free and committed.
freeCount++;
freeCommittedCount++;
}
}
MOZ_ASSERT(freeCount == info.numArenasFree);
MOZ_ASSERT(freeCommittedCount == info.numArenasFreeCommitted);
size_t freeListCount = 0;
for (Arena* arena = info.freeArenasHead; arena; arena = arena->next) {
freeListCount++;
}
MOZ_ASSERT(freeListCount == info.numArenasFreeCommitted);
}
#endif
void ChunkPool::Iter::next() {
@ -852,16 +895,31 @@ bool TenuredChunk::decommitOneFreeArena(GCRuntime* gc, AutoLockGC& lock) {
}
void TenuredChunk::decommitFreeArenasWithoutUnlocking(const AutoLockGC& lock) {
info.freeArenasHead = nullptr;
Arena** freeCursor = &info.freeArenasHead;
for (size_t i = 0; i < ArenasPerChunk; ++i) {
if (decommittedArenas[i] || arenas[i].allocated()) {
Arena* arena = &arenas[i];
if (decommittedArenas[i] || arena->allocated()) {
continue;
}
if (js::oom::ShouldFailWithOOM() ||
!MarkPagesUnusedSoft(arena, ArenaSize)) {
*freeCursor = arena;
freeCursor = &arena->next;
continue;
}
if (MarkPagesUnusedSoft(&arenas[i], ArenaSize)) {
info.numArenasFreeCommitted--;
decommittedArenas[i] = true;
}
}
*freeCursor = nullptr;
#ifdef DEBUG
verify();
#endif
}
void TenuredChunk::updateChunkListAfterAlloc(GCRuntime* gc,
@ -5592,6 +5650,10 @@ void GCRuntime::beginSweepPhase(JS::GCReason reason, AutoGCSession& session) {
releaseHeldRelocatedArenas();
#ifdef DEBUG
verifyAllChunks();
#endif
#ifdef JS_GC_ZEAL
computeNonIncrementalMarkingForValidation(session);
#endif

Просмотреть файл

@ -111,6 +111,7 @@ class ChunkPool {
public:
bool contains(TenuredChunk* chunk) const;
bool verify() const;
void verifyChunks() const;
#endif
public:
@ -518,6 +519,9 @@ class GCRuntime {
NonEmptyChunksIter allNonEmptyChunks(const AutoLockGC& lock) {
return NonEmptyChunksIter(availableChunks(lock), fullChunks(lock));
}
#ifdef DEBUG
void verifyAllChunks();
#endif
TenuredChunk* getOrAllocChunk(AutoLockGCBgAlloc& lock);
void recycleChunk(TenuredChunk* chunk, const AutoLockGC& lock);

Просмотреть файл

@ -263,6 +263,8 @@ class alignas(ArenaSize) Arena {
hasDelayedGrayMarking_ = 0;
nextDelayedMarkingArena_ = 0;
bufferedCells_ = nullptr;
MOZ_ASSERT(!allocated());
}
// Return an allocated arena to its unallocated state.
@ -653,6 +655,10 @@ class TenuredChunk : public TenuredChunkBase {
/* Unlink and return the freeArenasHead. */
Arena* fetchNextFreeArena(GCRuntime* gc);
#ifdef DEBUG
void verify() const;
#endif
private:
/* Search for a decommitted arena to allocate. */
unsigned findDecommittedArenaOffset();

Просмотреть файл

@ -0,0 +1,39 @@
// |jit-test| allow-oom; skip-if: !('oomAtAllocation' in this)
// Test TenuredChunk::decommitFreeArenasWithoutUnlocking updates chunk
// metadata correctly. The data is checked by assertions so this test is about
// exercising the code in question.
function allocateGarbage() {
gc();
for (let j = 0; j < 100000; j++) {
Symbol();
}
}
function collectUntilDecommit() {
startgc(1);
while (gcstate() != "NotActive" && gcstate() != "Decommit") {
gcslice(1000);
}
}
function triggerSyncDecommit() {
reportLargeAllocationFailure(1);
}
gczeal(0);
// Normally we skip decommit if GCs are happening frequently. Disable that for
// this test
gcparam("highFrequencyTimeLimit", 0);
allocateGarbage();
collectUntilDecommit();
triggerSyncDecommit();
allocateGarbage();
collectUntilDecommit();
oomAtAllocation(10);
triggerSyncDecommit();
resetOOMFailure();