// 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 { using compiler::ScopedExceptionHandler; class MicrotaskQueueBuiltinsAssembler : public CodeStubAssembler { public: explicit MicrotaskQueueBuiltinsAssembler(compiler::CodeAssemblerState* state) : CodeStubAssembler(state) {} TNode<RawPtrT> GetMicrotaskQueue(TNode<Context> context); TNode<RawPtrT> GetMicrotaskRingBuffer(TNode<RawPtrT> microtask_queue); TNode<IntPtrT> GetMicrotaskQueueCapacity(TNode<RawPtrT> microtask_queue); TNode<IntPtrT> GetMicrotaskQueueSize(TNode<RawPtrT> microtask_queue); void SetMicrotaskQueueSize(TNode<RawPtrT> microtask_queue, TNode<IntPtrT> new_size); TNode<IntPtrT> GetMicrotaskQueueStart(TNode<RawPtrT> microtask_queue); void SetMicrotaskQueueStart(TNode<RawPtrT> microtask_queue, TNode<IntPtrT> new_start); TNode<IntPtrT> CalculateRingBufferOffset(TNode<IntPtrT> capacity, TNode<IntPtrT> start, TNode<IntPtrT> index); void PrepareForContext(TNode<Context> microtask_context, Label* bailout); void RunSingleMicrotask(TNode<Context> current_context, TNode<Microtask> microtask); void IncrementFinishedMicrotaskCount(TNode<RawPtrT> microtask_queue); TNode<Context> GetCurrentContext(); void SetCurrentContext(TNode<Context> context); TNode<IntPtrT> GetEnteredContextCount(); void EnterMicrotaskContext(TNode<Context> native_context); void RewindEnteredContext(TNode<IntPtrT> saved_entered_context_count); void RunPromiseHook(Runtime::FunctionId id, TNode<Context> context, TNode<HeapObject> promise_or_capability); }; TNode<RawPtrT> MicrotaskQueueBuiltinsAssembler::GetMicrotaskQueue( TNode<Context> native_context) { CSA_ASSERT(this, IsNativeContext(native_context)); return LoadExternalPointerFromObject(native_context, NativeContext::kMicrotaskQueueOffset, kNativeContextMicrotaskQueueTag); } TNode<RawPtrT> MicrotaskQueueBuiltinsAssembler::GetMicrotaskRingBuffer( TNode<RawPtrT> microtask_queue) { return Load<RawPtrT>(microtask_queue, IntPtrConstant(MicrotaskQueue::kRingBufferOffset)); } TNode<IntPtrT> MicrotaskQueueBuiltinsAssembler::GetMicrotaskQueueCapacity( TNode<RawPtrT> microtask_queue) { return Load<IntPtrT>(microtask_queue, IntPtrConstant(MicrotaskQueue::kCapacityOffset)); } TNode<IntPtrT> MicrotaskQueueBuiltinsAssembler::GetMicrotaskQueueSize( TNode<RawPtrT> microtask_queue) { return Load<IntPtrT>(microtask_queue, IntPtrConstant(MicrotaskQueue::kSizeOffset)); } void MicrotaskQueueBuiltinsAssembler::SetMicrotaskQueueSize( TNode<RawPtrT> microtask_queue, TNode<IntPtrT> new_size) { StoreNoWriteBarrier(MachineType::PointerRepresentation(), microtask_queue, IntPtrConstant(MicrotaskQueue::kSizeOffset), new_size); } TNode<IntPtrT> MicrotaskQueueBuiltinsAssembler::GetMicrotaskQueueStart( TNode<RawPtrT> microtask_queue) { return Load<IntPtrT>(microtask_queue, IntPtrConstant(MicrotaskQueue::kStartOffset)); } void MicrotaskQueueBuiltinsAssembler::SetMicrotaskQueueStart( TNode<RawPtrT> microtask_queue, TNode<IntPtrT> new_start) { StoreNoWriteBarrier(MachineType::PointerRepresentation(), microtask_queue, IntPtrConstant(MicrotaskQueue::kStartOffset), new_start); } TNode<IntPtrT> MicrotaskQueueBuiltinsAssembler::CalculateRingBufferOffset( TNode<IntPtrT> capacity, TNode<IntPtrT> start, TNode<IntPtrT> index) { return TimesSystemPointerSize( WordAnd(IntPtrAdd(start, index), IntPtrSub(capacity, IntPtrConstant(1)))); } void MicrotaskQueueBuiltinsAssembler::PrepareForContext( TNode<Context> 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<Context> current_context, TNode<Microtask> microtask) { CSA_ASSERT(this, TaggedIsNotSmi(microtask)); StoreRoot(RootIndex::kCurrentMicrotask, microtask); TNode<IntPtrT> saved_entered_context_count = GetEnteredContextCount(); TNode<Map> microtask_map = LoadMap(microtask); TNode<Uint16T> microtask_type = LoadMapInstanceType(microtask_map); TVARIABLE(Object, var_exception); 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<Context> microtask_context = LoadObjectField<Context>(microtask, CallableTask::kContextOffset); TNode<NativeContext> native_context = LoadNativeContext(microtask_context); PrepareForContext(native_context, &done); TNode<JSReceiver> callable = LoadObjectField<JSReceiver>(microtask, CallableTask::kCallableOffset); { ScopedExceptionHandler handler(this, &if_exception, &var_exception); Call(microtask_context, callable, UndefinedConstant()); } RewindEnteredContext(saved_entered_context_count); SetCurrentContext(current_context); Goto(&done); } BIND(&is_callback); { const TNode<Object> microtask_callback = LoadObjectField(microtask, CallbackTask::kCallbackOffset); const TNode<Object> 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). { ScopedExceptionHandler handler(this, &if_exception, &var_exception); CallRuntime(Runtime::kRunMicrotaskCallback, current_context, microtask_callback, microtask_data); } Goto(&done); } BIND(&is_promise_resolve_thenable_job); { // Enter the context of the {microtask}. TNode<Context> microtask_context = LoadObjectField<Context>( microtask, PromiseResolveThenableJobTask::kContextOffset); TNode<NativeContext> native_context = LoadNativeContext(microtask_context); PrepareForContext(native_context, &done); const TNode<Object> promise_to_resolve = LoadObjectField( microtask, PromiseResolveThenableJobTask::kPromiseToResolveOffset); const TNode<Object> then = LoadObjectField(microtask, PromiseResolveThenableJobTask::kThenOffset); const TNode<Object> thenable = LoadObjectField( microtask, PromiseResolveThenableJobTask::kThenableOffset); RunPromiseHook(Runtime::kPromiseHookBefore, microtask_context, CAST(promise_to_resolve)); { ScopedExceptionHandler handler(this, &if_exception, &var_exception); CallBuiltin(Builtins::kPromiseResolveThenableJob, native_context, promise_to_resolve, thenable, then); } RunPromiseHook(Runtime::kPromiseHookAfter, microtask_context, CAST(promise_to_resolve)); RewindEnteredContext(saved_entered_context_count); SetCurrentContext(current_context); Goto(&done); } BIND(&is_promise_fulfill_reaction_job); { // Enter the context of the {microtask}. TNode<Context> microtask_context = LoadObjectField<Context>( microtask, PromiseReactionJobTask::kContextOffset); TNode<NativeContext> native_context = LoadNativeContext(microtask_context); PrepareForContext(native_context, &done); const TNode<Object> argument = LoadObjectField(microtask, PromiseReactionJobTask::kArgumentOffset); const TNode<Object> job_handler = LoadObjectField(microtask, PromiseReactionJobTask::kHandlerOffset); const TNode<HeapObject> promise_or_capability = CAST(LoadObjectField( microtask, PromiseReactionJobTask::kPromiseOrCapabilityOffset)); TNode<Object> preserved_embedder_data = LoadObjectField( microtask, PromiseReactionJobTask::kContinuationPreservedEmbedderDataOffset); Label preserved_data_done(this); GotoIf(IsUndefined(preserved_embedder_data), &preserved_data_done); StoreContextElement(native_context, Context::CONTINUATION_PRESERVED_EMBEDDER_DATA_INDEX, preserved_embedder_data); Goto(&preserved_data_done); BIND(&preserved_data_done); // Run the promise before/debug hook if enabled. RunPromiseHook(Runtime::kPromiseHookBefore, microtask_context, promise_or_capability); { ScopedExceptionHandler handler(this, &if_exception, &var_exception); CallBuiltin(Builtins::kPromiseFulfillReactionJob, microtask_context, argument, job_handler, promise_or_capability); } // Run the promise after/debug hook if enabled. RunPromiseHook(Runtime::kPromiseHookAfter, microtask_context, promise_or_capability); Label preserved_data_reset_done(this); GotoIf(IsUndefined(preserved_embedder_data), &preserved_data_reset_done); StoreContextElement(native_context, Context::CONTINUATION_PRESERVED_EMBEDDER_DATA_INDEX, UndefinedConstant()); Goto(&preserved_data_reset_done); BIND(&preserved_data_reset_done); RewindEnteredContext(saved_entered_context_count); SetCurrentContext(current_context); Goto(&done); } BIND(&is_promise_reject_reaction_job); { // Enter the context of the {microtask}. TNode<Context> microtask_context = LoadObjectField<Context>( microtask, PromiseReactionJobTask::kContextOffset); TNode<NativeContext> native_context = LoadNativeContext(microtask_context); PrepareForContext(native_context, &done); const TNode<Object> argument = LoadObjectField(microtask, PromiseReactionJobTask::kArgumentOffset); const TNode<Object> job_handler = LoadObjectField(microtask, PromiseReactionJobTask::kHandlerOffset); const TNode<HeapObject> promise_or_capability = CAST(LoadObjectField( microtask, PromiseReactionJobTask::kPromiseOrCapabilityOffset)); TNode<Object> preserved_embedder_data = LoadObjectField( microtask, PromiseReactionJobTask::kContinuationPreservedEmbedderDataOffset); Label preserved_data_done(this); GotoIf(IsUndefined(preserved_embedder_data), &preserved_data_done); StoreContextElement(native_context, Context::CONTINUATION_PRESERVED_EMBEDDER_DATA_INDEX, preserved_embedder_data); Goto(&preserved_data_done); BIND(&preserved_data_done); // Run the promise before/debug hook if enabled. RunPromiseHook(Runtime::kPromiseHookBefore, microtask_context, promise_or_capability); { ScopedExceptionHandler handler(this, &if_exception, &var_exception); CallBuiltin(Builtins::kPromiseRejectReactionJob, microtask_context, argument, job_handler, promise_or_capability); } // Run the promise after/debug hook if enabled. RunPromiseHook(Runtime::kPromiseHookAfter, microtask_context, promise_or_capability); Label preserved_data_reset_done(this); GotoIf(IsUndefined(preserved_embedder_data), &preserved_data_reset_done); StoreContextElement(native_context, Context::CONTINUATION_PRESERVED_EMBEDDER_DATA_INDEX, UndefinedConstant()); Goto(&preserved_data_reset_done); BIND(&preserved_data_reset_done); RewindEnteredContext(saved_entered_context_count); SetCurrentContext(current_context); Goto(&done); } BIND(&is_unreachable); Unreachable(); BIND(&if_exception); { // Report unhandled exceptions from microtasks. CallRuntime(Runtime::kReportMessageFromMicrotask, current_context, var_exception.value()); RewindEnteredContext(saved_entered_context_count); SetCurrentContext(current_context); Goto(&done); } BIND(&done); } void MicrotaskQueueBuiltinsAssembler::IncrementFinishedMicrotaskCount( TNode<RawPtrT> microtask_queue) { TNode<IntPtrT> count = Load<IntPtrT>( microtask_queue, IntPtrConstant(MicrotaskQueue::kFinishedMicrotaskCountOffset)); TNode<IntPtrT> new_count = IntPtrAdd(count, IntPtrConstant(1)); StoreNoWriteBarrier( MachineType::PointerRepresentation(), microtask_queue, IntPtrConstant(MicrotaskQueue::kFinishedMicrotaskCountOffset), new_count); } TNode<Context> MicrotaskQueueBuiltinsAssembler::GetCurrentContext() { auto ref = ExternalReference::Create(kContextAddress, isolate()); // TODO(delphick): Add a checked cast. For now this is not possible as context // can actually be Smi(0). return TNode<Context>::UncheckedCast(LoadFullTagged(ExternalConstant(ref))); } void MicrotaskQueueBuiltinsAssembler::SetCurrentContext( TNode<Context> context) { auto ref = ExternalReference::Create(kContextAddress, isolate()); StoreFullTaggedNoWriteBarrier(ExternalConstant(ref), context); } TNode<IntPtrT> MicrotaskQueueBuiltinsAssembler::GetEnteredContextCount() { auto ref = ExternalReference::handle_scope_implementer_address(isolate()); TNode<RawPtrT> hsi = Load<RawPtrT>(ExternalConstant(ref)); using ContextStack = DetachableVector<Context>; TNode<IntPtrT> size_offset = IntPtrConstant(HandleScopeImplementer::kEnteredContextsOffset + ContextStack::kSizeOffset); return Load<IntPtrT>(hsi, size_offset); } void MicrotaskQueueBuiltinsAssembler::EnterMicrotaskContext( TNode<Context> native_context) { CSA_ASSERT(this, IsNativeContext(native_context)); auto ref = ExternalReference::handle_scope_implementer_address(isolate()); TNode<RawPtrT> hsi = Load<RawPtrT>(ExternalConstant(ref)); using ContextStack = DetachableVector<Context>; TNode<IntPtrT> capacity_offset = IntPtrConstant(HandleScopeImplementer::kEnteredContextsOffset + ContextStack::kCapacityOffset); TNode<IntPtrT> size_offset = IntPtrConstant(HandleScopeImplementer::kEnteredContextsOffset + ContextStack::kSizeOffset); TNode<IntPtrT> capacity = Load<IntPtrT>(hsi, capacity_offset); TNode<IntPtrT> size = Load<IntPtrT>(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<IntPtrT> data_offset = IntPtrConstant(HandleScopeImplementer::kEnteredContextsOffset + ContextStack::kDataOffset); TNode<RawPtrT> data = Load<RawPtrT>(hsi, data_offset); StoreFullTaggedNoWriteBarrier(data, TimesSystemPointerSize(size), native_context); TNode<IntPtrT> new_size = IntPtrAdd(size, IntPtrConstant(1)); StoreNoWriteBarrier(MachineType::PointerRepresentation(), hsi, size_offset, new_size); using FlagStack = DetachableVector<int8_t>; TNode<IntPtrT> flag_data_offset = IntPtrConstant(HandleScopeImplementer::kIsMicrotaskContextOffset + FlagStack::kDataOffset); TNode<RawPtrT> flag_data = Load<RawPtrT>(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<ExternalReference> 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<IntPtrT> saved_entered_context_count) { auto ref = ExternalReference::handle_scope_implementer_address(isolate()); TNode<RawPtrT> hsi = Load<RawPtrT>(ExternalConstant(ref)); using ContextStack = DetachableVector<Context>; TNode<IntPtrT> size_offset = IntPtrConstant(HandleScopeImplementer::kEnteredContextsOffset + ContextStack::kSizeOffset); #ifdef ENABLE_VERIFY_CSA { TNode<IntPtrT> size = Load<IntPtrT>(hsi, size_offset); CSA_CHECK(this, IntPtrLessThan(IntPtrConstant(0), size)); CSA_CHECK(this, IntPtrLessThanOrEqual(saved_entered_context_count, size)); } #endif StoreNoWriteBarrier(MachineType::PointerRepresentation(), hsi, size_offset, saved_entered_context_count); using FlagStack = DetachableVector<int8_t>; StoreNoWriteBarrier( MachineType::PointerRepresentation(), hsi, IntPtrConstant(HandleScopeImplementer::kIsMicrotaskContextOffset + FlagStack::kSizeOffset), saved_entered_context_count); } void MicrotaskQueueBuiltinsAssembler::RunPromiseHook( Runtime::FunctionId id, TNode<Context> context, TNode<HeapObject> 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<HeapObject> promise = Select<HeapObject>( 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) { auto microtask = Parameter<Microtask>(Descriptor::kMicrotask); auto context = Parameter<Context>(Descriptor::kContext); TNode<NativeContext> native_context = LoadNativeContext(context); TNode<RawPtrT> 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<RawPtrT> ring_buffer = GetMicrotaskRingBuffer(microtask_queue); TNode<IntPtrT> capacity = GetMicrotaskQueueCapacity(microtask_queue); TNode<IntPtrT> size = GetMicrotaskQueueSize(microtask_queue); TNode<IntPtrT> 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<ExternalReference> isolate_constant = ExternalConstant(ExternalReference::isolate_address(isolate())); TNode<ExternalReference> 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<Context> current_context = GetCurrentContext(); auto microtask_queue = UncheckedParameter<RawPtrT>(Descriptor::kMicrotaskQueue); Label loop(this), done(this); Goto(&loop); BIND(&loop); TNode<IntPtrT> size = GetMicrotaskQueueSize(microtask_queue); // Exit if the queue is empty. GotoIf(WordEqual(size, IntPtrConstant(0)), &done); TNode<RawPtrT> ring_buffer = GetMicrotaskRingBuffer(microtask_queue); TNode<IntPtrT> capacity = GetMicrotaskQueueCapacity(microtask_queue); TNode<IntPtrT> start = GetMicrotaskQueueStart(microtask_queue); TNode<IntPtrT> offset = CalculateRingBufferOffset(capacity, start, IntPtrConstant(0)); TNode<RawPtrT> microtask_pointer = Load<RawPtrT>(ring_buffer, offset); TNode<Microtask> microtask = CAST(BitcastWordToTagged(microtask_pointer)); TNode<IntPtrT> new_size = IntPtrSub(size, IntPtrConstant(1)); TNode<IntPtrT> 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