a5c321024c
Tracking the context is not enough, as we might clear the context to avoid repeatedly reentering the same context. Also fix unittests that relied on the default microtask queue getting automatically processed instead of the one of the current context. Bug: chromium:728583 Change-Id: Ia9a51c513fc7363a518af86cc54c5bda26b5fbe8 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2859850 Reviewed-by: Toon Verwaest <verwaest@chromium.org> Commit-Queue: Jochen Eisinger <jochen@chromium.org> Cr-Commit-Position: refs/heads/master@{#74303}
657 lines
23 KiB
C++
657 lines
23 KiB
C++
// Copyright 2018 the V8 project authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "src/execution/microtask-queue.h"
|
|
|
|
#include <algorithm>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
#include "src/heap/factory.h"
|
|
#include "src/objects/foreign.h"
|
|
#include "src/objects/js-array-inl.h"
|
|
#include "src/objects/js-objects-inl.h"
|
|
#include "src/objects/objects-inl.h"
|
|
#include "src/objects/promise-inl.h"
|
|
#include "src/objects/visitors.h"
|
|
#include "test/unittests/test-utils.h"
|
|
#include "testing/gtest/include/gtest/gtest.h"
|
|
|
|
namespace v8 {
|
|
namespace internal {
|
|
|
|
using Closure = std::function<void()>;
|
|
|
|
void RunStdFunction(void* data) {
|
|
std::unique_ptr<Closure> f(static_cast<Closure*>(data));
|
|
(*f)();
|
|
}
|
|
|
|
template <typename TMixin>
|
|
class WithFinalizationRegistryMixin : public TMixin {
|
|
public:
|
|
WithFinalizationRegistryMixin() = default;
|
|
~WithFinalizationRegistryMixin() override = default;
|
|
WithFinalizationRegistryMixin(const WithFinalizationRegistryMixin&) = delete;
|
|
WithFinalizationRegistryMixin& operator=(
|
|
const WithFinalizationRegistryMixin&) = delete;
|
|
|
|
static void SetUpTestCase() {
|
|
CHECK_NULL(save_flags_);
|
|
save_flags_ = new SaveFlags();
|
|
FLAG_expose_gc = true;
|
|
FLAG_allow_natives_syntax = true;
|
|
TMixin::SetUpTestCase();
|
|
}
|
|
|
|
static void TearDownTestCase() {
|
|
TMixin::TearDownTestCase();
|
|
CHECK_NOT_NULL(save_flags_);
|
|
delete save_flags_;
|
|
save_flags_ = nullptr;
|
|
}
|
|
|
|
private:
|
|
static SaveFlags* save_flags_;
|
|
};
|
|
|
|
template <typename TMixin>
|
|
SaveFlags* WithFinalizationRegistryMixin<TMixin>::save_flags_ = nullptr;
|
|
|
|
using TestWithNativeContextAndFinalizationRegistry = //
|
|
WithInternalIsolateMixin< //
|
|
WithContextMixin< //
|
|
WithFinalizationRegistryMixin< //
|
|
WithIsolateScopeMixin< //
|
|
WithIsolateMixin< //
|
|
::testing::Test>>>>>;
|
|
|
|
namespace {
|
|
|
|
void DummyPromiseHook(PromiseHookType type, Local<Promise> promise,
|
|
Local<Value> parent) {}
|
|
|
|
} // namespace
|
|
|
|
class MicrotaskQueueTest : public TestWithNativeContextAndFinalizationRegistry,
|
|
public ::testing::WithParamInterface<bool> {
|
|
public:
|
|
template <typename F>
|
|
Handle<Microtask> NewMicrotask(F&& f) {
|
|
Handle<Foreign> runner =
|
|
factory()->NewForeign(reinterpret_cast<Address>(&RunStdFunction));
|
|
Handle<Foreign> data = factory()->NewForeign(
|
|
reinterpret_cast<Address>(new Closure(std::forward<F>(f))));
|
|
return factory()->NewCallbackTask(runner, data);
|
|
}
|
|
|
|
void SetUp() override {
|
|
microtask_queue_ = MicrotaskQueue::New(isolate());
|
|
native_context()->set_microtask_queue(isolate(), microtask_queue());
|
|
|
|
if (GetParam()) {
|
|
// Use a PromiseHook to switch the implementation to ResolvePromise
|
|
// runtime, instead of ResolvePromise builtin.
|
|
v8_isolate()->SetPromiseHook(&DummyPromiseHook);
|
|
}
|
|
}
|
|
|
|
void TearDown() override {
|
|
if (microtask_queue()) {
|
|
microtask_queue()->RunMicrotasks(isolate());
|
|
context()->DetachGlobal();
|
|
}
|
|
}
|
|
|
|
MicrotaskQueue* microtask_queue() const { return microtask_queue_.get(); }
|
|
|
|
void ClearTestMicrotaskQueue() {
|
|
context()->DetachGlobal();
|
|
microtask_queue_ = nullptr;
|
|
}
|
|
|
|
template <size_t N>
|
|
Handle<Name> NameFromChars(const char (&chars)[N]) {
|
|
return isolate()->factory()->NewStringFromStaticChars(chars);
|
|
}
|
|
|
|
private:
|
|
std::unique_ptr<MicrotaskQueue> microtask_queue_;
|
|
};
|
|
|
|
class RecordingVisitor : public RootVisitor {
|
|
public:
|
|
RecordingVisitor() = default;
|
|
~RecordingVisitor() override = default;
|
|
|
|
void VisitRootPointers(Root root, const char* description,
|
|
FullObjectSlot start, FullObjectSlot end) override {
|
|
for (FullObjectSlot current = start; current != end; ++current) {
|
|
visited_.push_back(*current);
|
|
}
|
|
}
|
|
|
|
const std::vector<Object>& visited() const { return visited_; }
|
|
|
|
private:
|
|
std::vector<Object> visited_;
|
|
};
|
|
|
|
// Sanity check. Ensure a microtask is stored in a queue and run.
|
|
TEST_P(MicrotaskQueueTest, EnqueueAndRun) {
|
|
bool ran = false;
|
|
EXPECT_EQ(0, microtask_queue()->capacity());
|
|
EXPECT_EQ(0, microtask_queue()->size());
|
|
microtask_queue()->EnqueueMicrotask(*NewMicrotask([&ran] {
|
|
EXPECT_FALSE(ran);
|
|
ran = true;
|
|
}));
|
|
EXPECT_EQ(MicrotaskQueue::kMinimumCapacity, microtask_queue()->capacity());
|
|
EXPECT_EQ(1, microtask_queue()->size());
|
|
EXPECT_EQ(1, microtask_queue()->RunMicrotasks(isolate()));
|
|
EXPECT_TRUE(ran);
|
|
EXPECT_EQ(0, microtask_queue()->size());
|
|
}
|
|
|
|
// Check for a buffer growth.
|
|
TEST_P(MicrotaskQueueTest, BufferGrowth) {
|
|
int count = 0;
|
|
|
|
// Enqueue and flush the queue first to have non-zero |start_|.
|
|
microtask_queue()->EnqueueMicrotask(
|
|
*NewMicrotask([&count] { EXPECT_EQ(0, count++); }));
|
|
EXPECT_EQ(1, microtask_queue()->RunMicrotasks(isolate()));
|
|
|
|
EXPECT_LT(0, microtask_queue()->capacity());
|
|
EXPECT_EQ(0, microtask_queue()->size());
|
|
EXPECT_EQ(1, microtask_queue()->start());
|
|
|
|
// Fill the queue with Microtasks.
|
|
for (int i = 1; i <= MicrotaskQueue::kMinimumCapacity; ++i) {
|
|
microtask_queue()->EnqueueMicrotask(
|
|
*NewMicrotask([&count, i] { EXPECT_EQ(i, count++); }));
|
|
}
|
|
EXPECT_EQ(MicrotaskQueue::kMinimumCapacity, microtask_queue()->capacity());
|
|
EXPECT_EQ(MicrotaskQueue::kMinimumCapacity, microtask_queue()->size());
|
|
|
|
// Add another to grow the ring buffer.
|
|
microtask_queue()->EnqueueMicrotask(*NewMicrotask(
|
|
[&] { EXPECT_EQ(MicrotaskQueue::kMinimumCapacity + 1, count++); }));
|
|
|
|
EXPECT_LT(MicrotaskQueue::kMinimumCapacity, microtask_queue()->capacity());
|
|
EXPECT_EQ(MicrotaskQueue::kMinimumCapacity + 1, microtask_queue()->size());
|
|
|
|
// Run all pending Microtasks to ensure they run in the proper order.
|
|
EXPECT_EQ(MicrotaskQueue::kMinimumCapacity + 1,
|
|
microtask_queue()->RunMicrotasks(isolate()));
|
|
EXPECT_EQ(MicrotaskQueue::kMinimumCapacity + 2, count);
|
|
}
|
|
|
|
// MicrotaskQueue instances form a doubly linked list.
|
|
TEST_P(MicrotaskQueueTest, InstanceChain) {
|
|
ClearTestMicrotaskQueue();
|
|
|
|
MicrotaskQueue* default_mtq = isolate()->default_microtask_queue();
|
|
ASSERT_TRUE(default_mtq);
|
|
EXPECT_EQ(default_mtq, default_mtq->next());
|
|
EXPECT_EQ(default_mtq, default_mtq->prev());
|
|
|
|
// Create two instances, and check their connection.
|
|
// The list contains all instances in the creation order, and the next of the
|
|
// last instance is the first instance:
|
|
// default_mtq -> mtq1 -> mtq2 -> default_mtq.
|
|
std::unique_ptr<MicrotaskQueue> mtq1 = MicrotaskQueue::New(isolate());
|
|
std::unique_ptr<MicrotaskQueue> mtq2 = MicrotaskQueue::New(isolate());
|
|
EXPECT_EQ(default_mtq->next(), mtq1.get());
|
|
EXPECT_EQ(mtq1->next(), mtq2.get());
|
|
EXPECT_EQ(mtq2->next(), default_mtq);
|
|
EXPECT_EQ(default_mtq, mtq1->prev());
|
|
EXPECT_EQ(mtq1.get(), mtq2->prev());
|
|
EXPECT_EQ(mtq2.get(), default_mtq->prev());
|
|
|
|
// Deleted item should be also removed from the list.
|
|
mtq1 = nullptr;
|
|
EXPECT_EQ(default_mtq->next(), mtq2.get());
|
|
EXPECT_EQ(mtq2->next(), default_mtq);
|
|
EXPECT_EQ(default_mtq, mtq2->prev());
|
|
EXPECT_EQ(mtq2.get(), default_mtq->prev());
|
|
}
|
|
|
|
// Pending Microtasks in MicrotaskQueues are strong roots. Ensure they are
|
|
// visited exactly once.
|
|
TEST_P(MicrotaskQueueTest, VisitRoot) {
|
|
// Ensure that the ring buffer has separate in-use region.
|
|
for (int i = 0; i < MicrotaskQueue::kMinimumCapacity / 2 + 1; ++i) {
|
|
microtask_queue()->EnqueueMicrotask(*NewMicrotask([] {}));
|
|
}
|
|
EXPECT_EQ(MicrotaskQueue::kMinimumCapacity / 2 + 1,
|
|
microtask_queue()->RunMicrotasks(isolate()));
|
|
|
|
std::vector<Object> expected;
|
|
for (int i = 0; i < MicrotaskQueue::kMinimumCapacity / 2 + 1; ++i) {
|
|
Handle<Microtask> microtask = NewMicrotask([] {});
|
|
expected.push_back(*microtask);
|
|
microtask_queue()->EnqueueMicrotask(*microtask);
|
|
}
|
|
EXPECT_GT(microtask_queue()->start() + microtask_queue()->size(),
|
|
microtask_queue()->capacity());
|
|
|
|
RecordingVisitor visitor;
|
|
microtask_queue()->IterateMicrotasks(&visitor);
|
|
|
|
std::vector<Object> actual = visitor.visited();
|
|
std::sort(expected.begin(), expected.end());
|
|
std::sort(actual.begin(), actual.end());
|
|
EXPECT_EQ(expected, actual);
|
|
}
|
|
|
|
TEST_P(MicrotaskQueueTest, PromiseHandlerContext) {
|
|
microtask_queue()->set_microtasks_policy(MicrotasksPolicy::kExplicit);
|
|
Local<v8::Context> v8_context2 = v8::Context::New(v8_isolate());
|
|
Local<v8::Context> v8_context3 = v8::Context::New(v8_isolate());
|
|
Local<v8::Context> v8_context4 = v8::Context::New(v8_isolate());
|
|
Handle<Context> context2 = Utils::OpenHandle(*v8_context2, isolate());
|
|
Handle<Context> context3 = Utils::OpenHandle(*v8_context3, isolate());
|
|
Handle<Context> context4 = Utils::OpenHandle(*v8_context3, isolate());
|
|
context2->native_context().set_microtask_queue(isolate(), microtask_queue());
|
|
context3->native_context().set_microtask_queue(isolate(), microtask_queue());
|
|
context4->native_context().set_microtask_queue(isolate(), microtask_queue());
|
|
|
|
Handle<JSFunction> handler;
|
|
Handle<JSProxy> proxy;
|
|
Handle<JSProxy> revoked_proxy;
|
|
Handle<JSBoundFunction> bound;
|
|
|
|
// Create a JSFunction on |context2|
|
|
{
|
|
v8::Context::Scope scope(v8_context2);
|
|
handler = RunJS<JSFunction>("()=>{}");
|
|
EXPECT_EQ(*context2,
|
|
*JSReceiver::GetContextForMicrotask(handler).ToHandleChecked());
|
|
}
|
|
|
|
// Create a JSProxy on |context3|.
|
|
{
|
|
v8::Context::Scope scope(v8_context3);
|
|
ASSERT_TRUE(
|
|
v8_context3->Global()
|
|
->Set(v8_context3, NewString("handler"), Utils::ToLocal(handler))
|
|
.FromJust());
|
|
proxy = RunJS<JSProxy>("new Proxy(handler, {})");
|
|
revoked_proxy = RunJS<JSProxy>(
|
|
"let {proxy, revoke} = Proxy.revocable(handler, {});"
|
|
"revoke();"
|
|
"proxy");
|
|
EXPECT_EQ(*context2,
|
|
*JSReceiver::GetContextForMicrotask(proxy).ToHandleChecked());
|
|
EXPECT_TRUE(JSReceiver::GetContextForMicrotask(revoked_proxy).is_null());
|
|
}
|
|
|
|
// Create a JSBoundFunction on |context4|.
|
|
// Note that its CreationContext and ContextForTaskCancellation is |context2|.
|
|
{
|
|
v8::Context::Scope scope(v8_context4);
|
|
ASSERT_TRUE(
|
|
v8_context4->Global()
|
|
->Set(v8_context4, NewString("handler"), Utils::ToLocal(handler))
|
|
.FromJust());
|
|
bound = RunJS<JSBoundFunction>("handler.bind()");
|
|
EXPECT_EQ(*context2,
|
|
*JSReceiver::GetContextForMicrotask(bound).ToHandleChecked());
|
|
}
|
|
|
|
// Give the objects to the main context.
|
|
SetGlobalProperty("handler", Utils::ToLocal(handler));
|
|
SetGlobalProperty("proxy", Utils::ToLocal(proxy));
|
|
SetGlobalProperty("revoked_proxy", Utils::ToLocal(revoked_proxy));
|
|
SetGlobalProperty("bound", Utils::ToLocal(Handle<JSReceiver>::cast(bound)));
|
|
RunJS(
|
|
"Promise.resolve().then(handler);"
|
|
"Promise.reject().catch(proxy);"
|
|
"Promise.resolve().then(revoked_proxy);"
|
|
"Promise.resolve().then(bound);");
|
|
|
|
ASSERT_EQ(4, microtask_queue()->size());
|
|
Handle<Microtask> microtask1(microtask_queue()->get(0), isolate());
|
|
ASSERT_TRUE(microtask1->IsPromiseFulfillReactionJobTask());
|
|
EXPECT_EQ(*context2,
|
|
Handle<PromiseFulfillReactionJobTask>::cast(microtask1)->context());
|
|
|
|
Handle<Microtask> microtask2(microtask_queue()->get(1), isolate());
|
|
ASSERT_TRUE(microtask2->IsPromiseRejectReactionJobTask());
|
|
EXPECT_EQ(*context2,
|
|
Handle<PromiseRejectReactionJobTask>::cast(microtask2)->context());
|
|
|
|
Handle<Microtask> microtask3(microtask_queue()->get(2), isolate());
|
|
ASSERT_TRUE(microtask3->IsPromiseFulfillReactionJobTask());
|
|
// |microtask3| corresponds to a PromiseReaction for |revoked_proxy|.
|
|
// As |revoked_proxy| doesn't have a context, the current context should be
|
|
// used as the fallback context.
|
|
EXPECT_EQ(*native_context(),
|
|
Handle<PromiseFulfillReactionJobTask>::cast(microtask3)->context());
|
|
|
|
Handle<Microtask> microtask4(microtask_queue()->get(3), isolate());
|
|
ASSERT_TRUE(microtask4->IsPromiseFulfillReactionJobTask());
|
|
EXPECT_EQ(*context2,
|
|
Handle<PromiseFulfillReactionJobTask>::cast(microtask4)->context());
|
|
|
|
v8_context4->DetachGlobal();
|
|
v8_context3->DetachGlobal();
|
|
v8_context2->DetachGlobal();
|
|
}
|
|
|
|
TEST_P(MicrotaskQueueTest, DetachGlobal_Enqueue) {
|
|
EXPECT_EQ(0, microtask_queue()->size());
|
|
|
|
// Detach MicrotaskQueue from the current context.
|
|
context()->DetachGlobal();
|
|
|
|
// No microtask should be enqueued after DetachGlobal call.
|
|
EXPECT_EQ(0, microtask_queue()->size());
|
|
RunJS("Promise.resolve().then(()=>{})");
|
|
EXPECT_EQ(0, microtask_queue()->size());
|
|
}
|
|
|
|
TEST_P(MicrotaskQueueTest, DetachGlobal_Run) {
|
|
microtask_queue()->set_microtasks_policy(MicrotasksPolicy::kExplicit);
|
|
EXPECT_EQ(0, microtask_queue()->size());
|
|
|
|
// Enqueue microtasks to the current context.
|
|
Handle<JSArray> ran = RunJS<JSArray>(
|
|
"var ran = [false, false, false, false];"
|
|
"Promise.resolve().then(() => { ran[0] = true; });"
|
|
"Promise.reject().catch(() => { ran[1] = true; });"
|
|
"ran");
|
|
|
|
Handle<JSFunction> function =
|
|
RunJS<JSFunction>("(function() { ran[2] = true; })");
|
|
Handle<CallableTask> callable =
|
|
factory()->NewCallableTask(function, Utils::OpenHandle(*context()));
|
|
microtask_queue()->EnqueueMicrotask(*callable);
|
|
|
|
// The handler should not run at this point.
|
|
const int kNumExpectedTasks = 3;
|
|
for (int i = 0; i < kNumExpectedTasks; ++i) {
|
|
EXPECT_TRUE(
|
|
Object::GetElement(isolate(), ran, i).ToHandleChecked()->IsFalse());
|
|
}
|
|
EXPECT_EQ(kNumExpectedTasks, microtask_queue()->size());
|
|
|
|
// Detach MicrotaskQueue from the current context.
|
|
context()->DetachGlobal();
|
|
|
|
// RunMicrotasks processes pending Microtasks, but Microtasks that are
|
|
// associated to a detached context should be cancelled and should not take
|
|
// effect.
|
|
microtask_queue()->RunMicrotasks(isolate());
|
|
EXPECT_EQ(0, microtask_queue()->size());
|
|
for (int i = 0; i < kNumExpectedTasks; ++i) {
|
|
EXPECT_TRUE(
|
|
Object::GetElement(isolate(), ran, i).ToHandleChecked()->IsFalse());
|
|
}
|
|
}
|
|
|
|
TEST_P(MicrotaskQueueTest, DetachGlobal_PromiseResolveThenableJobTask) {
|
|
microtask_queue()->set_microtasks_policy(MicrotasksPolicy::kExplicit);
|
|
RunJS(
|
|
"var resolve;"
|
|
"var promise = new Promise(r => { resolve = r; });"
|
|
"promise.then(() => {});"
|
|
"resolve({});");
|
|
|
|
// A PromiseResolveThenableJobTask is pending in the MicrotaskQueue.
|
|
EXPECT_EQ(1, microtask_queue()->size());
|
|
|
|
// Detach MicrotaskQueue from the current context.
|
|
context()->DetachGlobal();
|
|
|
|
// RunMicrotasks processes the pending Microtask, but Microtasks that are
|
|
// associated to a detached context should be cancelled and should not take
|
|
// effect.
|
|
// As PromiseResolveThenableJobTask queues another task for resolution,
|
|
// the return value is 2 if it ran.
|
|
EXPECT_EQ(1, microtask_queue()->RunMicrotasks(isolate()));
|
|
EXPECT_EQ(0, microtask_queue()->size());
|
|
}
|
|
|
|
TEST_P(MicrotaskQueueTest, DetachGlobal_ResolveThenableForeignThen) {
|
|
microtask_queue()->set_microtasks_policy(MicrotasksPolicy::kExplicit);
|
|
Handle<JSArray> result = RunJS<JSArray>(
|
|
"let result = [false];"
|
|
"result");
|
|
Handle<JSFunction> then = RunJS<JSFunction>("() => { result[0] = true; }");
|
|
|
|
Handle<JSPromise> stale_promise;
|
|
|
|
{
|
|
// Create a context with its own microtask queue.
|
|
std::unique_ptr<MicrotaskQueue> sub_microtask_queue =
|
|
MicrotaskQueue::New(isolate());
|
|
sub_microtask_queue->set_microtasks_policy(MicrotasksPolicy::kExplicit);
|
|
Local<v8::Context> sub_context = v8::Context::New(
|
|
v8_isolate(),
|
|
/* extensions= */ nullptr,
|
|
/* global_template= */ MaybeLocal<ObjectTemplate>(),
|
|
/* global_object= */ MaybeLocal<Value>(),
|
|
/* internal_fields_deserializer= */ DeserializeInternalFieldsCallback(),
|
|
sub_microtask_queue.get());
|
|
|
|
{
|
|
v8::Context::Scope scope(sub_context);
|
|
CHECK(sub_context->Global()
|
|
->Set(sub_context, NewString("then"),
|
|
Utils::ToLocal(Handle<JSReceiver>::cast(then)))
|
|
.FromJust());
|
|
|
|
ASSERT_EQ(0, microtask_queue()->size());
|
|
ASSERT_EQ(0, sub_microtask_queue->size());
|
|
ASSERT_TRUE(Object::GetElement(isolate(), result, 0)
|
|
.ToHandleChecked()
|
|
->IsFalse());
|
|
|
|
// With a regular thenable, a microtask is queued on the sub-context.
|
|
RunJS<JSPromise>("Promise.resolve({ then: cb => cb(1) })");
|
|
EXPECT_EQ(0, microtask_queue()->size());
|
|
EXPECT_EQ(1, sub_microtask_queue->size());
|
|
EXPECT_TRUE(Object::GetElement(isolate(), result, 0)
|
|
.ToHandleChecked()
|
|
->IsFalse());
|
|
|
|
// But when the `then` method comes from another context, a microtask is
|
|
// instead queued on the main context.
|
|
stale_promise = RunJS<JSPromise>("Promise.resolve({ then })");
|
|
EXPECT_EQ(1, microtask_queue()->size());
|
|
EXPECT_EQ(1, sub_microtask_queue->size());
|
|
EXPECT_TRUE(Object::GetElement(isolate(), result, 0)
|
|
.ToHandleChecked()
|
|
->IsFalse());
|
|
}
|
|
|
|
sub_context->DetachGlobal();
|
|
}
|
|
|
|
EXPECT_EQ(1, microtask_queue()->size());
|
|
EXPECT_TRUE(
|
|
Object::GetElement(isolate(), result, 0).ToHandleChecked()->IsFalse());
|
|
|
|
EXPECT_EQ(1, microtask_queue()->RunMicrotasks(isolate()));
|
|
EXPECT_EQ(0, microtask_queue()->size());
|
|
EXPECT_TRUE(
|
|
Object::GetElement(isolate(), result, 0).ToHandleChecked()->IsTrue());
|
|
}
|
|
|
|
TEST_P(MicrotaskQueueTest, DetachGlobal_HandlerContext) {
|
|
// EnqueueMicrotask should use the context associated to the handler instead
|
|
// of the current context. E.g.
|
|
// // At Context A.
|
|
// let resolved = Promise.resolve();
|
|
// // Call DetachGlobal on A, so that microtasks associated to A is
|
|
// // cancelled.
|
|
//
|
|
// // At Context B.
|
|
// let handler = () => {
|
|
// console.log("here");
|
|
// };
|
|
// // The microtask to run |handler| should be associated to B instead of A,
|
|
// // so that handler runs even |resolved| is on the detached context A.
|
|
// resolved.then(handler);
|
|
|
|
Handle<JSReceiver> results = isolate()->factory()->NewJSObjectWithNullProto();
|
|
|
|
// These belong to a stale Context.
|
|
Handle<JSPromise> stale_resolved_promise;
|
|
Handle<JSPromise> stale_rejected_promise;
|
|
Handle<JSReceiver> stale_handler;
|
|
|
|
Local<v8::Context> sub_context = v8::Context::New(v8_isolate());
|
|
{
|
|
v8::Context::Scope scope(sub_context);
|
|
stale_resolved_promise = RunJS<JSPromise>("Promise.resolve()");
|
|
stale_rejected_promise = RunJS<JSPromise>("Promise.reject()");
|
|
stale_handler = RunJS<JSReceiver>(
|
|
"(results, label) => {"
|
|
" results[label] = true;"
|
|
"}");
|
|
}
|
|
// DetachGlobal() cancells all microtasks associated to the context.
|
|
sub_context->DetachGlobal();
|
|
sub_context.Clear();
|
|
|
|
SetGlobalProperty("results", Utils::ToLocal(results));
|
|
SetGlobalProperty(
|
|
"stale_resolved_promise",
|
|
Utils::ToLocal(Handle<JSReceiver>::cast(stale_resolved_promise)));
|
|
SetGlobalProperty(
|
|
"stale_rejected_promise",
|
|
Utils::ToLocal(Handle<JSReceiver>::cast(stale_rejected_promise)));
|
|
SetGlobalProperty("stale_handler", Utils::ToLocal(stale_handler));
|
|
|
|
// Set valid handlers to stale promises.
|
|
RunJS(
|
|
"stale_resolved_promise.then(() => {"
|
|
" results['stale_resolved_promise'] = true;"
|
|
"})");
|
|
RunJS(
|
|
"stale_rejected_promise.catch(() => {"
|
|
" results['stale_rejected_promise'] = true;"
|
|
"})");
|
|
microtask_queue()->RunMicrotasks(isolate());
|
|
EXPECT_TRUE(
|
|
JSReceiver::HasProperty(results, NameFromChars("stale_resolved_promise"))
|
|
.FromJust());
|
|
EXPECT_TRUE(
|
|
JSReceiver::HasProperty(results, NameFromChars("stale_rejected_promise"))
|
|
.FromJust());
|
|
|
|
// Set stale handlers to valid promises.
|
|
RunJS(
|
|
"Promise.resolve("
|
|
" stale_handler.bind(null, results, 'stale_handler_resolve'))");
|
|
RunJS(
|
|
"Promise.reject("
|
|
" stale_handler.bind(null, results, 'stale_handler_reject'))");
|
|
microtask_queue()->RunMicrotasks(isolate());
|
|
EXPECT_FALSE(
|
|
JSReceiver::HasProperty(results, NameFromChars("stale_handler_resolve"))
|
|
.FromJust());
|
|
EXPECT_FALSE(
|
|
JSReceiver::HasProperty(results, NameFromChars("stale_handler_reject"))
|
|
.FromJust());
|
|
}
|
|
|
|
TEST_P(MicrotaskQueueTest, DetachGlobal_Chain) {
|
|
Handle<JSPromise> stale_rejected_promise;
|
|
|
|
Local<v8::Context> sub_context = v8::Context::New(v8_isolate());
|
|
{
|
|
v8::Context::Scope scope(sub_context);
|
|
stale_rejected_promise = RunJS<JSPromise>("Promise.reject()");
|
|
}
|
|
sub_context->DetachGlobal();
|
|
sub_context.Clear();
|
|
|
|
SetGlobalProperty(
|
|
"stale_rejected_promise",
|
|
Utils::ToLocal(Handle<JSReceiver>::cast(stale_rejected_promise)));
|
|
Handle<JSArray> result = RunJS<JSArray>(
|
|
"let result = [false];"
|
|
"stale_rejected_promise"
|
|
" .then(() => {})"
|
|
" .catch(() => {"
|
|
" result[0] = true;"
|
|
" });"
|
|
"result");
|
|
microtask_queue()->RunMicrotasks(isolate());
|
|
EXPECT_TRUE(
|
|
Object::GetElement(isolate(), result, 0).ToHandleChecked()->IsTrue());
|
|
}
|
|
|
|
TEST_P(MicrotaskQueueTest, DetachGlobal_InactiveHandler) {
|
|
Local<v8::Context> sub_context = v8::Context::New(v8_isolate());
|
|
Utils::OpenHandle(*sub_context)
|
|
->native_context()
|
|
.set_microtask_queue(isolate(), microtask_queue());
|
|
|
|
Handle<JSArray> result;
|
|
Handle<JSFunction> stale_handler;
|
|
Handle<JSPromise> stale_promise;
|
|
{
|
|
v8::Context::Scope scope(sub_context);
|
|
result = RunJS<JSArray>("var result = [false, false]; result");
|
|
stale_handler = RunJS<JSFunction>("() => { result[0] = true; }");
|
|
stale_promise = RunJS<JSPromise>(
|
|
"var stale_promise = new Promise(()=>{});"
|
|
"stale_promise");
|
|
RunJS("stale_promise.then(() => { result [1] = true; });");
|
|
}
|
|
sub_context->DetachGlobal();
|
|
sub_context.Clear();
|
|
|
|
// The context of |stale_handler| and |stale_promise| is detached at this
|
|
// point.
|
|
// Ensure that resolution handling for |stale_handler| is cancelled without
|
|
// crash. Also, the resolution of |stale_promise| is also cancelled.
|
|
|
|
SetGlobalProperty("stale_handler", Utils::ToLocal(stale_handler));
|
|
RunJS("%EnqueueMicrotask(stale_handler)");
|
|
|
|
v8_isolate()->EnqueueMicrotask(Utils::ToLocal(stale_handler));
|
|
|
|
JSPromise::Fulfill(
|
|
stale_promise,
|
|
handle(ReadOnlyRoots(isolate()).undefined_value(), isolate()));
|
|
|
|
microtask_queue()->RunMicrotasks(isolate());
|
|
EXPECT_TRUE(
|
|
Object::GetElement(isolate(), result, 0).ToHandleChecked()->IsFalse());
|
|
EXPECT_TRUE(
|
|
Object::GetElement(isolate(), result, 1).ToHandleChecked()->IsFalse());
|
|
}
|
|
|
|
TEST_P(MicrotaskQueueTest, MicrotasksScope) {
|
|
ASSERT_NE(isolate()->default_microtask_queue(), microtask_queue());
|
|
microtask_queue()->set_microtasks_policy(MicrotasksPolicy::kScoped);
|
|
|
|
bool ran = false;
|
|
{
|
|
MicrotasksScope scope(v8_isolate(), microtask_queue(),
|
|
MicrotasksScope::kRunMicrotasks);
|
|
microtask_queue()->EnqueueMicrotask(*NewMicrotask([&ran]() {
|
|
EXPECT_FALSE(ran);
|
|
ran = true;
|
|
}));
|
|
}
|
|
EXPECT_TRUE(ran);
|
|
}
|
|
|
|
INSTANTIATE_TEST_SUITE_P(
|
|
, MicrotaskQueueTest, ::testing::Values(false, true),
|
|
[](const ::testing::TestParamInfo<MicrotaskQueueTest::ParamType>& info) {
|
|
return info.param ? "runtime" : "builtin";
|
|
});
|
|
|
|
} // namespace internal
|
|
} // namespace v8
|