Bug 1308039 - GC interrupt callbacks (r=jonco)
--- a/js/public/GCAPI.h
+++ b/js/public/GCAPI.h
@@ -699,11 +699,23 @@ extern JS_FRIEND_API(void)
PokeGC(JSContext* cx);
/*
* Internal to Firefox.
*/
extern JS_FRIEND_API(void)
NotifyDidPaint(JSContext* cx);
+// GC Interrupt callbacks are run during GC. You should not run JS code or use
+// the JS engine at all while the callback is running. Otherwise they resemble
+// normal JS interrupt callbacks.
+typedef bool
+(* GCInterruptCallback)(JSContext* cx);
+
+extern JS_FRIEND_API(bool)
+AddGCInterruptCallback(JSContext* cx, GCInterruptCallback callback);
+
+extern JS_FRIEND_API(void)
+RequestGCInterruptCallback(JSContext* cx);
+
} /* namespace JS */
#endif /* js_GCAPI_h */
--- a/js/public/SliceBudget.h
+++ b/js/public/SliceBudget.h
@@ -2,16 +2,18 @@
* vim: set ts=8 sts=4 et sw=4 tw=99:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef js_SliceBudget_h
#define js_SliceBudget_h
+#include "mozilla/Atomics.h"
+
#include <stdint.h>
namespace js {
struct JS_PUBLIC_API(TimeBudget)
{
int64_t budget;
@@ -31,57 +33,82 @@ struct JS_PUBLIC_API(WorkBudget)
* to run for unlimited time, and others are bounded. To reduce the number of
* gettimeofday calls, we only check the time every 1000 operations.
*/
class JS_PUBLIC_API(SliceBudget)
{
static const int64_t unlimitedDeadline = INT64_MAX;
static const intptr_t unlimitedStartCounter = INTPTR_MAX;
- bool checkOverBudget();
+ bool checkOverBudget(JSContext* maybeCx);
SliceBudget();
public:
// Memory of the originally requested budget. If isUnlimited, neither of
// these are in use. If deadline==0, then workBudget is valid. Otherwise
// timeBudget is valid.
TimeBudget timeBudget;
WorkBudget workBudget;
int64_t deadline; /* in microseconds */
- intptr_t counter;
+ mozilla::Atomic<intptr_t, mozilla::Relaxed> counter;
static const intptr_t CounterReset = 1000;
static const int64_t UnlimitedTimeBudget = -1;
static const int64_t UnlimitedWorkBudget = -1;
/* Use to create an unlimited budget. */
static SliceBudget unlimited() { return SliceBudget(); }
/* Instantiate as SliceBudget(TimeBudget(n)). */
explicit SliceBudget(TimeBudget time);
/* Instantiate as SliceBudget(WorkBudget(n)). */
explicit SliceBudget(WorkBudget work);
+ // Need an explicit copy constructor because Atomic fails to provide one.
+ SliceBudget(const SliceBudget& other)
+ : timeBudget(other.timeBudget),
+ workBudget(other.workBudget),
+ deadline(other.deadline),
+ counter(other.counter)
+ {}
+
+ // Need an explicit operator= because Atomic fails to provide one.
+ SliceBudget& operator=(const SliceBudget& other) {
+ timeBudget = other.timeBudget;
+ workBudget = other.workBudget;
+ deadline = other.deadline;
+ counter = intptr_t(other.counter);
+ return *this;
+ }
+
void makeUnlimited() {
deadline = unlimitedDeadline;
counter = unlimitedStartCounter;
}
+ // Request that checkOverBudget be called the next time isOverBudget is
+ // called.
+ void requestFullCheck() {
+ counter = 0;
+ }
+
void step(intptr_t amt = 1) {
counter -= amt;
}
- bool isOverBudget() {
+ // Only need to pass maybeCx if the GC interrupt callback should be checked
+ // (and possibly invoked).
+ bool isOverBudget(JSContext* maybeCx = nullptr) {
if (counter > 0)
return false;
- return checkOverBudget();
+ return checkOverBudget(maybeCx);
}
bool isWorkBudget() const { return deadline == 0; }
bool isTimeBudget() const { return deadline > 0 && !isUnlimited(); }
bool isUnlimited() const { return deadline == unlimitedDeadline; }
int describe(char* buffer, size_t maxlen) const;
};
--- a/js/src/gc/GCRuntime.h
+++ b/js/src/gc/GCRuntime.h
@@ -19,16 +19,17 @@
#include "gc/StoreBuffer.h"
#include "gc/Tracer.h"
#include "js/GCAnnotations.h"
namespace js {
class AutoLockGC;
class AutoLockHelperThreadState;
+class SliceBudget;
class VerifyPreTracer;
namespace gc {
typedef Vector<JS::Zone*, 4, SystemAllocPolicy> ZoneVector;
using BlackGrayEdgeVector = Vector<TenuredCell*, 0, SystemAllocPolicy>;
class AutoMaybeStartBackgroundAllocation;
@@ -858,16 +859,29 @@ class GCRuntime
void startVerifyPreBarriers();
void endVerifyPreBarriers();
void finishVerifier();
bool isVerifyPreBarriersEnabled() const { return !!verifyPreData; }
#else
bool isVerifyPreBarriersEnabled() const { return false; }
#endif
+ // GC interrupt callbacks.
+ bool addInterruptCallback(JS::GCInterruptCallback callback);
+ void requestInterruptCallback();
+
+ bool checkInterruptCallback(JSContext* cx) {
+ if (interruptCallbackRequested) {
+ invokeInterruptCallback(cx);
+ return true;
+ }
+ return false;
+ }
+ void invokeInterruptCallback(JSContext* cx);
+
// Free certain LifoAlloc blocks when it is safe to do so.
void freeUnusedLifoBlocksAfterSweeping(LifoAlloc* lifo);
void freeAllLifoBlocksAfterSweeping(LifoAlloc* lifo);
void freeAllLifoBlocksAfterMinorGC(LifoAlloc* lifo);
// Queue a thunk to run after the next minor GC.
void callAfterMinorGC(void (*thunk)(void* data), void* data) {
nursery.queueSweepAction(thunk, data);
@@ -1070,16 +1084,23 @@ class GCRuntime
mozilla::Atomic<uint64_t, mozilla::ReleaseAcquire> nextCellUniqueId_;
/*
* Number of the committed arenas in all GC chunks including empty chunks.
*/
mozilla::Atomic<uint32_t, mozilla::ReleaseAcquire> numArenasFreeCommitted;
VerifyPreTracer* verifyPreData;
+ // GC interrupt callbacks.
+ using GCInterruptCallbackVector = js::Vector<JS::GCInterruptCallback, 2, js::SystemAllocPolicy>;
+ GCInterruptCallbackVector interruptCallbacks;
+
+ mozilla::Atomic<bool, mozilla::Relaxed> interruptCallbackRequested;
+ SliceBudget* currentBudget;
+
private:
bool chunkAllocationSinceLastGC;
int64_t lastGCTime;
JSGCMode mode;
mozilla::Atomic<size_t, mozilla::ReleaseAcquire> numActiveZoneIters;
--- a/js/src/gc/Marking.cpp
+++ b/js/src/gc/Marking.cpp
@@ -1540,23 +1540,25 @@ bool
GCMarker::drainMarkStack(SliceBudget& budget)
{
#ifdef DEBUG
MOZ_ASSERT(!strictCompartmentChecking);
strictCompartmentChecking = true;
auto acc = mozilla::MakeScopeExit([&] {strictCompartmentChecking = false;});
#endif
- if (budget.isOverBudget())
+ JSContext* cx = runtime()->contextFromMainThread();
+
+ if (budget.isOverBudget(cx))
return false;
for (;;) {
while (!stack.isEmpty()) {
processMarkStackTop(budget);
- if (budget.isOverBudget()) {
+ if (budget.isOverBudget(cx)) {
saveValueRanges();
return false;
}
}
if (!hasDelayedChildren())
break;
@@ -1621,16 +1623,18 @@ GCMarker::processMarkStackTop(SliceBudge
HeapSlot* end;
JSObject* obj;
// Decode
uintptr_t addr = stack.pop();
uintptr_t tag = addr & StackTagMask;
addr &= ~StackTagMask;
+ JSContext* cx = runtime()->contextFromMainThread();
+
// Dispatch
switch (tag) {
case ValueArrayTag: {
JS_STATIC_ASSERT(ValueArrayTag == 0);
MOZ_ASSERT(!(addr & CellMask));
obj = reinterpret_cast<JSObject*>(addr);
uintptr_t addr2 = stack.pop();
uintptr_t addr3 = stack.pop();
@@ -1674,17 +1678,17 @@ GCMarker::processMarkStackTop(SliceBudge
default: MOZ_CRASH("Invalid tag in mark stack");
}
return;
scan_value_array:
MOZ_ASSERT(vp <= end);
while (vp != end) {
budget.step();
- if (budget.isOverBudget()) {
+ if (budget.isOverBudget(cx)) {
pushValueArray(obj, vp, end);
return;
}
const Value& v = *vp++;
if (v.isString()) {
traverseEdge(obj, v.toString());
} else if (v.isObject()) {
@@ -1704,17 +1708,17 @@ GCMarker::processMarkStackTop(SliceBudge
}
return;
scan_obj:
{
AssertZoneIsMarking(obj);
budget.step();
- if (budget.isOverBudget()) {
+ if (budget.isOverBudget(cx)) {
repush(obj);
return;
}
markImplicitEdges(obj);
ObjectGroup* group = obj->groupFromGC();
traverseEdge(obj, group);
@@ -2152,17 +2156,17 @@ GCMarker::markDelayedChildren(SliceBudge
unmarkedArenaStackTop = arena->getNextDelayedMarking();
arena->unsetDelayedMarking();
#ifdef DEBUG
markLaterArenas--;
#endif
markDelayedChildren(arena);
budget.step(150);
- if (budget.isOverBudget())
+ if (budget.isOverBudget(runtime()->contextFromMainThread()))
return false;
} while (unmarkedArenaStackTop);
MOZ_ASSERT(!markLaterArenas);
return true;
}
template<typename T>
--- a/js/src/jsgc.cpp
+++ b/js/src/jsgc.cpp
@@ -183,16 +183,17 @@
#include "jsgcinlines.h"
#include "mozilla/ArrayUtils.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/MacroForEach.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/Move.h"
+#include "mozilla/ScopeExit.h"
#include <ctype.h>
#include <string.h>
#ifndef XP_WIN
# include <sys/mman.h>
# include <unistd.h>
#endif
@@ -501,30 +502,32 @@ FinalizeTypedArenas(FreeOp* fop,
// During background sweeping free arenas are released later on in
// sweepBackgroundThings().
MOZ_ASSERT_IF(!fop->onMainThread(), keepArenas == ArenaLists::KEEP_ARENAS);
size_t thingSize = Arena::thingSize(thingKind);
size_t thingsPerArena = Arena::thingsPerArena(thingKind);
+ JSContext* cx = fop->onMainThread() ? fop->runtime()->contextFromMainThread() : nullptr;
+
while (Arena* arena = *src) {
*src = arena->next;
size_t nmarked = arena->finalize<T>(fop, thingKind, thingSize);
size_t nfree = thingsPerArena - nmarked;
if (nmarked)
dest.insertAt(arena, nfree);
else if (keepArenas == ArenaLists::KEEP_ARENAS)
arena->chunk()->recycleArena(arena, dest, thingsPerArena);
else
fop->runtime()->gc.releaseArena(arena, maybeLock.ref());
budget.step(thingsPerArena);
- if (budget.isOverBudget())
+ if (budget.isOverBudget(cx))
return false;
}
return true;
}
/*
* Finalize the list. On return, |al|'s cursor points to the first non-empty
@@ -803,16 +806,18 @@ GCRuntime::GCRuntime(JSRuntime* rt) :
stats(rt),
marker(rt),
usage(nullptr),
mMemProfiler(rt),
maxMallocBytes(0),
nextCellUniqueId_(LargestTaggedNullCellPointer + 1), // Ensure disjoint from null tagged pointers.
numArenasFreeCommitted(0),
verifyPreData(nullptr),
+ interruptCallbackRequested(false),
+ currentBudget(nullptr),
chunkAllocationSinceLastGC(false),
lastGCTime(PRMJ_Now()),
mode(JSGC_MODE_INCREMENTAL),
numActiveZoneIters(0),
cleanUpEverything(false),
grayBufferState(GCRuntime::GrayBufferState::Unused),
grayBitsValid(false),
majorGCTriggerReason(JS::gcreason::NO_REASON),
@@ -2928,18 +2933,21 @@ SliceBudget::describe(char* buffer, size
return snprintf(buffer, maxlen, "unlimited");
else if (isWorkBudget())
return snprintf(buffer, maxlen, "work(%" PRId64 ")", workBudget.budget);
else
return snprintf(buffer, maxlen, "%" PRId64 "ms", timeBudget.budget);
}
bool
-SliceBudget::checkOverBudget()
-{
+SliceBudget::checkOverBudget(JSContext* cx)
+{
+ if (cx)
+ cx->gc.checkInterruptCallback(cx);
+
bool over = PRMJ_Now() >= deadline;
if (!over)
counter = CounterReset;
return over;
}
void
js::MarkCompartmentActive(InterpreterFrame* fp)
@@ -5476,17 +5484,17 @@ GCRuntime::compactPhase(JS::gcreason::Re
Zone* zone = zonesToMaybeCompact.front();
MOZ_ASSERT(zone->isGCFinished());
zone->setGCState(Zone::Compact);
if (relocateArenas(zone, reason, relocatedArenas, sliceBudget))
updatePointersToRelocatedCells(zone, lock);
zone->setGCState(Zone::Finished);
zonesToMaybeCompact.removeFront();
- if (sliceBudget.isOverBudget())
+ if (sliceBudget.isOverBudget(rt->contextFromMainThread()))
break;
}
if (ShouldProtectRelocatedArenas(reason))
protectAndHoldArenas(relocatedArenas);
else
releaseRelocatedArenas(relocatedArenas);
@@ -5882,17 +5890,17 @@ GCRuntime::incrementalCollectSlice(Slice
incrementalState = State::Sweep;
/*
* This runs to completion, but we don't continue if the budget is
* now exhasted.
*/
beginSweepPhase(destroyingRuntime, lock);
- if (budget.isOverBudget())
+ if (budget.isOverBudget(rt->contextFromMainThread()))
break;
/*
* Always yield here when running in incremental multi-slice zeal
* mode, so RunDebugGC can reset the slice buget.
*/
if (isIncremental && useZeal && hasZealMode(ZealMode::IncrementalMultipleSlices))
break;
@@ -6289,16 +6297,21 @@ GCRuntime::collect(bool nonincrementalBy
{
// Checks run for each request, even if we do not actually GC.
checkCanCallAPI();
// Check if we are allowed to GC at this time before proceeding.
if (!checkIfGCAllowedInCurrentState(reason))
return;
+ currentBudget = &budget;
+ auto guard = mozilla::MakeScopeExit([&] {
+ currentBudget = nullptr;
+ });
+
AutoTraceLog logGC(TraceLoggerForMainThread(rt), TraceLogger_GC);
AutoStopVerifyingBarriers av(rt, IsShutdownGC(reason));
AutoEnqueuePendingParseTasksAfterGC aept(*this);
AutoScheduleZonesForGC asz(rt);
bool repeat = false;
do {
poked = false;
@@ -7663,8 +7676,45 @@ js::gc::Cell::dump(FILE* fp) const
// For use in a debugger.
void
js::gc::Cell::dump() const
{
dump(stderr);
}
#endif
+
+bool
+JS::AddGCInterruptCallback(JSContext* cx, GCInterruptCallback callback)
+{
+ return cx->runtime()->gc.addInterruptCallback(callback);
+}
+
+void
+JS::RequestGCInterruptCallback(JSContext* cx)
+{
+ cx->runtime()->gc.requestInterruptCallback();
+}
+
+bool
+GCRuntime::addInterruptCallback(JS::GCInterruptCallback callback)
+{
+ return interruptCallbacks.append(callback);
+}
+
+void
+GCRuntime::requestInterruptCallback()
+{
+ if (currentBudget) {
+ interruptCallbackRequested = true;
+ currentBudget->requestFullCheck();
+ }
+}
+
+void
+GCRuntime::invokeInterruptCallback(JSContext* cx)
+{
+ JS::AutoAssertOnGC nogc(cx);
+ JS::AutoAssertOnBarrier nobarrier(cx);
+ for (JS::GCInterruptCallback callback : interruptCallbacks) {
+ (*callback)(cx);
+ }
+}