// 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/api/api.h" #include "src/builtins/builtins-utils-gen.h" #include "src/codegen/code-stub-assembler.h" #include "src/execution/microtask-queue.h" #include "src/objects/js-weak-refs.h" #include "src/objects/microtask-inl.h" #include "src/objects/promise.h" #include "src/objects/smi-inl.h" namespace v8 { namespace internal { template using TNode = compiler::TNode; class MicrotaskQueueBuiltinsAssembler : public CodeStubAssembler { public: explicit MicrotaskQueueBuiltinsAssembler(compiler::CodeAssemblerState* state) : CodeStubAssembler(state) {} TNode GetMicrotaskQueue(TNode context); TNode GetMicrotaskRingBuffer(TNode microtask_queue); TNode GetMicrotaskQueueCapacity(TNode microtask_queue); TNode GetMicrotaskQueueSize(TNode microtask_queue); void SetMicrotaskQueueSize(TNode microtask_queue, TNode new_size); TNode GetMicrotaskQueueStart(TNode microtask_queue); void SetMicrotaskQueueStart(TNode microtask_queue, TNode new_start); TNode CalculateRingBufferOffset(TNode capacity, TNode start, TNode index); void PrepareForContext(TNode microtask_context, Label* bailout); void RunSingleMicrotask(TNode current_context, TNode microtask); void IncrementFinishedMicrotaskCount(TNode microtask_queue); TNode GetCurrentContext(); void SetCurrentContext(TNode context); TNode GetEnteredContextCount(); void EnterMicrotaskContext(TNode native_context); void RewindEnteredContext(TNode saved_entered_context_count); void RunPromiseHook(Runtime::FunctionId id, TNode context, SloppyTNode promise_or_capability); }; TNode MicrotaskQueueBuiltinsAssembler::GetMicrotaskQueue( TNode native_context) { CSA_ASSERT(this, IsNativeContext(native_context)); return LoadObjectField(native_context, NativeContext::kMicrotaskQueueOffset); } TNode MicrotaskQueueBuiltinsAssembler::GetMicrotaskRingBuffer( TNode microtask_queue) { return UncheckedCast( Load(MachineType::Pointer(), microtask_queue, IntPtrConstant(MicrotaskQueue::kRingBufferOffset))); } TNode MicrotaskQueueBuiltinsAssembler::GetMicrotaskQueueCapacity( TNode microtask_queue) { return UncheckedCast( Load(MachineType::IntPtr(), microtask_queue, IntPtrConstant(MicrotaskQueue::kCapacityOffset))); } TNode MicrotaskQueueBuiltinsAssembler::GetMicrotaskQueueSize( TNode microtask_queue) { return UncheckedCast( Load(MachineType::IntPtr(), microtask_queue, IntPtrConstant(MicrotaskQueue::kSizeOffset))); } void MicrotaskQueueBuiltinsAssembler::SetMicrotaskQueueSize( TNode microtask_queue, TNode new_size) { StoreNoWriteBarrier(MachineType::PointerRepresentation(), microtask_queue, IntPtrConstant(MicrotaskQueue::kSizeOffset), new_size); } TNode MicrotaskQueueBuiltinsAssembler::GetMicrotaskQueueStart( TNode microtask_queue) { return UncheckedCast( Load(MachineType::IntPtr(), microtask_queue, IntPtrConstant(MicrotaskQueue::kStartOffset))); } void MicrotaskQueueBuiltinsAssembler::SetMicrotaskQueueStart( TNode microtask_queue, TNode new_start) { StoreNoWriteBarrier(MachineType::PointerRepresentation(), microtask_queue, IntPtrConstant(MicrotaskQueue::kStartOffset), new_start); } TNode MicrotaskQueueBuiltinsAssembler::CalculateRingBufferOffset( TNode capacity, TNode start, TNode index) { return TimesSystemPointerSize( WordAnd(IntPtrAdd(start, index), IntPtrSub(capacity, IntPtrConstant(1)))); } void MicrotaskQueueBuiltinsAssembler::PrepareForContext( TNode native_context, Label* bailout) { CSA_ASSERT(this, IsNativeContext(native_context)); // Skip the microtask execution if the associated context is shutdown. GotoIf(WordEqual(GetMicrotaskQueue(native_context), IntPtrConstant(0)), bailout); EnterMicrotaskContext(native_context); SetCurrentContext(native_context); } void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( TNode current_context, TNode microtask) { CSA_ASSERT(this, TaggedIsNotSmi(microtask)); StoreRoot(RootIndex::kCurrentMicrotask, microtask); TNode saved_entered_context_count = GetEnteredContextCount(); TNode microtask_map = LoadMap(microtask); TNode microtask_type = LoadMapInstanceType(microtask_map); VARIABLE(var_exception, MachineRepresentation::kTagged, TheHoleConstant()); Label if_exception(this, Label::kDeferred); Label is_callable(this), is_callback(this), is_promise_fulfill_reaction_job(this), is_promise_reject_reaction_job(this), is_promise_resolve_thenable_job(this), is_unreachable(this, Label::kDeferred), done(this); int32_t case_values[] = {CALLABLE_TASK_TYPE, CALLBACK_TASK_TYPE, PROMISE_FULFILL_REACTION_JOB_TASK_TYPE, PROMISE_REJECT_REACTION_JOB_TASK_TYPE, PROMISE_RESOLVE_THENABLE_JOB_TASK_TYPE}; Label* case_labels[] = { &is_callable, &is_callback, &is_promise_fulfill_reaction_job, &is_promise_reject_reaction_job, &is_promise_resolve_thenable_job}; static_assert(arraysize(case_values) == arraysize(case_labels), ""); Switch(microtask_type, &is_unreachable, case_values, case_labels, arraysize(case_labels)); BIND(&is_callable); { // Enter the context of the {microtask}. TNode microtask_context = LoadObjectField(microtask, CallableTask::kContextOffset); TNode native_context = LoadNativeContext(microtask_context); PrepareForContext(native_context, &done); TNode callable = LoadObjectField(microtask, CallableTask::kCallableOffset); Node* const result = CallJS( CodeFactory::Call(isolate(), ConvertReceiverMode::kNullOrUndefined), microtask_context, callable, UndefinedConstant()); GotoIfException(result, &if_exception, &var_exception); RewindEnteredContext(saved_entered_context_count); SetCurrentContext(current_context); Goto(&done); } BIND(&is_callback); { TNode const microtask_callback = LoadObjectField(microtask, CallbackTask::kCallbackOffset); TNode const microtask_data = LoadObjectField(microtask, CallbackTask::kDataOffset); // If this turns out to become a bottleneck because of the calls // to C++ via CEntry, we can choose to speed them up using a // similar mechanism that we use for the CallApiFunction stub, // except that calling the MicrotaskCallback is even easier, since // it doesn't accept any tagged parameters, doesn't return a value // and ignores exceptions. // // But from our current measurements it doesn't seem to be a // serious performance problem, even if the microtask is full // of CallHandlerTasks (which is not a realistic use case anyways). TNode const result = CallRuntime(Runtime::kRunMicrotaskCallback, current_context, microtask_callback, microtask_data); GotoIfException(result, &if_exception, &var_exception); Goto(&done); } BIND(&is_promise_resolve_thenable_job); { // Enter the context of the {microtask}. TNode microtask_context = LoadObjectField( microtask, PromiseResolveThenableJobTask::kContextOffset); TNode native_context = LoadNativeContext(microtask_context); PrepareForContext(native_context, &done); TNode const promise_to_resolve = LoadObjectField( microtask, PromiseResolveThenableJobTask::kPromiseToResolveOffset); TNode const then = LoadObjectField(microtask, PromiseResolveThenableJobTask::kThenOffset); TNode const thenable = LoadObjectField( microtask, PromiseResolveThenableJobTask::kThenableOffset); TNode const result = CallBuiltin(Builtins::kPromiseResolveThenableJob, native_context, promise_to_resolve, thenable, then); GotoIfException(result, &if_exception, &var_exception); RewindEnteredContext(saved_entered_context_count); SetCurrentContext(current_context); Goto(&done); } BIND(&is_promise_fulfill_reaction_job); { // Enter the context of the {microtask}. TNode microtask_context = LoadObjectField( microtask, PromiseReactionJobTask::kContextOffset); TNode native_context = LoadNativeContext(microtask_context); PrepareForContext(native_context, &done); TNode const argument = LoadObjectField(microtask, PromiseReactionJobTask::kArgumentOffset); TNode const handler = LoadObjectField(microtask, PromiseReactionJobTask::kHandlerOffset); TNode const promise_or_capability = CAST(LoadObjectField( microtask, PromiseReactionJobTask::kPromiseOrCapabilityOffset)); // Run the promise before/debug hook if enabled. RunPromiseHook(Runtime::kPromiseHookBefore, microtask_context, promise_or_capability); TNode const result = CallBuiltin(Builtins::kPromiseFulfillReactionJob, microtask_context, argument, handler, promise_or_capability); GotoIfException(result, &if_exception, &var_exception); // Run the promise after/debug hook if enabled. RunPromiseHook(Runtime::kPromiseHookAfter, microtask_context, promise_or_capability); RewindEnteredContext(saved_entered_context_count); SetCurrentContext(current_context); Goto(&done); } BIND(&is_promise_reject_reaction_job); { // Enter the context of the {microtask}. TNode microtask_context = LoadObjectField( microtask, PromiseReactionJobTask::kContextOffset); TNode native_context = LoadNativeContext(microtask_context); PrepareForContext(native_context, &done); TNode const argument = LoadObjectField(microtask, PromiseReactionJobTask::kArgumentOffset); TNode const handler = LoadObjectField(microtask, PromiseReactionJobTask::kHandlerOffset); TNode const promise_or_capability = CAST(LoadObjectField( microtask, PromiseReactionJobTask::kPromiseOrCapabilityOffset)); // Run the promise before/debug hook if enabled. RunPromiseHook(Runtime::kPromiseHookBefore, microtask_context, promise_or_capability); TNode const result = CallBuiltin(Builtins::kPromiseRejectReactionJob, microtask_context, argument, handler, promise_or_capability); GotoIfException(result, &if_exception, &var_exception); // Run the promise after/debug hook if enabled. RunPromiseHook(Runtime::kPromiseHookAfter, microtask_context, promise_or_capability); RewindEnteredContext(saved_entered_context_count); SetCurrentContext(current_context); Goto(&done); } BIND(&is_unreachable); Unreachable(); BIND(&if_exception); { // Report unhandled exceptions from microtasks. CallRuntime(Runtime::kReportMessage, current_context, var_exception.value()); RewindEnteredContext(saved_entered_context_count); SetCurrentContext(current_context); Goto(&done); } BIND(&done); } void MicrotaskQueueBuiltinsAssembler::IncrementFinishedMicrotaskCount( TNode microtask_queue) { TNode count = UncheckedCast( Load(MachineType::IntPtr(), microtask_queue, IntPtrConstant(MicrotaskQueue::kFinishedMicrotaskCountOffset))); TNode new_count = IntPtrAdd(count, IntPtrConstant(1)); StoreNoWriteBarrier( MachineType::PointerRepresentation(), microtask_queue, IntPtrConstant(MicrotaskQueue::kFinishedMicrotaskCountOffset), new_count); } TNode MicrotaskQueueBuiltinsAssembler::GetCurrentContext() { auto ref = ExternalReference::Create(kContextAddress, isolate()); return TNode::UncheckedCast(LoadFullTagged(ExternalConstant(ref))); } void MicrotaskQueueBuiltinsAssembler::SetCurrentContext( TNode context) { auto ref = ExternalReference::Create(kContextAddress, isolate()); StoreFullTaggedNoWriteBarrier(ExternalConstant(ref), context); } TNode MicrotaskQueueBuiltinsAssembler::GetEnteredContextCount() { auto ref = ExternalReference::handle_scope_implementer_address(isolate()); Node* hsi = Load(MachineType::Pointer(), ExternalConstant(ref)); using ContextStack = DetachableVector; TNode size_offset = IntPtrConstant(HandleScopeImplementer::kEnteredContextsOffset + ContextStack::kSizeOffset); TNode size = UncheckedCast(Load(MachineType::IntPtr(), hsi, size_offset)); return size; } void MicrotaskQueueBuiltinsAssembler::EnterMicrotaskContext( TNode native_context) { CSA_ASSERT(this, IsNativeContext(native_context)); auto ref = ExternalReference::handle_scope_implementer_address(isolate()); Node* hsi = Load(MachineType::Pointer(), ExternalConstant(ref)); using ContextStack = DetachableVector; TNode capacity_offset = IntPtrConstant(HandleScopeImplementer::kEnteredContextsOffset + ContextStack::kCapacityOffset); TNode size_offset = IntPtrConstant(HandleScopeImplementer::kEnteredContextsOffset + ContextStack::kSizeOffset); TNode capacity = UncheckedCast(Load(MachineType::IntPtr(), hsi, capacity_offset)); TNode size = UncheckedCast(Load(MachineType::IntPtr(), hsi, size_offset)); Label if_append(this), if_grow(this, Label::kDeferred), done(this); Branch(WordEqual(size, capacity), &if_grow, &if_append); BIND(&if_append); { TNode data_offset = IntPtrConstant(HandleScopeImplementer::kEnteredContextsOffset + ContextStack::kDataOffset); Node* data = Load(MachineType::Pointer(), hsi, data_offset); StoreFullTaggedNoWriteBarrier(data, TimesSystemPointerSize(size), native_context); TNode new_size = IntPtrAdd(size, IntPtrConstant(1)); StoreNoWriteBarrier(MachineType::PointerRepresentation(), hsi, size_offset, new_size); using FlagStack = DetachableVector; TNode flag_data_offset = IntPtrConstant(HandleScopeImplementer::kIsMicrotaskContextOffset + FlagStack::kDataOffset); Node* flag_data = Load(MachineType::Pointer(), hsi, flag_data_offset); StoreNoWriteBarrier(MachineRepresentation::kWord8, flag_data, size, BoolConstant(true)); StoreNoWriteBarrier( MachineType::PointerRepresentation(), hsi, IntPtrConstant(HandleScopeImplementer::kIsMicrotaskContextOffset + FlagStack::kSizeOffset), new_size); Goto(&done); } BIND(&if_grow); { TNode function = ExternalConstant(ExternalReference::call_enter_context_function()); CallCFunction(function, MachineType::Int32(), std::make_pair(MachineType::Pointer(), hsi), std::make_pair(MachineType::Pointer(), BitcastTaggedToWord(native_context))); Goto(&done); } BIND(&done); } void MicrotaskQueueBuiltinsAssembler::RewindEnteredContext( TNode saved_entered_context_count) { auto ref = ExternalReference::handle_scope_implementer_address(isolate()); Node* hsi = Load(MachineType::Pointer(), ExternalConstant(ref)); using ContextStack = DetachableVector; TNode size_offset = IntPtrConstant(HandleScopeImplementer::kEnteredContextsOffset + ContextStack::kSizeOffset); #ifdef ENABLE_VERIFY_CSA TNode size = UncheckedCast(Load(MachineType::IntPtr(), hsi, size_offset)); CSA_ASSERT(this, IntPtrLessThan(IntPtrConstant(0), size)); CSA_ASSERT(this, IntPtrLessThanOrEqual(saved_entered_context_count, size)); #endif StoreNoWriteBarrier(MachineType::PointerRepresentation(), hsi, size_offset, saved_entered_context_count); using FlagStack = DetachableVector; StoreNoWriteBarrier( MachineType::PointerRepresentation(), hsi, IntPtrConstant(HandleScopeImplementer::kIsMicrotaskContextOffset + FlagStack::kSizeOffset), saved_entered_context_count); } void MicrotaskQueueBuiltinsAssembler::RunPromiseHook( Runtime::FunctionId id, TNode context, SloppyTNode promise_or_capability) { Label hook(this, Label::kDeferred), done_hook(this); Branch(IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(), &hook, &done_hook); BIND(&hook); { // Get to the underlying JSPromise instance. TNode promise = Select( IsPromiseCapability(promise_or_capability), [=] { return CAST(LoadObjectField(promise_or_capability, PromiseCapability::kPromiseOffset)); }, [=] { return promise_or_capability; }); GotoIf(IsUndefined(promise), &done_hook); CallRuntime(id, context, promise); Goto(&done_hook); } BIND(&done_hook); } TF_BUILTIN(EnqueueMicrotask, MicrotaskQueueBuiltinsAssembler) { TNode microtask = UncheckedCast(Parameter(Descriptor::kMicrotask)); TNode context = CAST(Parameter(Descriptor::kContext)); TNode native_context = LoadNativeContext(context); TNode microtask_queue = GetMicrotaskQueue(native_context); // Do not store the microtask if MicrotaskQueue is not available, that may // happen when the context shutdown. Label if_shutdown(this, Label::kDeferred); GotoIf(WordEqual(microtask_queue, IntPtrConstant(0)), &if_shutdown); TNode ring_buffer = GetMicrotaskRingBuffer(microtask_queue); TNode capacity = GetMicrotaskQueueCapacity(microtask_queue); TNode size = GetMicrotaskQueueSize(microtask_queue); TNode start = GetMicrotaskQueueStart(microtask_queue); Label if_grow(this, Label::kDeferred); GotoIf(IntPtrEqual(size, capacity), &if_grow); // |microtask_queue| has an unused slot to store |microtask|. { StoreNoWriteBarrier(MachineType::PointerRepresentation(), ring_buffer, CalculateRingBufferOffset(capacity, start, size), BitcastTaggedToWord(microtask)); StoreNoWriteBarrier(MachineType::PointerRepresentation(), microtask_queue, IntPtrConstant(MicrotaskQueue::kSizeOffset), IntPtrAdd(size, IntPtrConstant(1))); Return(UndefinedConstant()); } // |microtask_queue| has no space to store |microtask|. Fall back to C++ // implementation to grow the buffer. BIND(&if_grow); { TNode isolate_constant = ExternalConstant(ExternalReference::isolate_address(isolate())); TNode function = ExternalConstant(ExternalReference::call_enqueue_microtask_function()); CallCFunction(function, MachineType::AnyTagged(), std::make_pair(MachineType::Pointer(), isolate_constant), std::make_pair(MachineType::IntPtr(), microtask_queue), std::make_pair(MachineType::AnyTagged(), microtask)); Return(UndefinedConstant()); } Bind(&if_shutdown); Return(UndefinedConstant()); } TF_BUILTIN(RunMicrotasks, MicrotaskQueueBuiltinsAssembler) { // Load the current context from the isolate. TNode current_context = GetCurrentContext(); TNode microtask_queue = UncheckedCast(Parameter(Descriptor::kMicrotaskQueue)); Label loop(this), done(this); Goto(&loop); BIND(&loop); TNode size = GetMicrotaskQueueSize(microtask_queue); // Exit if the queue is empty. GotoIf(WordEqual(size, IntPtrConstant(0)), &done); TNode ring_buffer = GetMicrotaskRingBuffer(microtask_queue); TNode capacity = GetMicrotaskQueueCapacity(microtask_queue); TNode start = GetMicrotaskQueueStart(microtask_queue); TNode offset = CalculateRingBufferOffset(capacity, start, IntPtrConstant(0)); TNode microtask_pointer = UncheckedCast(Load(MachineType::Pointer(), ring_buffer, offset)); TNode microtask = CAST(BitcastWordToTagged(microtask_pointer)); TNode new_size = IntPtrSub(size, IntPtrConstant(1)); TNode new_start = WordAnd(IntPtrAdd(start, IntPtrConstant(1)), IntPtrSub(capacity, IntPtrConstant(1))); // Remove |microtask| from |ring_buffer| before running it, since its // invocation may add another microtask into |ring_buffer|. SetMicrotaskQueueSize(microtask_queue, new_size); SetMicrotaskQueueStart(microtask_queue, new_start); RunSingleMicrotask(current_context, microtask); IncrementFinishedMicrotaskCount(microtask_queue); Goto(&loop); BIND(&done); { // Reset the "current microtask" on the isolate. StoreRoot(RootIndex::kCurrentMicrotask, UndefinedConstant()); Return(UndefinedConstant()); } } } // namespace internal } // namespace v8