d59db06bf5
Bug: v8:8179 Change-Id: I7f699073807d1874d0c10a4f1641de6bfb0efe6f Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2741582 Commit-Queue: Shu-yu Guo <syg@chromium.org> Reviewed-by: Shu-yu Guo <syg@chromium.org> Reviewed-by: Adam Klein <adamk@chromium.org> Reviewed-by: Sathya Gunasekaran <gsathya@chromium.org> Cr-Commit-Position: refs/heads/master@{#73871}
652 lines
23 KiB
C++
652 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) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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());
|
|
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
|