Bug 1672121 - Implement interruptible GC slice budgets r=jonco

Differential Revision: https://phabricator.services.mozilla.com/D109630
This commit is contained in:
Steve Fink 2021-11-29 19:59:52 +00:00
Родитель 7eceb02f3d
Коммит 0181c4d9ce
7 изменённых файлов: 146 добавлений и 23 удалений

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

@ -594,8 +594,7 @@ js::SliceBudget CCGCScheduler::ComputeCCSliceBudget(
if (aCCBeginTime.IsNull()) {
// If no CC is in progress, use the standard slice time.
return js::SliceBudget(js::TimeBudget(baseBudget),
kNumCCNodesBetweenTimeChecks);
return js::SliceBudget(js::TimeBudget(baseBudget));
}
// Only run a limited slice if we're within the max running time.
@ -624,9 +623,8 @@ js::SliceBudget CCGCScheduler::ComputeCCSliceBudget(
// Note: We may have already overshot the deadline, in which case
// baseBudget will be negative and we will end up returning
// laterSliceBudget.
return js::SliceBudget(js::TimeBudget(std::max(
{delaySliceBudget, laterSliceBudget, baseBudget})),
kNumCCNodesBetweenTimeChecks);
return js::SliceBudget(js::TimeBudget(
std::max({delaySliceBudget, laterSliceBudget, baseBudget})));
}
TimeDuration CCGCScheduler::ComputeInterSliceGCBudget(TimeStamp aDeadline,

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

@ -61,9 +61,6 @@ static const TimeDuration kMaxCCLockedoutTime = TimeDuration::FromSeconds(30);
// Trigger a CC if the purple buffer exceeds this size when we check it.
static const uint32_t kCCPurpleLimit = 200;
// How many cycle collected nodes to traverse between time checks.
static const int64_t kNumCCNodesBetweenTimeChecks = 1000;
// Actions performed by the GCRunner state machine.
enum class GCRunnerAction {
WaitToMajorGC, // We want to start a new major GC

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

@ -16,6 +16,7 @@
#include "js/GCAnnotations.h"
#include "js/shadow/Zone.h"
#include "js/SliceBudget.h"
#include "js/TypeDecls.h"
#include "js/UniquePtr.h"
#include "js/Utility.h"
@ -888,6 +889,20 @@ typedef void (*DoCycleCollectionCallback)(JSContext* cx);
extern JS_PUBLIC_API DoCycleCollectionCallback
SetDoCycleCollectionCallback(JSContext* cx, DoCycleCollectionCallback callback);
using CreateSliceBudgetCallback = js::SliceBudget (*)(JS::GCReason reason,
int64_t millis);
/**
* Called when generating a GC slice budget. It allows the embedding to control
* the duration of slices and potentially check an interrupt flag as well. For
* internally triggered GCs, the given millis parameter is the JS engine's
* internal scheduling decision, which the embedding can choose to ignore.
* (Otherwise, it will be the value that was passed to eg
* JS::IncrementalGCSlice()).
*/
extern JS_PUBLIC_API void SetCreateGCSliceBudgetCallback(
JSContext* cx, CreateSliceBudgetCallback cb);
/**
* Incremental GC defaults to enabled, but may be disabled for testing or in
* embeddings that have not yet implemented barriers on their native classes.

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

@ -8,6 +8,7 @@
#define js_SliceBudget_h
#include "mozilla/Assertions.h"
#include "mozilla/Atomics.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/Variant.h"
@ -44,32 +45,56 @@ struct UnlimitedBudget {};
* operations.
*/
class JS_PUBLIC_API SliceBudget {
public:
using InterruptRequestFlag = mozilla::Atomic<bool>;
private:
static const intptr_t UnlimitedCounter = INTPTR_MAX;
static const intptr_t DefaultStepsPerTimeCheck = 1000;
// Most calls to isOverBudget will only check the counter value. Every N
// steps, do a more "expensive" check -- look at the current time and/or
// check the atomic interrupt flag.
static constexpr intptr_t StepsPerExpensiveCheck = 1000;
// Configuration
mozilla::Variant<TimeBudget, WorkBudget, UnlimitedBudget> budget;
int64_t stepsPerTimeCheck = DefaultStepsPerTimeCheck;
int64_t counter;
// External flag to request the current slice to be interrupted
// (and return isOverBudget() early.) Applies only to time-based budgets.
InterruptRequestFlag* interruptRequested = nullptr;
SliceBudget() : budget(UnlimitedBudget()), counter(UnlimitedCounter) {}
// How many steps to count before checking the time and possibly the interrupt
// flag.
int64_t counter = StepsPerExpensiveCheck;
// This SliceBudget is considered interrupted from the time isOverBudget()
// finds the interrupt flag set, to the next time resetOverBudget() (or
// checkAndResetOverBudget()) is called.
bool interrupted = false;
explicit SliceBudget(InterruptRequestFlag* irqPtr)
: budget(UnlimitedBudget()),
interruptRequested(irqPtr),
counter(irqPtr ? StepsPerExpensiveCheck : UnlimitedCounter) {}
[[nodiscard]] bool isOverBudgetSlow();
public:
// Use to create an unlimited budget.
static SliceBudget unlimited() { return SliceBudget(); }
static SliceBudget unlimited() { return SliceBudget(nullptr); }
// Instantiate as SliceBudget(TimeBudget(n)).
explicit SliceBudget(TimeBudget time,
int64_t stepsPerTimeCheck = DefaultStepsPerTimeCheck);
InterruptRequestFlag* interrupt = nullptr);
explicit SliceBudget(mozilla::TimeDuration duration,
InterruptRequestFlag* interrupt = nullptr)
: SliceBudget(TimeBudget(duration.ToMilliseconds()), interrupt) {}
// Instantiate as SliceBudget(WorkBudget(n)).
explicit SliceBudget(WorkBudget work);
explicit SliceBudget(mozilla::TimeDuration time)
: SliceBudget(TimeBudget(time.ToMilliseconds())) {}
// Register having performed the given number of steps (counted against a
// work budget, or progress towards the next time or callback check).
void step(uint64_t steps = 1) {
@ -91,8 +116,9 @@ class JS_PUBLIC_API SliceBudget {
}
void resetOverBudget() {
interrupted = false;
if (isTimeBudget()) {
counter = stepsPerTimeCheck;
counter = StepsPerExpensiveCheck;
} else if (isWorkBudget()) {
counter = workBudget();
}

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

@ -375,6 +375,7 @@ GCRuntime::GCRuntime(JSRuntime* rt)
helperThreadRatio(TuningDefaults::HelperThreadRatio),
maxHelperThreads(TuningDefaults::MaxHelperThreads),
helperThreadCount(1),
createBudgetCallback(nullptr),
rootsHash(256),
nextCellUniqueId_(LargestTaggedNullCellPointer +
1), // Ensure disjoint from null tagged pointers.
@ -1402,16 +1403,21 @@ bool GCRuntime::isCompactingGCEnabled() const {
rt->mainContextFromOwnThread()->compactingDisabledCount == 0;
}
SliceBudget::SliceBudget(TimeBudget time, int64_t stepsPerTimeCheckArg)
JS_PUBLIC_API void JS::SetCreateGCSliceBudgetCallback(
JSContext* cx, JS::CreateSliceBudgetCallback cb) {
cx->runtime()->gc.createBudgetCallback = cb;
}
SliceBudget::SliceBudget(TimeBudget time, InterruptRequestFlag* interrupt)
: budget(TimeBudget(time)),
stepsPerTimeCheck(stepsPerTimeCheckArg),
counter(stepsPerTimeCheckArg) {
interruptRequested(interrupt),
counter(StepsPerExpensiveCheck) {
budget.as<TimeBudget>().deadline =
ReallyNow() + TimeDuration::FromMilliseconds(timeBudget());
}
SliceBudget::SliceBudget(WorkBudget work)
: budget(work), counter(work.budget) {}
: budget(work), interruptRequested(nullptr), counter(work.budget) {}
int SliceBudget::describe(char* buffer, size_t maxlen) const {
if (isUnlimited()) {
@ -1419,7 +1425,8 @@ int SliceBudget::describe(char* buffer, size_t maxlen) const {
} else if (isWorkBudget()) {
return snprintf(buffer, maxlen, "work(%" PRId64 ")", workBudget());
} else {
return snprintf(buffer, maxlen, "%" PRId64 "ms", timeBudget());
return snprintf(buffer, maxlen, "%" PRId64 "ms%s", timeBudget(),
interruptRequested ? ", interruptible" : "");
}
}
@ -1431,6 +1438,15 @@ bool SliceBudget::isOverBudgetSlow() {
return true;
}
if (interruptRequested && *interruptRequested) {
*interruptRequested = false;
interrupted = true;
}
if (interrupted) {
return true;
}
if (ReallyNow() >= budget.as<TimeBudget>().deadline) {
return true;
}
@ -3847,6 +3863,9 @@ void GCRuntime::collect(bool nonincrementalByAPI, const SliceBudget& budget,
}
SliceBudget GCRuntime::defaultBudget(JS::GCReason reason, int64_t millis) {
// millis == 0 means use internal GC scheduling logic to come up with
// a duration for the slice budget. This may end up still being zero
// based on preferences.
if (millis == 0) {
if (reason == JS::GCReason::ALLOC_TRIGGER) {
millis = defaultSliceBudgetMS();
@ -3857,6 +3876,13 @@ SliceBudget GCRuntime::defaultBudget(JS::GCReason reason, int64_t millis) {
}
}
// If the embedding has registered a callback for creating SliceBudgets,
// then use it.
if (createBudgetCallback) {
return createBudgetCallback(reason, millis);
}
// Otherwise, the preference can request an unlimited duration slice.
if (millis == 0) {
return SliceBudget::unlimited();
}

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

@ -921,6 +921,12 @@ class GCRuntime {
// State used for managing atom mark bitmaps in each zone.
AtomMarkingRuntime atomMarking;
/*
* Pointer to a callback that, if set, will be used to create a
* budget for internally-triggered GCs.
*/
MainThreadData<JS::CreateSliceBudgetCallback> createBudgetCallback;
private:
// Arenas used for permanent things created at startup and shared by child
// runtimes.

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

@ -82,3 +82,58 @@ BEGIN_TEST(testSliceBudgetTimeZero) {
return true;
}
END_TEST(testSliceBudgetTimeZero)
BEGIN_TEST(testSliceBudgetInterruptibleTime) {
mozilla::Atomic<bool> wantInterrupt(false);
// Interruptible 100 second budget. This test will finish in well under that
// time.
static constexpr int64_t LONG_TIME = 100000;
SliceBudget budget = SliceBudget(TimeBudget(LONG_TIME), &wantInterrupt);
CHECK(!budget.isUnlimited());
CHECK(!budget.isWorkBudget());
CHECK(budget.isTimeBudget());
CHECK(budget.timeBudget() == LONG_TIME);
CHECK(!budget.isOverBudget());
// We do a little work, very small amount of time passes.
budget.step(500);
// Not enough work to check interrupt, and no interrupt anyway.
CHECK(!budget.isOverBudget());
// External signal: interrupt requested.
wantInterrupt = true;
// Interrupt requested, but not enough work has been done to check for it.
CHECK(!budget.isOverBudget());
// Do enough work for an expensive check.
budget.step(1000);
// Interrupt requested! This will reset the external flag, but internally
// remember that an interrupt was requested.
CHECK(budget.isOverBudget());
CHECK(!wantInterrupt);
CHECK(budget.isOverBudget());
// Caller would handle the interrupt here.
budget.resetOverBudget();
CHECK(!budget.isOverBudget());
budget.step(5);
CHECK(!budget.isOverBudget());
// The external signal gets picked up only when isOverBudget() is called.
wantInterrupt = true;
budget.step(2000);
wantInterrupt = false;
CHECK(!budget.isOverBudget());
// This doesn't test the deadline is correct as that would require waiting.
return true;
}
END_TEST(testSliceBudgetInterruptibleTime)