Bug 1587096 - Part 4: Support FinlalizationGroups in the shell and add tests r=sfink

Differential Revision: https://phabricator.services.mozilla.com/D49947

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Jon Coppeard 2019-11-01 10:38:08 +00:00
Родитель f3b48418af
Коммит 08d1fba1d5
3 изменённых файлов: 311 добавлений и 13 удалений

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

@ -22,6 +22,11 @@ function assertThrowsTypeError(thunk) {
// 3.1 The FinalizationGroup Constructor
assertEq(typeof this.FinalizationGroup, "function");
// 3.1.1 FinalizationGroup ( cleanupCallback )
assertThrowsTypeError(() => new FinalizationGroup());
assertThrowsTypeError(() => new FinalizationGroup(1));
new FinalizationGroup(x => 0);
// 3.2 Properties of the FinalizationGroup Constructor
assertEq(Object.getPrototypeOf(FinalizationGroup), Function.prototype);
@ -35,10 +40,249 @@ assertEq(Object.getPrototypeOf(proto), Object.prototype);
// 3.3.1 FinalizationGroup.prototype.constructor
assertEq(proto.constructor, FinalizationGroup);
//3.3.5 FinalizationGroup.prototype [ @@toStringTag ]
// 3.3.2 FinalizationGroup.prototype.register ( target , holdings [, unregisterToken ] )
assertEq(proto.hasOwnProperty('register'), true);
assertEq(typeof proto.register, 'function');
// 3.3.3 FinalizationGroup.prototype.unregister ( unregisterToken )
assertEq(proto.hasOwnProperty('unregister'), true);
assertEq(typeof proto.unregister, 'function');
// 3.3.4 FinalizationGroup.prototype.cleanupSome ( [ callback ] )
assertEq(proto.hasOwnProperty('cleanupSome'), true);
assertEq(typeof proto.cleanupSome, 'function');
// 3.3.5 FinalizationGroup.prototype [ @@toStringTag ]
assertEq(proto[Symbol.toStringTag], "FinalizationGroup");
checkPropertyDescriptor(proto, Symbol.toStringTag, false, false, true);
assertThrowsTypeError(() => new FinalizationGroup());
assertThrowsTypeError(() => new FinalizationGroup(1));
let group = new FinalizationGroup(x => 1);
// 3.4 Properties of FinalizationGroup Instances
let group = new FinalizationGroup(x => 0);
assertEq(Object.getPrototypeOf(group), proto);
assertEq(Object.getOwnPropertyNames(group).length, 0);
// Get a cleanup iterator.
let iterator;
new FinalizationGroup(it => iterator = it).register({}, 0);
gc();
drainJobQueue();
assertEq(typeof iterator, 'object');
// 3.5.2 The %FinalizationGroupCleanupIteratorPrototype% Object
let arrayIterator = [][Symbol.iterator]();
let iteratorProto = arrayIterator.__proto__.__proto__;
proto = iterator.__proto__;
assertEq(typeof proto, "object");
assertEq(proto.__proto__, iteratorProto);
// 3.5.2.1 %FinalizationGroupCleanupIteratorPrototype%.next()
assertEq(proto.hasOwnProperty("next"), true);
assertEq(typeof proto.next, "function");
// 3.5.2.2 %FinalizationGroupCleanupIteratorPrototype% [ @@toStringTag ]
assertEq(proto[Symbol.toStringTag], "FinalizationGroup Cleanup Iterator");
checkPropertyDescriptor(proto, Symbol.toStringTag, false, false, true);
// 3.5.3 Properties of FinalizationGroup Cleanup Iterator Instances
assertEq(Object.getOwnPropertyNames(iterator).length, 0);
let holdings = [];
group = new FinalizationGroup(iterator => {
for (const holding of iterator) {
holdings.push(holding);
}
});
// Test a single target.
holdings = [];
group.register({}, 42);
gc();
drainJobQueue();
assertEq(holdings.length, 1);
assertEq(holdings[0], 42);
// Test multiple targets.
holdings = [];
for (let i = 0; i < 100; i++) {
group.register({}, i);
}
gc();
drainJobQueue();
assertEq(holdings.length, 100);
holdings = holdings.sort((a, b) => a - b);
for (let i = 0; i < 100; i++) {
assertEq(holdings[i], i);
}
// Test a single object in multiple groups
holdings = [];
let holdings2 = [];
let group2 = new FinalizationGroup(iterator => {
for (const holding of iterator) {
holdings2.push(holding);
}
});
{
let object = {};
group.register(object, 1);
group2.register(object, 2);
object = null;
}
gc();
drainJobQueue();
assertEq(holdings.length, 1);
assertEq(holdings[0], 1);
assertEq(holdings2.length, 1);
assertEq(holdings2[0], 2);
// Unregister a single target.
holdings = [];
let token = {};
group.register({}, 1, token);
group.unregister(token);
gc();
drainJobQueue();
assertEq(holdings.length, 0);
// Unregister multiple targets.
holdings = [];
let token2 = {};
group.register({}, 1, token);
group.register({}, 2, token2);
group.register({}, 3, token);
group.register({}, 4, token2);
group.unregister(token);
gc();
drainJobQueue();
assertEq(holdings.length, 2);
holdings = holdings.sort((a, b) => a - b);
assertEq(holdings[0], 2);
assertEq(holdings[1], 4);
// OOM test.
if ('oomTest' in this) {
oomTest(() => new FinalizationGroup(x => 0));
let token = {};
oomTest(() => group.register({}, 1, token));
oomTest(() => group.unregister(token));
}
// Watch object in another global.
let other = newGlobal({newCompartment: true});
holdings = [];
group.register(evalcx('({})', other), 1);
gc();
drainJobQueue();
assertEq(holdings.length, 1);
assertEq(holdings[0], 1);
// Pass holdings from another global.
let holding = evalcx('{}', other);
holdings = [];
group.register({}, holding);
gc();
drainJobQueue();
assertEq(holdings.length, 1);
assertEq(holdings[0], holding);
// Pass unregister token from another global.
token = evalcx('({})', other);
holdings = [];
group.register({}, 1, token);
gc();
drainJobQueue();
assertEq(holdings.length, 1);
assertEq(holdings[0], 1);
holdings = [];
group.register({}, 1, token);
group.unregister(token);
gc();
drainJobQueue();
assertEq(holdings.length, 0);
// FinalizationGroup is designed to be subclassable.
class MyGroup extends FinalizationGroup {
constructor(callback) {
super(callback);
}
}
let g2 = new MyGroup(iterator => {
for (const holding of iterator) {
holdings.push(holding);
}
});
holdings = [];
g2.register({}, 42);
gc();
drainJobQueue();
assertEq(holdings.length, 1);
assertEq(holdings[0], 42);
// Test trying to use iterator after the callback.
iterator = undefined;
let g3 = new FinalizationGroup(i => iterator = i);
g3.register({}, 1);
gc();
drainJobQueue();
assertEq(typeof iterator, 'object');
assertThrowsTypeError(() => iterator.next());
// Test trying to use the wrong iterator inside the callback.
let g4 = new FinalizationGroup(x => {
assertThrowsTypeError(() => iterator.next());
});
g4.register({}, 1);
gc();
drainJobQueue();
// Test cleanupSome.
holdings = [];
let g5 = new FinalizationGroup(i => holdings = [...i]);
g5.register({}, 1);
g5.register({}, 2);
g5.register({}, 3);
gc();
g5.cleanupSome();
assertEq(holdings.length, 3);
holdings = holdings.sort((a, b) => a - b);
assertEq(holdings[0], 1);
assertEq(holdings[1], 2);
assertEq(holdings[2], 3);
// Test trying to call cleanupSome in callback.
let g6 = new FinalizationGroup(x => {
assertThrowsTypeError(() => g6.cleanupSome());
});
g6.register({}, 1);
gc();
drainJobQueue();
// Test combinations of arguments in different compartments.
function ccw() {
return evalcx('({})', newGlobal({newCompartment: true}));
}
function incrementalGC() {
startgc(1);
while (gcstate() !== "NotActive") {
gcslice(1000);
}
}
for (let x of [false, true]) {
for (let y of [false, true]) {
for (let z of [false, true]) {
let target = x ? ccw() : {};
let holding = x ? ccw() : {};
let token = x ? ccw() : {};
group.register(target, holding, token);
group.unregister(token);
group.register(target, holding, token);
target = undefined;
incrementalGC();
holdings = [];
group.cleanupSome();
assertEq(holdings.length, 1);
assertEq(holdings[0], holding);
}
}
}

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

@ -638,7 +638,8 @@ ShellContext::ShellContext(JSContext* cx)
readLineBufPos(0),
errFilePtr(nullptr),
outFilePtr(nullptr),
offThreadMonitor(mutexid::ShellOffThreadState) {}
offThreadMonitor(mutexid::ShellOffThreadState),
finalizationGroupsToCleanUp(cx) {}
ShellContext::~ShellContext() { MOZ_ASSERT(offThreadJobs.empty()); }
@ -1015,6 +1016,38 @@ static MOZ_MUST_USE bool RunModule(JSContext* cx, const char* filename,
return JS_CallFunction(cx, nullptr, importFun, args, &value);
}
static void ShellCleanupFinalizationGroupCallback(JSObject* group, void* data) {
// In the browser this queues a task. Shell jobs correspond to microtasks so
// we arrange for cleanup to happen after all jobs/microtasks have run.
auto sc = static_cast<ShellContext*>(data);
AutoEnterOOMUnsafeRegion oomUnsafe;
if (!sc->finalizationGroupsToCleanUp.append(group)) {
oomUnsafe.crash("ShellCleanupFinalizationGroupCallback");
}
}
static void MaybeRunFinalizationGroupCleanupTasks(JSContext* cx) {
ShellContext* sc = GetShellContext(cx);
MOZ_ASSERT(!sc->quitting);
Rooted<ShellContext::ObjectVector> groups(cx);
std::swap(groups.get(), sc->finalizationGroupsToCleanUp.get());
RootedObject group(cx);
for (const auto& g : groups) {
group = g;
{
AutoReportException are(cx);
mozilla::Unused << JS::CleanupQueuedFinalizationGroup(cx, group);
}
if (sc->quitting) {
break;
}
}
}
static bool EnqueueJob(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
@ -1029,6 +1062,22 @@ static bool EnqueueJob(JSContext* cx, unsigned argc, Value* vp) {
return js::EnqueueJob(cx, job);
}
static void RunShellJobs(JSContext* cx) {
ShellContext* sc = GetShellContext(cx);
if (sc->quitting) {
return;
}
// Run microtasks.
js::RunJobs(cx);
if (sc->quitting) {
return;
}
// Run tasks (only finalization group clean tasks are possible).
MaybeRunFinalizationGroupCleanupTasks(cx);
}
static bool DrainJobQueue(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
@ -1038,7 +1087,7 @@ static bool DrainJobQueue(JSContext* cx, unsigned argc, Value* vp) {
return false;
}
js::RunJobs(cx);
RunShellJobs(cx);
if (GetShellContext(cx)->quitting) {
return false;
@ -1403,10 +1452,7 @@ static MOZ_MUST_USE bool ReadEvalPrintLoop(JSContext* cx, FILE* in,
stderr);
}
if (!GetShellContext(cx)->quitting) {
js::RunJobs(cx);
}
RunShellJobs(cx);
} while (!hitEOF && !sc->quitting);
if (gOutFile->isOpen()) {
@ -4054,6 +4100,9 @@ static void WorkerMain(WorkerInput* input) {
js::UseInternalJobQueues(cx);
JS::SetHostCleanupFinalizationGroupCallback(
cx, ShellCleanupFinalizationGroupCallback, sc);
if (!JS::InitSelfHostedCode(cx)) {
return;
}
@ -10838,9 +10887,7 @@ static int Shell(JSContext* cx, OptionParser* op, char** envp) {
* tasks before the main thread JSRuntime is torn down. Drain after
* uncaught exceptions have been reported since draining runs callbacks.
*/
if (!GetShellContext(cx)->quitting) {
js::RunJobs(cx);
}
RunShellJobs(cx);
// Only if there's no other error, report unhandled rejections.
if (!result && !sc->exitCode) {
@ -11428,6 +11475,9 @@ int main(int argc, char** argv, char** envp) {
js::UseInternalJobQueues(cx);
JS::SetHostCleanupFinalizationGroupCallback(
cx, ShellCleanupFinalizationGroupCallback, sc.get());
auto shutdownShellThreads = MakeScopeExit([cx] {
KillWatchdog(cx);
KillWorkerThreads(cx);

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

@ -182,6 +182,10 @@ struct ShellContext {
// Off-thread parse state.
js::Monitor offThreadMonitor;
Vector<OffThreadJob*, 0, SystemAllocPolicy> offThreadJobs;
// Queued finalization group cleanup jobs.
using ObjectVector = GCVector<JSObject*, 0, SystemAllocPolicy>;
JS::PersistentRooted<ObjectVector> finalizationGroupsToCleanUp;
};
extern ShellContext* GetShellContext(JSContext* cx);