// Copyright 2017 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/wasm/module-compiler.h" #include #include #include #include #include "src/api/api-inl.h" #include "src/base/enum-set.h" #include "src/base/optional.h" #include "src/base/platform/mutex.h" #include "src/base/platform/semaphore.h" #include "src/base/platform/time.h" #include "src/codegen/compiler.h" #include "src/compiler/wasm-compiler.h" #include "src/debug/debug.h" #include "src/handles/global-handles-inl.h" #include "src/logging/counters-scopes.h" #include "src/logging/metrics.h" #include "src/tracing/trace-event.h" #include "src/wasm/code-space-access.h" #include "src/wasm/module-decoder.h" #include "src/wasm/pgo.h" #include "src/wasm/std-object-sizes.h" #include "src/wasm/streaming-decoder.h" #include "src/wasm/wasm-code-manager.h" #include "src/wasm/wasm-engine.h" #include "src/wasm/wasm-import-wrapper-cache.h" #include "src/wasm/wasm-js.h" #include "src/wasm/wasm-limits.h" #include "src/wasm/wasm-objects-inl.h" #include "src/wasm/wasm-result.h" #include "src/wasm/wasm-serialization.h" #define TRACE_COMPILE(...) \ do { \ if (v8_flags.trace_wasm_compiler) PrintF(__VA_ARGS__); \ } while (false) #define TRACE_STREAMING(...) \ do { \ if (v8_flags.trace_wasm_streaming) PrintF(__VA_ARGS__); \ } while (false) #define TRACE_LAZY(...) \ do { \ if (v8_flags.trace_wasm_lazy_compilation) PrintF(__VA_ARGS__); \ } while (false) namespace v8 { namespace internal { namespace wasm { namespace { enum class CompileStrategy : uint8_t { // Compiles functions on first use. In this case, execution will block until // the function's baseline is reached and top tier compilation starts in // background (if applicable). // Lazy compilation can help to reduce startup time and code size at the risk // of blocking execution. kLazy, // Compiles baseline ahead of execution and starts top tier compilation in // background (if applicable). kEager, // Triggers baseline compilation on first use (just like {kLazy}) with the // difference that top tier compilation is started eagerly. // This strategy can help to reduce startup time at the risk of blocking // execution, but only in its early phase (until top tier compilation // finishes). kLazyBaselineEagerTopTier, // Marker for default strategy. kDefault = kEager, }; class CompilationStateImpl; class CompilationUnitBuilder; class V8_NODISCARD BackgroundCompileScope { public: explicit BackgroundCompileScope(std::weak_ptr native_module) : native_module_(native_module.lock()) {} NativeModule* native_module() const { DCHECK(native_module_); return native_module_.get(); } inline CompilationStateImpl* compilation_state() const; bool cancelled() const; private: // Keep the native module alive while in this scope. std::shared_ptr native_module_; }; enum CompilationTier { kBaseline = 0, kTopTier = 1, kNumTiers = kTopTier + 1 }; // A set of work-stealing queues (vectors of units). Each background compile // task owns one of the queues and steals from all others once its own queue // runs empty. class CompilationUnitQueues { public: // Public API for QueueImpl. struct Queue { bool ShouldPublish(int num_processed_units) const; }; explicit CompilationUnitQueues(int num_declared_functions) : num_declared_functions_(num_declared_functions) { // Add one first queue, to add units to. queues_.emplace_back(std::make_unique(0)); #if !defined(__cpp_lib_atomic_value_initialization) || \ __cpp_lib_atomic_value_initialization < 201911L for (auto& atomic_counter : num_units_) { std::atomic_init(&atomic_counter, size_t{0}); } #endif top_tier_compiled_ = std::make_unique[]>(num_declared_functions); #if !defined(__cpp_lib_atomic_value_initialization) || \ __cpp_lib_atomic_value_initialization < 201911L for (int i = 0; i < num_declared_functions; i++) { std::atomic_init(&top_tier_compiled_.get()[i], false); } #endif } Queue* GetQueueForTask(int task_id) { int required_queues = task_id + 1; { base::SharedMutexGuard queues_guard{&queues_mutex_}; if (V8_LIKELY(static_cast(queues_.size()) >= required_queues)) { return queues_[task_id].get(); } } // Otherwise increase the number of queues. base::SharedMutexGuard queues_guard{&queues_mutex_}; int num_queues = static_cast(queues_.size()); while (num_queues < required_queues) { int steal_from = num_queues + 1; queues_.emplace_back(std::make_unique(steal_from)); ++num_queues; } // Update the {publish_limit}s of all queues. // We want background threads to publish regularly (to avoid contention when // they are all publishing at the end). On the other side, each publishing // has some overhead (part of it for synchronizing between threads), so it // should not happen *too* often. Thus aim for 4-8 publishes per thread, but // distribute it such that publishing is likely to happen at different // times. int units_per_thread = num_declared_functions_ / num_queues; int min = std::max(10, units_per_thread / 8); int queue_id = 0; for (auto& queue : queues_) { // Set a limit between {min} and {2*min}, but not smaller than {10}. int limit = min + (min * queue_id / num_queues); queue->publish_limit.store(limit, std::memory_order_relaxed); ++queue_id; } return queues_[task_id].get(); } base::Optional GetNextUnit(Queue* queue, CompilationTier tier) { DCHECK_LT(tier, CompilationTier::kNumTiers); if (auto unit = GetNextUnitOfTier(queue, tier)) { size_t old_units_count = num_units_[tier].fetch_sub(1, std::memory_order_relaxed); DCHECK_LE(1, old_units_count); USE(old_units_count); return unit; } return {}; } void AddUnits(base::Vector baseline_units, base::Vector top_tier_units, const WasmModule* module) { DCHECK_LT(0, baseline_units.size() + top_tier_units.size()); // Add to the individual queues in a round-robin fashion. No special care is // taken to balance them; they will be balanced by work stealing. QueueImpl* queue; { int queue_to_add = next_queue_to_add.load(std::memory_order_relaxed); base::SharedMutexGuard queues_guard{&queues_mutex_}; while (!next_queue_to_add.compare_exchange_weak( queue_to_add, next_task_id(queue_to_add, queues_.size()), std::memory_order_relaxed)) { // Retry with updated {queue_to_add}. } queue = queues_[queue_to_add].get(); } base::MutexGuard guard(&queue->mutex); base::Optional big_units_guard; for (auto pair : {std::make_pair(CompilationTier::kBaseline, baseline_units), std::make_pair(CompilationTier::kTopTier, top_tier_units)}) { int tier = pair.first; base::Vector units = pair.second; if (units.empty()) continue; num_units_[tier].fetch_add(units.size(), std::memory_order_relaxed); for (WasmCompilationUnit unit : units) { size_t func_size = module->functions[unit.func_index()].code.length(); if (func_size <= kBigUnitsLimit) { queue->units[tier].push_back(unit); } else { if (!big_units_guard) { big_units_guard.emplace(&big_units_queue_.mutex); } big_units_queue_.has_units[tier].store(true, std::memory_order_relaxed); big_units_queue_.units[tier].emplace(func_size, unit); } } } } void AddTopTierPriorityUnit(WasmCompilationUnit unit, size_t priority) { base::SharedMutexGuard queues_guard{&queues_mutex_}; // Add to the individual queues in a round-robin fashion. No special care is // taken to balance them; they will be balanced by work stealing. // Priorities should only be seen as a hint here; without balancing, we // might pop a unit with lower priority from one queue while other queues // still hold higher-priority units. // Since updating priorities in a std::priority_queue is difficult, we just // add new units with higher priorities, and use the // {CompilationUnitQueues::top_tier_compiled_} array to discard units for // functions which are already being compiled. int queue_to_add = next_queue_to_add.load(std::memory_order_relaxed); while (!next_queue_to_add.compare_exchange_weak( queue_to_add, next_task_id(queue_to_add, queues_.size()), std::memory_order_relaxed)) { // Retry with updated {queue_to_add}. } { auto* queue = queues_[queue_to_add].get(); base::MutexGuard guard(&queue->mutex); queue->top_tier_priority_units.emplace(priority, unit); num_priority_units_.fetch_add(1, std::memory_order_relaxed); num_units_[CompilationTier::kTopTier].fetch_add( 1, std::memory_order_relaxed); } } // Get the current number of units in the queue for |tier|. This is only a // momentary snapshot, it's not guaranteed that {GetNextUnit} returns a unit // if this method returns non-zero. size_t GetSizeForTier(CompilationTier tier) const { DCHECK_LT(tier, CompilationTier::kNumTiers); return num_units_[tier].load(std::memory_order_relaxed); } void AllowAnotherTopTierJob(uint32_t func_index) { top_tier_compiled_[func_index].store(false, std::memory_order_relaxed); } void AllowAnotherTopTierJobForAllFunctions() { for (int i = 0; i < num_declared_functions_; i++) { AllowAnotherTopTierJob(i); } } size_t EstimateCurrentMemoryConsumption() const; private: // Functions bigger than {kBigUnitsLimit} will be compiled first, in ascending // order of their function body size. static constexpr size_t kBigUnitsLimit = 4096; struct BigUnit { BigUnit(size_t func_size, WasmCompilationUnit unit) : func_size{func_size}, unit(unit) {} size_t func_size; WasmCompilationUnit unit; bool operator<(const BigUnit& other) const { return func_size < other.func_size; } }; struct TopTierPriorityUnit { TopTierPriorityUnit(int priority, WasmCompilationUnit unit) : priority(priority), unit(unit) {} size_t priority; WasmCompilationUnit unit; bool operator<(const TopTierPriorityUnit& other) const { return priority < other.priority; } }; struct BigUnitsQueue { BigUnitsQueue() { #if !defined(__cpp_lib_atomic_value_initialization) || \ __cpp_lib_atomic_value_initialization < 201911L for (auto& atomic : has_units) std::atomic_init(&atomic, false); #endif } mutable base::Mutex mutex; // Can be read concurrently to check whether any elements are in the queue. std::atomic has_units[CompilationTier::kNumTiers]; // Protected by {mutex}: std::priority_queue units[CompilationTier::kNumTiers]; }; struct QueueImpl : public Queue { explicit QueueImpl(int next_steal_task_id) : next_steal_task_id(next_steal_task_id) {} // Number of units after which the task processing this queue should publish // compilation results. Updated (reduced, using relaxed ordering) when new // queues are allocated. If there is only one thread running, we can delay // publishing arbitrarily. std::atomic publish_limit{kMaxInt}; base::Mutex mutex; // All fields below are protected by {mutex}. std::vector units[CompilationTier::kNumTiers]; std::priority_queue top_tier_priority_units; int next_steal_task_id; }; int next_task_id(int task_id, size_t num_queues) const { int next = task_id + 1; return next == static_cast(num_queues) ? 0 : next; } base::Optional GetNextUnitOfTier(Queue* public_queue, int tier) { QueueImpl* queue = static_cast(public_queue); // First check whether there is a priority unit. Execute that first. if (tier == CompilationTier::kTopTier) { if (auto unit = GetTopTierPriorityUnit(queue)) { return unit; } } // Then check whether there is a big unit of that tier. if (auto unit = GetBigUnitOfTier(tier)) return unit; // Finally check whether our own queue has a unit of the wanted tier. If // so, return it, otherwise get the task id to steal from. int steal_task_id; { base::MutexGuard mutex_guard(&queue->mutex); if (!queue->units[tier].empty()) { auto unit = queue->units[tier].back(); queue->units[tier].pop_back(); return unit; } steal_task_id = queue->next_steal_task_id; } // Try to steal from all other queues. If this succeeds, return one of the // stolen units. { base::SharedMutexGuard guard{&queues_mutex_}; for (size_t steal_trials = 0; steal_trials < queues_.size(); ++steal_trials, ++steal_task_id) { if (steal_task_id >= static_cast(queues_.size())) { steal_task_id = 0; } if (auto unit = StealUnitsAndGetFirst(queue, steal_task_id, tier)) { return unit; } } } // If we reach here, we didn't find any unit of the requested tier. return {}; } base::Optional GetBigUnitOfTier(int tier) { // Fast path without locking. if (!big_units_queue_.has_units[tier].load(std::memory_order_relaxed)) { return {}; } base::MutexGuard guard(&big_units_queue_.mutex); if (big_units_queue_.units[tier].empty()) return {}; WasmCompilationUnit unit = big_units_queue_.units[tier].top().unit; big_units_queue_.units[tier].pop(); if (big_units_queue_.units[tier].empty()) { big_units_queue_.has_units[tier].store(false, std::memory_order_relaxed); } return unit; } base::Optional GetTopTierPriorityUnit(QueueImpl* queue) { // Fast path without locking. if (num_priority_units_.load(std::memory_order_relaxed) == 0) { return {}; } int steal_task_id; { base::MutexGuard mutex_guard(&queue->mutex); while (!queue->top_tier_priority_units.empty()) { auto unit = queue->top_tier_priority_units.top().unit; queue->top_tier_priority_units.pop(); num_priority_units_.fetch_sub(1, std::memory_order_relaxed); if (!top_tier_compiled_[unit.func_index()].exchange( true, std::memory_order_relaxed)) { return unit; } num_units_[CompilationTier::kTopTier].fetch_sub( 1, std::memory_order_relaxed); } steal_task_id = queue->next_steal_task_id; } // Try to steal from all other queues. If this succeeds, return one of the // stolen units. { base::SharedMutexGuard guard{&queues_mutex_}; for (size_t steal_trials = 0; steal_trials < queues_.size(); ++steal_trials, ++steal_task_id) { if (steal_task_id >= static_cast(queues_.size())) { steal_task_id = 0; } if (auto unit = StealTopTierPriorityUnit(queue, steal_task_id)) { return unit; } } } return {}; } // Steal units of {wanted_tier} from {steal_from_task_id} to {queue}. Return // first stolen unit (rest put in queue of {task_id}), or {nullopt} if // {steal_from_task_id} had no units of {wanted_tier}. // Hold a shared lock on {queues_mutex_} when calling this method. base::Optional StealUnitsAndGetFirst( QueueImpl* queue, int steal_from_task_id, int wanted_tier) { auto* steal_queue = queues_[steal_from_task_id].get(); // Cannot steal from own queue. if (steal_queue == queue) return {}; std::vector stolen; base::Optional returned_unit; { base::MutexGuard guard(&steal_queue->mutex); auto* steal_from_vector = &steal_queue->units[wanted_tier]; if (steal_from_vector->empty()) return {}; size_t remaining = steal_from_vector->size() / 2; auto steal_begin = steal_from_vector->begin() + remaining; returned_unit = *steal_begin; stolen.assign(steal_begin + 1, steal_from_vector->end()); steal_from_vector->erase(steal_begin, steal_from_vector->end()); } base::MutexGuard guard(&queue->mutex); auto* target_queue = &queue->units[wanted_tier]; target_queue->insert(target_queue->end(), stolen.begin(), stolen.end()); queue->next_steal_task_id = steal_from_task_id + 1; return returned_unit; } // Steal one priority unit from {steal_from_task_id} to {task_id}. Return // stolen unit, or {nullopt} if {steal_from_task_id} had no priority units. // Hold a shared lock on {queues_mutex_} when calling this method. base::Optional StealTopTierPriorityUnit( QueueImpl* queue, int steal_from_task_id) { auto* steal_queue = queues_[steal_from_task_id].get(); // Cannot steal from own queue. if (steal_queue == queue) return {}; base::Optional returned_unit; { base::MutexGuard guard(&steal_queue->mutex); while (true) { if (steal_queue->top_tier_priority_units.empty()) return {}; auto unit = steal_queue->top_tier_priority_units.top().unit; steal_queue->top_tier_priority_units.pop(); num_priority_units_.fetch_sub(1, std::memory_order_relaxed); if (!top_tier_compiled_[unit.func_index()].exchange( true, std::memory_order_relaxed)) { returned_unit = unit; break; } num_units_[CompilationTier::kTopTier].fetch_sub( 1, std::memory_order_relaxed); } } base::MutexGuard guard(&queue->mutex); queue->next_steal_task_id = steal_from_task_id + 1; return returned_unit; } // {queues_mutex_} protectes {queues_}; mutable base::SharedMutex queues_mutex_; std::vector> queues_; const int num_declared_functions_; BigUnitsQueue big_units_queue_; std::atomic num_units_[CompilationTier::kNumTiers]; std::atomic num_priority_units_{0}; std::unique_ptr[]> top_tier_compiled_; std::atomic next_queue_to_add{0}; }; size_t CompilationUnitQueues::EstimateCurrentMemoryConsumption() const { UPDATE_WHEN_CLASS_CHANGES(CompilationUnitQueues, 248); UPDATE_WHEN_CLASS_CHANGES(QueueImpl, 144); UPDATE_WHEN_CLASS_CHANGES(BigUnitsQueue, 120); // Not including sizeof(CompilationUnitQueues) because that's included in // sizeof(CompilationStateImpl). size_t result = 0; { base::SharedMutexGuard lock(&queues_mutex_); result += ContentSize(queues_) + queues_.size() * sizeof(QueueImpl); for (const auto& q : queues_) { result += ContentSize(*q->units); result += q->top_tier_priority_units.size() * sizeof(TopTierPriorityUnit); } } { base::MutexGuard lock(&big_units_queue_.mutex); result += big_units_queue_.units[0].size() * sizeof(BigUnit); result += big_units_queue_.units[1].size() * sizeof(BigUnit); } // For {top_tier_compiled_}. result += sizeof(std::atomic) * num_declared_functions_; return result; } bool CompilationUnitQueues::Queue::ShouldPublish( int num_processed_units) const { auto* queue = static_cast(this); return num_processed_units >= queue->publish_limit.load(std::memory_order_relaxed); } // The {CompilationStateImpl} keeps track of the compilation state of the // owning NativeModule, i.e. which functions are left to be compiled. // It contains a task manager to allow parallel and asynchronous background // compilation of functions. // Its public interface {CompilationState} lives in compilation-environment.h. class CompilationStateImpl { public: CompilationStateImpl(const std::shared_ptr& native_module, std::shared_ptr async_counters, DynamicTiering dynamic_tiering); ~CompilationStateImpl() { if (js_to_wasm_wrapper_job_ && js_to_wasm_wrapper_job_->IsValid()) js_to_wasm_wrapper_job_->CancelAndDetach(); if (baseline_compile_job_->IsValid()) baseline_compile_job_->CancelAndDetach(); if (top_tier_compile_job_->IsValid()) top_tier_compile_job_->CancelAndDetach(); } // Call right after the constructor, after the {compilation_state_} field in // the {NativeModule} has been initialized. void InitCompileJob(); // {kCancelUnconditionally}: Cancel all compilation. // {kCancelInitialCompilation}: Cancel all compilation if initial (baseline) // compilation is not finished yet. enum CancellationPolicy { kCancelUnconditionally, kCancelInitialCompilation }; void CancelCompilation(CancellationPolicy); bool cancelled() const; // Apply a compilation hint to the initial compilation progress, updating all // internal fields accordingly. void ApplyCompilationHintToInitialProgress(const WasmCompilationHint& hint, size_t hint_idx); // Use PGO information to choose a better initial compilation progress // (tiering decisions). void ApplyPgoInfoToInitialProgress(ProfileInformation* pgo_info); // Apply PGO information to a fully initialized compilation state. Also // trigger compilation as needed. void ApplyPgoInfoLate(ProfileInformation* pgo_info); // Initialize compilation progress. Set compilation tiers to expect for // baseline and top tier compilation. Must be set before // {CommitCompilationUnits} is invoked which triggers background compilation. void InitializeCompilationProgress(int num_import_wrappers, int num_export_wrappers, ProfileInformation* pgo_info); void InitializeCompilationProgressAfterDeserialization( base::Vector lazy_functions, base::Vector eager_functions); // Initializes compilation units based on the information encoded in the // {compilation_progress_}. void InitializeCompilationUnits( std::unique_ptr builder); // Adds compilation units for another function to the // {CompilationUnitBuilder}. This function is the streaming compilation // equivalent to {InitializeCompilationUnits}. void AddCompilationUnit(CompilationUnitBuilder* builder, int func_index); // Add the callback to be called on compilation events. Needs to be // set before {CommitCompilationUnits} is run to ensure that it receives all // events. The callback object must support being deleted from any thread. void AddCallback(std::unique_ptr callback); // Inserts new functions to compile and kicks off compilation. void CommitCompilationUnits( base::Vector baseline_units, base::Vector top_tier_units, base::Vector> js_to_wasm_wrapper_units); void CommitTopTierCompilationUnit(WasmCompilationUnit); void AddTopTierPriorityCompilationUnit(WasmCompilationUnit, size_t); CompilationUnitQueues::Queue* GetQueueForCompileTask(int task_id); base::Optional GetNextCompilationUnit( CompilationUnitQueues::Queue*, CompilationTier tier); std::shared_ptr GetJSToWasmWrapperCompilationUnit(size_t index); void FinalizeJSToWasmWrappers(Isolate* isolate, const WasmModule* module); void OnFinishedUnits(base::Vector); void OnFinishedJSToWasmWrapperUnits(); void OnCompilationStopped(WasmFeatures detected); void PublishDetectedFeatures(Isolate*); void SchedulePublishCompilationResults( std::vector> unpublished_code, CompilationTier tier); size_t NumOutstandingCompilations(CompilationTier tier) const; void SetError(); void WaitForCompilationEvent(CompilationEvent event); void TierUpAllFunctions(); void AllowAnotherTopTierJob(uint32_t func_index) { compilation_unit_queues_.AllowAnotherTopTierJob(func_index); } void AllowAnotherTopTierJobForAllFunctions() { compilation_unit_queues_.AllowAnotherTopTierJobForAllFunctions(); } bool failed() const { return compile_failed_.load(std::memory_order_relaxed); } bool baseline_compilation_finished() const { base::MutexGuard guard(&callbacks_mutex_); return outstanding_baseline_units_ == 0 && !has_outstanding_export_wrappers_; } DynamicTiering dynamic_tiering() const { return dynamic_tiering_; } Counters* counters() const { return async_counters_.get(); } void SetWireBytesStorage( std::shared_ptr wire_bytes_storage) { base::MutexGuard guard(&mutex_); wire_bytes_storage_ = std::move(wire_bytes_storage); } std::shared_ptr GetWireBytesStorage() const { base::MutexGuard guard(&mutex_); DCHECK_NOT_NULL(wire_bytes_storage_); return wire_bytes_storage_; } void set_compilation_id(int compilation_id) { DCHECK_EQ(compilation_id_, kInvalidCompilationID); compilation_id_ = compilation_id; } std::weak_ptr const native_module_weak() const { return native_module_weak_; } size_t EstimateCurrentMemoryConsumption() const; private: void AddCompilationUnitInternal(CompilationUnitBuilder* builder, int function_index, uint8_t function_progress); // Trigger callbacks according to the internal counters below // (outstanding_...). // Hold the {callbacks_mutex_} when calling this method. void TriggerCallbacks(); void PublishCompilationResults( std::vector> unpublished_code); void PublishCode(base::Vector> codes); NativeModule* const native_module_; std::weak_ptr const native_module_weak_; const std::shared_ptr async_counters_; // Compilation error, atomically updated. This flag can be updated and read // using relaxed semantics. std::atomic compile_failed_{false}; // True if compilation was cancelled and worker threads should return. This // flag can be updated and read using relaxed semantics. std::atomic compile_cancelled_{false}; CompilationUnitQueues compilation_unit_queues_; // Wrapper compilation units are stored in shared_ptrs so that they are kept // alive by the tasks even if the NativeModule dies. std::vector> js_to_wasm_wrapper_units_; // Cache the dynamic tiering configuration to be consistent for the whole // compilation. const DynamicTiering dynamic_tiering_; // This mutex protects all information of this {CompilationStateImpl} which is // being accessed concurrently. mutable base::Mutex mutex_; // The compile job handles, initialized right after construction of // {CompilationStateImpl}. std::unique_ptr js_to_wasm_wrapper_job_; std::unique_ptr baseline_compile_job_; std::unique_ptr top_tier_compile_job_; // The compilation id to identify trace events linked to this compilation. static constexpr int kInvalidCompilationID = -1; int compilation_id_ = kInvalidCompilationID; ////////////////////////////////////////////////////////////////////////////// // Protected by {mutex_}: // Features detected to be used in this module. Features can be detected // as a module is being compiled. WasmFeatures detected_features_ = WasmFeatures::None(); // Abstraction over the storage of the wire bytes. Held in a shared_ptr so // that background compilation jobs can keep the storage alive while // compiling. std::shared_ptr wire_bytes_storage_; // End of fields protected by {mutex_}. ////////////////////////////////////////////////////////////////////////////// // This mutex protects the callbacks vector, and the counters used to // determine which callbacks to call. The counters plus the callbacks // themselves need to be synchronized to ensure correct order of events. mutable base::Mutex callbacks_mutex_; ////////////////////////////////////////////////////////////////////////////// // Protected by {callbacks_mutex_}: // Callbacks to be called on compilation events. std::vector> callbacks_; // Events that already happened. base::EnumSet finished_events_; int outstanding_baseline_units_ = 0; bool has_outstanding_export_wrappers_ = false; // The amount of generated top tier code since the last // {kFinishedCompilationChunk} event. size_t bytes_since_last_chunk_ = 0; std::vector compilation_progress_; // End of fields protected by {callbacks_mutex_}. ////////////////////////////////////////////////////////////////////////////// struct PublishState { // {mutex_} protects {publish_queue_} and {publisher_running_}. base::Mutex mutex_; std::vector> publish_queue_; bool publisher_running_ = false; }; PublishState publish_state_[CompilationTier::kNumTiers]; // Encoding of fields in the {compilation_progress_} vector. using RequiredBaselineTierField = base::BitField8; using RequiredTopTierField = base::BitField8; using ReachedTierField = base::BitField8; }; CompilationStateImpl* Impl(CompilationState* compilation_state) { return reinterpret_cast(compilation_state); } const CompilationStateImpl* Impl(const CompilationState* compilation_state) { return reinterpret_cast(compilation_state); } CompilationStateImpl* BackgroundCompileScope::compilation_state() const { DCHECK(native_module_); return Impl(native_module_->compilation_state()); } size_t CompilationStateImpl::EstimateCurrentMemoryConsumption() const { UPDATE_WHEN_CLASS_CHANGES(CompilationStateImpl, 704); UPDATE_WHEN_CLASS_CHANGES(JSToWasmWrapperCompilationUnit, 40); size_t result = sizeof(CompilationStateImpl); result += compilation_unit_queues_.EstimateCurrentMemoryConsumption(); result += ContentSize(js_to_wasm_wrapper_units_); result += js_to_wasm_wrapper_units_.size() * (sizeof(JSToWasmWrapperCompilationUnit) + sizeof(TurbofanCompilationJob)); { base::MutexGuard lock(&callbacks_mutex_); result += ContentSize(callbacks_); // Concrete subclasses of CompilationEventCallback will be bigger, but we // can't know that here. result += callbacks_.size() * sizeof(CompilationEventCallback); result += ContentSize(compilation_progress_); } if (v8_flags.trace_wasm_offheap_memory) { PrintF("CompilationStateImpl: %zu\n", result); } return result; } bool BackgroundCompileScope::cancelled() const { return native_module_ == nullptr || Impl(native_module_->compilation_state())->cancelled(); } void UpdateFeatureUseCounts(Isolate* isolate, WasmFeatures detected) { using Feature = v8::Isolate::UseCounterFeature; constexpr static std::pair kUseCounters[] = { {kFeature_reftypes, Feature::kWasmRefTypes}, {kFeature_simd, Feature::kWasmSimdOpcodes}, {kFeature_threads, Feature::kWasmThreadOpcodes}, {kFeature_eh, Feature::kWasmExceptionHandling}, {kFeature_memory64, Feature::kWasmMemory64}, {kFeature_multi_memory, Feature::kWasmMultiMemory}, {kFeature_gc, Feature::kWasmGC}}; for (auto& feature : kUseCounters) { if (detected.contains(feature.first)) isolate->CountUsage(feature.second); } } } // namespace ////////////////////////////////////////////////////// // PIMPL implementation of {CompilationState}. CompilationState::~CompilationState() { Impl(this)->~CompilationStateImpl(); } void CompilationState::InitCompileJob() { Impl(this)->InitCompileJob(); } void CompilationState::CancelCompilation() { Impl(this)->CancelCompilation(CompilationStateImpl::kCancelUnconditionally); } void CompilationState::CancelInitialCompilation() { Impl(this)->CancelCompilation( CompilationStateImpl::kCancelInitialCompilation); } void CompilationState::SetError() { Impl(this)->SetError(); } void CompilationState::SetWireBytesStorage( std::shared_ptr wire_bytes_storage) { Impl(this)->SetWireBytesStorage(std::move(wire_bytes_storage)); } std::shared_ptr CompilationState::GetWireBytesStorage() const { return Impl(this)->GetWireBytesStorage(); } void CompilationState::AddCallback( std::unique_ptr callback) { return Impl(this)->AddCallback(std::move(callback)); } void CompilationState::TierUpAllFunctions() { Impl(this)->TierUpAllFunctions(); } void CompilationState::AllowAnotherTopTierJob(uint32_t func_index) { Impl(this)->AllowAnotherTopTierJob(func_index); } void CompilationState::AllowAnotherTopTierJobForAllFunctions() { Impl(this)->AllowAnotherTopTierJobForAllFunctions(); } void CompilationState::InitializeAfterDeserialization( base::Vector lazy_functions, base::Vector eager_functions) { Impl(this)->InitializeCompilationProgressAfterDeserialization( lazy_functions, eager_functions); } bool CompilationState::failed() const { return Impl(this)->failed(); } bool CompilationState::baseline_compilation_finished() const { return Impl(this)->baseline_compilation_finished(); } void CompilationState::set_compilation_id(int compilation_id) { Impl(this)->set_compilation_id(compilation_id); } DynamicTiering CompilationState::dynamic_tiering() const { return Impl(this)->dynamic_tiering(); } size_t CompilationState::EstimateCurrentMemoryConsumption() const { return Impl(this)->EstimateCurrentMemoryConsumption(); } // static std::unique_ptr CompilationState::New( const std::shared_ptr& native_module, std::shared_ptr async_counters, DynamicTiering dynamic_tiering) { return std::unique_ptr(reinterpret_cast( new CompilationStateImpl(std::move(native_module), std::move(async_counters), dynamic_tiering))); } // End of PIMPL implementation of {CompilationState}. ////////////////////////////////////////////////////// namespace { ExecutionTier ApplyHintToExecutionTier(WasmCompilationHintTier hint, ExecutionTier default_tier) { switch (hint) { case WasmCompilationHintTier::kDefault: return default_tier; case WasmCompilationHintTier::kBaseline: return ExecutionTier::kLiftoff; case WasmCompilationHintTier::kOptimized: return ExecutionTier::kTurbofan; } UNREACHABLE(); } const WasmCompilationHint* GetCompilationHint(const WasmModule* module, uint32_t func_index) { DCHECK_LE(module->num_imported_functions, func_index); uint32_t hint_index = declared_function_index(module, func_index); const std::vector& compilation_hints = module->compilation_hints; if (hint_index < compilation_hints.size()) { return &compilation_hints[hint_index]; } return nullptr; } CompileStrategy GetCompileStrategy(const WasmModule* module, WasmFeatures enabled_features, uint32_t func_index, bool lazy_module) { if (lazy_module) return CompileStrategy::kLazy; if (!enabled_features.has_compilation_hints()) { return CompileStrategy::kDefault; } auto* hint = GetCompilationHint(module, func_index); if (hint == nullptr) return CompileStrategy::kDefault; switch (hint->strategy) { case WasmCompilationHintStrategy::kLazy: return CompileStrategy::kLazy; case WasmCompilationHintStrategy::kEager: return CompileStrategy::kEager; case WasmCompilationHintStrategy::kLazyBaselineEagerTopTier: return CompileStrategy::kLazyBaselineEagerTopTier; case WasmCompilationHintStrategy::kDefault: return CompileStrategy::kDefault; } } struct ExecutionTierPair { ExecutionTier baseline_tier; ExecutionTier top_tier; }; // Pass the debug state as a separate parameter to avoid data races: the debug // state may change between its use here and its use at the call site. To have // a consistent view on the debug state, the caller reads the debug state once // and then passes it to this function. ExecutionTierPair GetDefaultTiersPerModule(NativeModule* native_module, DynamicTiering dynamic_tiering, DebugState is_in_debug_state, bool lazy_module) { const WasmModule* module = native_module->module(); if (is_asmjs_module(module)) { return {ExecutionTier::kTurbofan, ExecutionTier::kTurbofan}; } if (lazy_module) { return {ExecutionTier::kNone, ExecutionTier::kNone}; } if (is_in_debug_state) { return {ExecutionTier::kLiftoff, ExecutionTier::kLiftoff}; } ExecutionTier baseline_tier = v8_flags.liftoff ? ExecutionTier::kLiftoff : ExecutionTier::kTurbofan; bool eager_tier_up = !dynamic_tiering && v8_flags.wasm_tier_up; ExecutionTier top_tier = eager_tier_up ? ExecutionTier::kTurbofan : baseline_tier; return {baseline_tier, top_tier}; } ExecutionTierPair GetLazyCompilationTiers(NativeModule* native_module, uint32_t func_index, DebugState is_in_debug_state) { DynamicTiering dynamic_tiering = Impl(native_module->compilation_state())->dynamic_tiering(); // For lazy compilation, get the tiers we would use if lazy compilation is // disabled. constexpr bool kNotLazy = false; ExecutionTierPair tiers = GetDefaultTiersPerModule( native_module, dynamic_tiering, is_in_debug_state, kNotLazy); // If we are in debug mode, we ignore compilation hints. if (is_in_debug_state) return tiers; // Check if compilation hints override default tiering behaviour. if (native_module->enabled_features().has_compilation_hints()) { if (auto* hint = GetCompilationHint(native_module->module(), func_index)) { tiers.baseline_tier = ApplyHintToExecutionTier(hint->baseline_tier, tiers.baseline_tier); tiers.top_tier = ApplyHintToExecutionTier(hint->top_tier, tiers.top_tier); } } if (V8_UNLIKELY(v8_flags.wasm_tier_up_filter >= 0 && func_index != static_cast(v8_flags.wasm_tier_up_filter))) { tiers.top_tier = tiers.baseline_tier; } // Correct top tier if necessary. static_assert(ExecutionTier::kLiftoff < ExecutionTier::kTurbofan, "Assume an order on execution tiers"); if (tiers.baseline_tier > tiers.top_tier) { tiers.top_tier = tiers.baseline_tier; } return tiers; } // The {CompilationUnitBuilder} builds compilation units and stores them in an // internal buffer. The buffer is moved into the working queue of the // {CompilationStateImpl} when {Commit} is called. class CompilationUnitBuilder { public: explicit CompilationUnitBuilder(NativeModule* native_module) : native_module_(native_module) {} void AddImportUnit(uint32_t func_index) { DCHECK_GT(native_module_->module()->num_imported_functions, func_index); baseline_units_.emplace_back(func_index, ExecutionTier::kNone, kNotForDebugging); } void AddJSToWasmWrapperUnit( std::shared_ptr unit) { js_to_wasm_wrapper_units_.emplace_back(std::move(unit)); } void AddBaselineUnit(int func_index, ExecutionTier tier) { baseline_units_.emplace_back(func_index, tier, kNotForDebugging); } void AddTopTierUnit(int func_index, ExecutionTier tier) { tiering_units_.emplace_back(func_index, tier, kNotForDebugging); } void Commit() { if (baseline_units_.empty() && tiering_units_.empty() && js_to_wasm_wrapper_units_.empty()) { return; } compilation_state()->CommitCompilationUnits( base::VectorOf(baseline_units_), base::VectorOf(tiering_units_), base::VectorOf(js_to_wasm_wrapper_units_)); Clear(); } void Clear() { baseline_units_.clear(); tiering_units_.clear(); js_to_wasm_wrapper_units_.clear(); } const WasmModule* module() { return native_module_->module(); } private: CompilationStateImpl* compilation_state() const { return Impl(native_module_->compilation_state()); } NativeModule* const native_module_; std::vector baseline_units_; std::vector tiering_units_; std::vector> js_to_wasm_wrapper_units_; }; DecodeResult ValidateSingleFunction(const WasmModule* module, int func_index, base::Vector code, WasmFeatures enabled_features) { // Sometimes functions get validated unpredictably in the background, for // debugging or when inlining one function into another. We check here if that // is the case, and exit early if so. if (module->function_was_validated(func_index)) return {}; const WasmFunction* func = &module->functions[func_index]; FunctionBody body{func->sig, func->code.offset(), code.begin(), code.end()}; WasmFeatures detected_features; DecodeResult result = ValidateFunctionBody(enabled_features, module, &detected_features, body); if (result.ok()) module->set_function_validated(func_index); return result; } enum OnlyLazyFunctions : bool { kAllFunctions = false, kOnlyLazyFunctions = true, }; bool IsLazyModule(const WasmModule* module) { return v8_flags.wasm_lazy_compilation || (v8_flags.asm_wasm_lazy_compilation && is_asmjs_module(module)); } class CompileLazyTimingScope { public: CompileLazyTimingScope(Counters* counters, NativeModule* native_module) : counters_(counters), native_module_(native_module) { timer_.Start(); } ~CompileLazyTimingScope() { base::TimeDelta elapsed = timer_.Elapsed(); native_module_->AddLazyCompilationTimeSample(elapsed.InMicroseconds()); counters_->wasm_lazy_compile_time()->AddTimedSample(elapsed); } private: Counters* counters_; NativeModule* native_module_; base::ElapsedTimer timer_; }; } // namespace bool CompileLazy(Isolate* isolate, Tagged instance, int func_index) { DisallowGarbageCollection no_gc; Tagged module_object = instance->module_object(); NativeModule* native_module = module_object->native_module(); Counters* counters = isolate->counters(); // Put the timer scope around everything, including the {CodeSpaceWriteScope} // and its destruction, to measure complete overhead (apart from the runtime // function itself, which has constant overhead). base::Optional lazy_compile_time_scope; if (base::TimeTicks::IsHighResolution()) { lazy_compile_time_scope.emplace(counters, native_module); } DCHECK(!native_module->lazy_compile_frozen()); TRACE_LAZY("Compiling wasm-function#%d.\n", func_index); CompilationStateImpl* compilation_state = Impl(native_module->compilation_state()); DebugState is_in_debug_state = native_module->IsInDebugState(); ExecutionTierPair tiers = GetLazyCompilationTiers(native_module, func_index, is_in_debug_state); DCHECK_LE(native_module->num_imported_functions(), func_index); DCHECK_LT(func_index, native_module->num_functions()); WasmCompilationUnit baseline_unit{ func_index, tiers.baseline_tier, is_in_debug_state ? kForDebugging : kNotForDebugging}; CompilationEnv env = native_module->CreateCompilationEnv(); WasmFeatures detected_features; WasmCompilationResult result = baseline_unit.ExecuteCompilation( &env, compilation_state->GetWireBytesStorage().get(), counters, &detected_features); compilation_state->OnCompilationStopped(detected_features); // During lazy compilation, we can only get compilation errors when // {--wasm-lazy-validation} is enabled. Otherwise, the module was fully // verified before starting its execution. CHECK_IMPLIES(result.failed(), v8_flags.wasm_lazy_validation); if (result.failed()) { return false; } WasmCodeRefScope code_ref_scope; WasmCode* code = native_module->PublishCode(native_module->AddCompiledCode(result)); DCHECK_EQ(func_index, code->index()); if (V8_UNLIKELY(native_module->log_code())) { GetWasmEngine()->LogCode(base::VectorOf(&code, 1)); // Log the code immediately in the current isolate. GetWasmEngine()->LogOutstandingCodesForIsolate(isolate); } counters->wasm_lazily_compiled_functions()->Increment(); const WasmModule* module = native_module->module(); const bool lazy_module = IsLazyModule(module); if (GetCompileStrategy(module, native_module->enabled_features(), func_index, lazy_module) == CompileStrategy::kLazy && tiers.baseline_tier < tiers.top_tier) { WasmCompilationUnit tiering_unit{func_index, tiers.top_tier, kNotForDebugging}; compilation_state->CommitTopTierCompilationUnit(tiering_unit); } return true; } void ThrowLazyCompilationError(Isolate* isolate, const NativeModule* native_module, int func_index) { const WasmModule* module = native_module->module(); CompilationStateImpl* compilation_state = Impl(native_module->compilation_state()); const WasmFunction* func = &module->functions[func_index]; base::Vector code = compilation_state->GetWireBytesStorage()->GetCode(func->code); auto enabled_features = native_module->enabled_features(); DecodeResult decode_result = ValidateSingleFunction(module, func_index, code, enabled_features); CHECK(decode_result.failed()); wasm::ErrorThrower thrower(isolate, nullptr); thrower.CompileFailed(GetWasmErrorWithName(native_module->wire_bytes(), func_index, module, std::move(decode_result).error())); } class TransitiveTypeFeedbackProcessor { public: static void Process(Tagged instance, int func_index) { TransitiveTypeFeedbackProcessor{instance, func_index}.ProcessQueue(); } private: TransitiveTypeFeedbackProcessor(Tagged instance, int func_index) : instance_(instance), module_(instance->module()), mutex_guard(&module_->type_feedback.mutex), feedback_for_function_(module_->type_feedback.feedback_for_function) { queue_.insert(func_index); } ~TransitiveTypeFeedbackProcessor() { DCHECK(queue_.empty()); } void ProcessQueue() { while (!queue_.empty()) { auto next = queue_.cbegin(); ProcessFunction(*next); queue_.erase(next); } } void ProcessFunction(int func_index); void EnqueueCallees(const std::vector& feedback) { for (size_t i = 0; i < feedback.size(); i++) { const CallSiteFeedback& csf = feedback[i]; for (int j = 0; j < csf.num_cases(); j++) { int func = csf.function_index(j); // Don't spend time on calls that have never been executed. if (csf.call_count(j) == 0) continue; // Don't recompute feedback that has already been processed. auto existing = feedback_for_function_.find(func); if (existing != feedback_for_function_.end() && existing->second.feedback_vector.size() > 0) { continue; } queue_.insert(func); } } } DisallowGarbageCollection no_gc_scope_; WasmInstanceObject instance_; const WasmModule* const module_; // TODO(jkummerow): Check if it makes a difference to apply any updates // as a single batch at the end. base::SharedMutexGuard mutex_guard; std::unordered_map& feedback_for_function_; std::set queue_; }; class FeedbackMaker { public: FeedbackMaker(Tagged instance, int func_index, int num_calls) : instance_(instance), num_imported_functions_( static_cast(instance->module()->num_imported_functions)), func_index_(func_index) { result_.reserve(num_calls); } void AddCandidate(Tagged maybe_function, int count) { if (!IsWasmInternalFunction(maybe_function)) return; Tagged function = WasmInternalFunction::cast(maybe_function); if (function->ref() != instance_) { // Not a wasm function, or not a function declared in this instance. return; } if (function->function_index() < num_imported_functions_) return; AddCall(function->function_index(), count); } void AddCall(int target, int count) { // Keep the cache sorted (using insertion-sort), highest count first. int insertion_index = 0; while (insertion_index < cache_usage_ && counts_cache_[insertion_index] >= count) { insertion_index++; } for (int shifted_index = cache_usage_ - 1; shifted_index >= insertion_index; shifted_index--) { targets_cache_[shifted_index + 1] = targets_cache_[shifted_index]; counts_cache_[shifted_index + 1] = counts_cache_[shifted_index]; } targets_cache_[insertion_index] = target; counts_cache_[insertion_index] = count; cache_usage_++; } void FinalizeCall() { if (cache_usage_ == 0) { result_.emplace_back(); } else if (cache_usage_ == 1) { if (v8_flags.trace_wasm_inlining) { PrintF("[function %d: call_ref #%zu inlineable (monomorphic)]\n", func_index_, result_.size()); } result_.emplace_back(targets_cache_[0], counts_cache_[0]); } else { if (v8_flags.trace_wasm_inlining) { PrintF("[function %d: call_ref #%zu inlineable (polymorphic %d)]\n", func_index_, result_.size(), cache_usage_); } CallSiteFeedback::PolymorphicCase* polymorphic = new CallSiteFeedback::PolymorphicCase[cache_usage_]; for (int i = 0; i < cache_usage_; i++) { polymorphic[i].function_index = targets_cache_[i]; polymorphic[i].absolute_call_frequency = counts_cache_[i]; } result_.emplace_back(polymorphic, cache_usage_); } cache_usage_ = 0; } // {GetResult} can only be called on a r-value reference to make it more // obvious at call sites that {this} should not be used after this operation. std::vector&& GetResult() && { return std::move(result_); } private: const WasmInstanceObject instance_; std::vector result_; const int num_imported_functions_; const int func_index_; int cache_usage_{0}; int targets_cache_[kMaxPolymorphism]; int counts_cache_[kMaxPolymorphism]; }; void TransitiveTypeFeedbackProcessor::ProcessFunction(int func_index) { int which_vector = declared_function_index(module_, func_index); Tagged maybe_feedback = instance_->feedback_vectors()->get(which_vector); if (!IsFixedArray(maybe_feedback)) return; Tagged feedback = FixedArray::cast(maybe_feedback); base::Vector call_direct_targets = module_->type_feedback.feedback_for_function[func_index] .call_targets.as_vector(); int checked_feedback_length = feedback->length(); DCHECK_EQ(checked_feedback_length, call_direct_targets.size() * 2); FeedbackMaker fm(instance_, func_index, checked_feedback_length / 2); for (int i = 0; i < checked_feedback_length; i += 2) { Tagged value = feedback->get(i); if (IsWasmInternalFunction(value)) { // Monomorphic. int count = Smi::cast(feedback->get(i + 1)).value(); fm.AddCandidate(value, count); } else if (IsFixedArray(value)) { // Polymorphic. Tagged polymorphic = FixedArray::cast(value); int checked_polymorphic_length = polymorphic->length(); DCHECK_LE(checked_polymorphic_length, 2 * kMaxPolymorphism); for (int j = 0; j < checked_polymorphic_length; j += 2) { Tagged function = polymorphic->get(j); int count = Smi::cast(polymorphic->get(j + 1)).value(); fm.AddCandidate(function, count); } } else if (IsSmi(value)) { // Uninitialized, or a direct call collecting call count. uint32_t target = call_direct_targets[i / 2]; if (target != FunctionTypeFeedback::kNonDirectCall) { int count = Smi::cast(value).value(); fm.AddCall(static_cast(target), count); } else if (v8_flags.trace_wasm_inlining) { PrintF("[function %d: call #%d: uninitialized]\n", func_index, i / 2); } } else if (v8_flags.trace_wasm_inlining) { if (value == ReadOnlyRoots(instance_->GetIsolate()).megamorphic_symbol()) { PrintF("[function %d: call #%d: megamorphic]\n", func_index, i / 2); } } fm.FinalizeCall(); } std::vector result = std::move(fm).GetResult(); EnqueueCallees(result); feedback_for_function_[func_index].feedback_vector = std::move(result); } void TriggerTierUp(Tagged instance, int func_index) { NativeModule* native_module = instance->module_object()->native_module(); CompilationStateImpl* compilation_state = Impl(native_module->compilation_state()); WasmCompilationUnit tiering_unit{func_index, ExecutionTier::kTurbofan, kNotForDebugging}; const WasmModule* module = native_module->module(); int priority; { base::SharedMutexGuard mutex_guard( &module->type_feedback.mutex); int array_index = wasm::declared_function_index(instance->module(), func_index); instance->tiering_budget_array()[array_index] = v8_flags.wasm_tiering_budget; int& stored_priority = module->type_feedback.feedback_for_function[func_index].tierup_priority; if (stored_priority < kMaxInt) ++stored_priority; priority = stored_priority; } // Only create a compilation unit if this is the first time we detect this // function as hot (priority == 1), or if the priority increased // significantly. The latter is assumed to be the case if the priority // increased at least to four, and is a power of two. if (priority == 2 || !base::bits::IsPowerOfTwo(priority)) return; // Before adding the tier-up unit or increasing priority, do process type // feedback for best code generation. if (native_module->enabled_features().has_inlining()) { // TODO(jkummerow): we could have collisions here if different instances // of the same module have collected different feedback. If that ever // becomes a problem, figure out a solution. TransitiveTypeFeedbackProcessor::Process(instance, func_index); } compilation_state->AddTopTierPriorityCompilationUnit(tiering_unit, priority); } void TierUpNowForTesting(Isolate* isolate, Tagged instance, int func_index) { NativeModule* native_module = instance->module_object()->native_module(); if (native_module->enabled_features().has_inlining()) { TransitiveTypeFeedbackProcessor::Process(instance, func_index); } wasm::GetWasmEngine()->CompileFunction(isolate->counters(), native_module, func_index, wasm::ExecutionTier::kTurbofan); CHECK(!native_module->compilation_state()->failed()); } namespace { void RecordStats(Tagged code, Counters* counters) { if (!code->has_instruction_stream()) return; counters->wasm_generated_code_size()->Increment(code->body_size()); counters->wasm_reloc_size()->Increment(code->relocation_size()); } enum CompilationExecutionResult : int8_t { kNoMoreUnits, kYield }; namespace { const char* GetCompilationEventName(const WasmCompilationUnit& unit, const CompilationEnv& env) { ExecutionTier tier = unit.tier(); if (tier == ExecutionTier::kLiftoff) { return "wasm.BaselineCompilation"; } if (tier == ExecutionTier::kTurbofan) { return "wasm.TopTierCompilation"; } if (unit.func_index() < static_cast(env.module->num_imported_functions)) { return "wasm.WasmToJSWrapperCompilation"; } return "wasm.OtherCompilation"; } } // namespace constexpr uint8_t kMainTaskId = 0; // Run by the {BackgroundCompileJob} (on any thread). CompilationExecutionResult ExecuteCompilationUnits( std::weak_ptr native_module, Counters* counters, JobDelegate* delegate, CompilationTier tier) { TRACE_EVENT0("v8.wasm", "wasm.ExecuteCompilationUnits"); // These fields are initialized in a {BackgroundCompileScope} before // starting compilation. base::Optional env; std::shared_ptr wire_bytes; std::shared_ptr module; // Task 0 is any main thread (there might be multiple from multiple isolates), // worker threads start at 1 (thus the "+ 1"). static_assert(kMainTaskId == 0); int task_id = delegate ? (int{delegate->GetTaskId()} + 1) : kMainTaskId; DCHECK_LE(0, task_id); CompilationUnitQueues::Queue* queue; base::Optional unit; WasmFeatures global_detected_features = WasmFeatures::None(); // Preparation (synchronized): Initialize the fields above and get the first // compilation unit. { BackgroundCompileScope compile_scope(native_module); if (compile_scope.cancelled()) return kYield; env.emplace(compile_scope.native_module()->CreateCompilationEnv()); wire_bytes = compile_scope.compilation_state()->GetWireBytesStorage(); module = compile_scope.native_module()->shared_module(); queue = compile_scope.compilation_state()->GetQueueForCompileTask(task_id); unit = compile_scope.compilation_state()->GetNextCompilationUnit(queue, tier); if (!unit) return kNoMoreUnits; } TRACE_COMPILE("ExecuteCompilationUnits (task id %d)\n", task_id); std::vector results_to_publish; while (true) { ExecutionTier current_tier = unit->tier(); const char* event_name = GetCompilationEventName(unit.value(), env.value()); TRACE_EVENT0("v8.wasm", event_name); while (unit->tier() == current_tier) { // Track detected features on a per-function basis before collecting them // into {global_detected_features}. WasmFeatures per_function_detected_features = WasmFeatures::None(); // (asynchronous): Execute the compilation. WasmCompilationResult result = unit->ExecuteCompilation(&env.value(), wire_bytes.get(), counters, &per_function_detected_features); global_detected_features.Add(per_function_detected_features); results_to_publish.emplace_back(std::move(result)); bool yield = delegate && delegate->ShouldYield(); // (synchronized): Publish the compilation result and get the next unit. BackgroundCompileScope compile_scope(native_module); if (compile_scope.cancelled()) return kYield; if (!results_to_publish.back().succeeded()) { compile_scope.compilation_state()->SetError(); return kNoMoreUnits; } if (!unit->for_debugging() && result.result_tier != current_tier) { compile_scope.native_module()->AddLiftoffBailout(); } // Yield or get next unit. if (yield || !(unit = compile_scope.compilation_state()->GetNextCompilationUnit( queue, tier))) { std::vector> unpublished_code = compile_scope.native_module()->AddCompiledCode( base::VectorOf(results_to_publish)); results_to_publish.clear(); compile_scope.compilation_state()->SchedulePublishCompilationResults( std::move(unpublished_code), tier); compile_scope.compilation_state()->OnCompilationStopped( global_detected_features); return yield ? kYield : kNoMoreUnits; } // Publish after finishing a certain amount of units, to avoid contention // when all threads publish at the end. bool batch_full = queue->ShouldPublish(static_cast(results_to_publish.size())); // Also publish each time the compilation tier changes from Liftoff to // TurboFan, such that we immediately publish the baseline compilation // results to start execution, and do not wait for a batch to fill up. bool liftoff_finished = unit->tier() != current_tier && unit->tier() == ExecutionTier::kTurbofan; if (batch_full || liftoff_finished) { std::vector> unpublished_code = compile_scope.native_module()->AddCompiledCode( base::VectorOf(results_to_publish)); results_to_publish.clear(); compile_scope.compilation_state()->SchedulePublishCompilationResults( std::move(unpublished_code), tier); } } } UNREACHABLE(); } // (function is imported, canonical type index) using JSToWasmWrapperKey = std::pair; // Returns the number of units added. int AddExportWrapperUnits(Isolate* isolate, NativeModule* native_module, CompilationUnitBuilder* builder) { std::unordered_set> keys; for (auto exp : native_module->module()->export_table) { if (exp.kind != kExternalFunction) continue; auto& function = native_module->module()->functions[exp.index]; uint32_t canonical_type_index = native_module->module() ->isorecursive_canonical_type_ids[function.sig_index]; int wrapper_index = GetExportWrapperIndex(canonical_type_index, function.imported); if (wrapper_index < isolate->heap()->js_to_wasm_wrappers()->length()) { MaybeObject existing_wrapper = isolate->heap()->js_to_wasm_wrappers()->Get(wrapper_index); if (existing_wrapper.IsStrongOrWeak() && !IsUndefined(existing_wrapper.GetHeapObject())) { // Skip wrapper compilation as the wrapper is already cached. // Note that this does not guarantee that the wrapper is still cached // at the moment at which the WasmInternalFunction is instantiated. continue; } } JSToWasmWrapperKey key(function.imported, canonical_type_index); if (keys.insert(key).second) { auto unit = std::make_shared( isolate, function.sig, canonical_type_index, native_module->module(), function.imported, native_module->enabled_features(), JSToWasmWrapperCompilationUnit::kAllowGeneric); builder->AddJSToWasmWrapperUnit(std::move(unit)); } } return static_cast(keys.size()); } // Returns the number of units added. int AddImportWrapperUnits(NativeModule* native_module, CompilationUnitBuilder* builder) { std::unordered_set keys; int num_imported_functions = native_module->num_imported_functions(); for (int func_index = 0; func_index < num_imported_functions; func_index++) { const WasmFunction& function = native_module->module()->functions[func_index]; if (!IsJSCompatibleSignature(function.sig)) continue; if (UseGenericWasmToJSWrapper(kDefaultImportCallKind, function.sig, kNoSuspend)) { continue; } uint32_t canonical_type_index = native_module->module() ->isorecursive_canonical_type_ids[function.sig_index]; WasmImportWrapperCache::CacheKey key( kDefaultImportCallKind, canonical_type_index, static_cast(function.sig->parameter_count()), kNoSuspend); auto it = keys.insert(key); if (it.second) { // Ensure that all keys exist in the cache, so that we can populate the // cache later without locking. (*native_module->import_wrapper_cache())[key] = nullptr; builder->AddImportUnit(func_index); } } return static_cast(keys.size()); } std::unique_ptr InitializeCompilation( Isolate* isolate, NativeModule* native_module, ProfileInformation* pgo_info) { CompilationStateImpl* compilation_state = Impl(native_module->compilation_state()); auto builder = std::make_unique(native_module); int num_import_wrappers = AddImportWrapperUnits(native_module, builder.get()); int num_export_wrappers = AddExportWrapperUnits(isolate, native_module, builder.get()); compilation_state->InitializeCompilationProgress( num_import_wrappers, num_export_wrappers, pgo_info); return builder; } bool MayCompriseLazyFunctions(const WasmModule* module, WasmFeatures enabled_features) { if (IsLazyModule(module)) return true; if (enabled_features.has_compilation_hints()) return true; #ifdef ENABLE_SLOW_DCHECKS int start = module->num_imported_functions; int end = start + module->num_declared_functions; for (int func_index = start; func_index < end; func_index++) { SLOW_DCHECK(GetCompileStrategy(module, enabled_features, func_index, false) != CompileStrategy::kLazy); } #endif return false; } class CompilationTimeCallback : public CompilationEventCallback { public: enum CompileMode { kSynchronous, kAsync, kStreaming }; explicit CompilationTimeCallback( std::shared_ptr async_counters, std::shared_ptr metrics_recorder, v8::metrics::Recorder::ContextId context_id, std::weak_ptr native_module, CompileMode compile_mode) : start_time_(base::TimeTicks::Now()), async_counters_(std::move(async_counters)), metrics_recorder_(std::move(metrics_recorder)), context_id_(context_id), native_module_(std::move(native_module)), compile_mode_(compile_mode) {} void call(CompilationEvent compilation_event) override { DCHECK(base::TimeTicks::IsHighResolution()); std::shared_ptr native_module = native_module_.lock(); if (!native_module) return; auto now = base::TimeTicks::Now(); auto duration = now - start_time_; if (compilation_event == CompilationEvent::kFinishedBaselineCompilation) { // Reset {start_time_} to measure tier-up time. start_time_ = now; if (compile_mode_ != kSynchronous) { TimedHistogram* histogram = compile_mode_ == kAsync ? async_counters_->wasm_async_compile_wasm_module_time() : async_counters_->wasm_streaming_compile_wasm_module_time(); histogram->AddSample(static_cast(duration.InMicroseconds())); } v8::metrics::WasmModuleCompiled event{ (compile_mode_ != kSynchronous), // async (compile_mode_ == kStreaming), // streamed false, // cached false, // deserialized v8_flags.wasm_lazy_compilation, // lazy true, // success native_module->liftoff_code_size(), // code_size_in_bytes native_module->liftoff_bailout_count(), // liftoff_bailout_count duration.InMicroseconds()}; // wall_clock_duration_in_us metrics_recorder_->DelayMainThreadEvent(event, context_id_); } if (compilation_event == CompilationEvent::kFailedCompilation) { v8::metrics::WasmModuleCompiled event{ (compile_mode_ != kSynchronous), // async (compile_mode_ == kStreaming), // streamed false, // cached false, // deserialized v8_flags.wasm_lazy_compilation, // lazy false, // success native_module->liftoff_code_size(), // code_size_in_bytes native_module->liftoff_bailout_count(), // liftoff_bailout_count duration.InMicroseconds()}; // wall_clock_duration_in_us metrics_recorder_->DelayMainThreadEvent(event, context_id_); } } private: base::TimeTicks start_time_; const std::shared_ptr async_counters_; std::shared_ptr metrics_recorder_; v8::metrics::Recorder::ContextId context_id_; std::weak_ptr native_module_; const CompileMode compile_mode_; }; WasmError ValidateFunctions(const WasmModule* module, base::Vector wire_bytes, WasmFeatures enabled_features, OnlyLazyFunctions only_lazy_functions) { DCHECK_EQ(module->origin, kWasmOrigin); if (only_lazy_functions && !MayCompriseLazyFunctions(module, enabled_features)) { return {}; } std::function filter; // Initially empty for "all functions". if (only_lazy_functions) { const bool is_lazy_module = IsLazyModule(module); filter = [module, enabled_features, is_lazy_module](int func_index) { CompileStrategy strategy = GetCompileStrategy(module, enabled_features, func_index, is_lazy_module); return strategy == CompileStrategy::kLazy || strategy == CompileStrategy::kLazyBaselineEagerTopTier; }; } // Call {ValidateFunctions} in the module decoder. return ValidateFunctions(module, enabled_features, wire_bytes, filter); } WasmError ValidateFunctions(const NativeModule& native_module, OnlyLazyFunctions only_lazy_functions) { return ValidateFunctions(native_module.module(), native_module.wire_bytes(), native_module.enabled_features(), only_lazy_functions); } void CompileNativeModule(Isolate* isolate, v8::metrics::Recorder::ContextId context_id, ErrorThrower* thrower, std::shared_ptr native_module, ProfileInformation* pgo_info) { CHECK(!v8_flags.jitless); const WasmModule* module = native_module->module(); // The callback captures a shared ptr to the semaphore. auto* compilation_state = Impl(native_module->compilation_state()); if (base::TimeTicks::IsHighResolution()) { compilation_state->AddCallback(std::make_unique( isolate->async_counters(), isolate->metrics_recorder(), context_id, native_module, CompilationTimeCallback::kSynchronous)); } // Initialize the compilation units and kick off background compile tasks. std::unique_ptr builder = InitializeCompilation(isolate, native_module.get(), pgo_info); compilation_state->InitializeCompilationUnits(std::move(builder)); // Validate wasm modules for lazy compilation if requested. Never validate // asm.js modules as these are valid by construction (additionally a CHECK // will catch this during lazy compilation). if (!v8_flags.wasm_lazy_validation && module->origin == kWasmOrigin) { DCHECK(!thrower->error()); if (WasmError validation_error = ValidateFunctions(*native_module, kOnlyLazyFunctions)) { thrower->CompileFailed(std::move(validation_error)); return; } } compilation_state->WaitForCompilationEvent( CompilationEvent::kFinishedExportWrappers); if (!compilation_state->failed()) { compilation_state->FinalizeJSToWasmWrappers(isolate, module); compilation_state->WaitForCompilationEvent( CompilationEvent::kFinishedBaselineCompilation); compilation_state->PublishDetectedFeatures(isolate); } if (compilation_state->failed()) { DCHECK_IMPLIES(IsLazyModule(module), !v8_flags.wasm_lazy_validation); WasmError validation_error = ValidateFunctions(*native_module, kAllFunctions); CHECK(validation_error.has_error()); thrower->CompileFailed(std::move(validation_error)); } } class BaseCompileJSToWasmWrapperJob : public JobTask { public: explicit BaseCompileJSToWasmWrapperJob(size_t compilation_units) : outstanding_units_(compilation_units), total_units_(compilation_units) {} size_t GetMaxConcurrency(size_t worker_count) const override { size_t flag_limit = static_cast( std::max(1, v8_flags.wasm_num_compilation_tasks.value())); // {outstanding_units_} includes the units that other workers are currently // working on, so we can safely ignore the {worker_count} and just return // the current number of outstanding units. return std::min(flag_limit, outstanding_units_.load(std::memory_order_relaxed)); } protected: // Returns {true} and places the index of the next unit to process in // {index_out} if there are still units to be processed. Returns {false} // otherwise. bool GetNextUnitIndex(size_t* index_out) { size_t next_index = unit_index_.fetch_add(1, std::memory_order_relaxed); if (next_index >= total_units_) { // {unit_index_} may exceeed {total_units_}, but only by the number of // workers at worst, thus it can't exceed 2 * {total_units_} and overflow // shouldn't happen. DCHECK_GE(2 * total_units_, next_index); return false; } *index_out = next_index; return true; } // Returns true if the last unit was completed. bool CompleteUnit() { size_t outstanding_units = outstanding_units_.fetch_sub(1, std::memory_order_relaxed); DCHECK_GE(outstanding_units, 1); return outstanding_units == 1; } // When external cancellation is detected, call this method to bump // {unit_index_} and reset {outstanding_units_} such that no more tasks are // being scheduled for this job and all tasks exit as soon as possible. void FlushRemainingUnits() { // After being cancelled, make sure to reduce outstanding_units_ to // *basically* zero, but leave the count positive if other workers are still // running, to avoid underflow in {CompleteUnit}. size_t next_undone_unit = unit_index_.exchange(total_units_, std::memory_order_relaxed); size_t undone_units = next_undone_unit >= total_units_ ? 0 : total_units_ - next_undone_unit; // Note that the caller requested one unit that we also still need to remove // from {outstanding_units_}. ++undone_units; size_t previous_outstanding_units = outstanding_units_.fetch_sub(undone_units, std::memory_order_relaxed); CHECK_LE(undone_units, previous_outstanding_units); } private: std::atomic unit_index_{0}; std::atomic outstanding_units_; const size_t total_units_; }; class AsyncCompileJSToWasmWrapperJob final : public BaseCompileJSToWasmWrapperJob { public: explicit AsyncCompileJSToWasmWrapperJob( std::weak_ptr native_module, size_t compilation_units) : BaseCompileJSToWasmWrapperJob(compilation_units), native_module_(std::move(native_module)), engine_barrier_(GetWasmEngine()->GetBarrierForBackgroundCompile()) {} void Run(JobDelegate* delegate) override { auto engine_scope = engine_barrier_->TryLock(); if (!engine_scope) return; std::shared_ptr wrapper_unit = nullptr; OperationsBarrier::Token wrapper_compilation_token; Isolate* isolate; size_t index; if (!GetNextUnitIndex(&index)) return; { BackgroundCompileScope compile_scope(native_module_); if (compile_scope.cancelled()) return FlushRemainingUnits(); wrapper_unit = compile_scope.compilation_state()->GetJSToWasmWrapperCompilationUnit( index); isolate = wrapper_unit->isolate(); wrapper_compilation_token = wasm::GetWasmEngine()->StartWrapperCompilation(isolate); if (!wrapper_compilation_token) return FlushRemainingUnits(); } TRACE_EVENT0("v8.wasm", "wasm.JSToWasmWrapperCompilation"); while (true) { DCHECK_EQ(isolate, wrapper_unit->isolate()); wrapper_unit->Execute(); bool complete_last_unit = CompleteUnit(); bool yield = delegate && delegate->ShouldYield(); if (yield && !complete_last_unit) return; BackgroundCompileScope compile_scope(native_module_); if (compile_scope.cancelled()) return; if (complete_last_unit) { compile_scope.compilation_state()->OnFinishedJSToWasmWrapperUnits(); } if (yield) return; if (!GetNextUnitIndex(&index)) return; wrapper_unit = compile_scope.compilation_state()->GetJSToWasmWrapperCompilationUnit( index); } } private: std::weak_ptr native_module_; std::shared_ptr engine_barrier_; }; class BackgroundCompileJob final : public JobTask { public: explicit BackgroundCompileJob(std::weak_ptr native_module, std::shared_ptr async_counters, CompilationTier tier) : native_module_(std::move(native_module)), engine_barrier_(GetWasmEngine()->GetBarrierForBackgroundCompile()), async_counters_(std::move(async_counters)), tier_(tier) {} void Run(JobDelegate* delegate) override { auto engine_scope = engine_barrier_->TryLock(); if (!engine_scope) return; ExecuteCompilationUnits(native_module_, async_counters_.get(), delegate, tier_); } size_t GetMaxConcurrency(size_t worker_count) const override { BackgroundCompileScope compile_scope(native_module_); if (compile_scope.cancelled()) return 0; size_t flag_limit = static_cast( std::max(1, v8_flags.wasm_num_compilation_tasks.value())); // NumOutstandingCompilations() does not reflect the units that running // workers are processing, thus add the current worker count to that number. return std::min(flag_limit, worker_count + compile_scope.compilation_state() ->NumOutstandingCompilations(tier_)); } private: std::weak_ptr native_module_; std::shared_ptr engine_barrier_; const std::shared_ptr async_counters_; const CompilationTier tier_; }; } // namespace std::shared_ptr CompileToNativeModule( Isolate* isolate, WasmFeatures enabled_features, ErrorThrower* thrower, std::shared_ptr module, ModuleWireBytes wire_bytes, int compilation_id, v8::metrics::Recorder::ContextId context_id, ProfileInformation* pgo_info) { WasmEngine* engine = GetWasmEngine(); base::OwnedVector wire_bytes_copy = base::OwnedVector::Of(wire_bytes.module_bytes()); // Prefer {wire_bytes_copy} to {wire_bytes.module_bytes()} for the temporary // cache key. When we eventually install the module in the cache, the wire // bytes of the temporary key and the new key have the same base pointer and // we can skip the full bytes comparison. std::shared_ptr native_module = engine->MaybeGetNativeModule( module->origin, wire_bytes_copy.as_vector(), isolate); if (native_module) { CompileJsToWasmWrappers(isolate, module.get()); return native_module; } base::Optional wasm_compile_module_time_scope; if (base::TimeTicks::IsHighResolution()) { wasm_compile_module_time_scope.emplace(SELECT_WASM_COUNTER( isolate->counters(), module->origin, wasm_compile, module_time)); } // Embedder usage count for declared shared memories. const bool has_shared_memory = std::any_of(module->memories.begin(), module->memories.end(), [](auto& memory) { return memory.is_shared; }); if (has_shared_memory) { isolate->CountUsage(v8::Isolate::UseCounterFeature::kWasmSharedMemory); } // Create a new {NativeModule} first. const bool include_liftoff = module->origin == kWasmOrigin && v8_flags.liftoff; size_t code_size_estimate = wasm::WasmCodeManager::EstimateNativeModuleCodeSize( module.get(), include_liftoff, DynamicTiering{v8_flags.wasm_dynamic_tiering.value()}); native_module = engine->NewNativeModule(isolate, enabled_features, module, code_size_estimate); native_module->SetWireBytes(std::move(wire_bytes_copy)); native_module->compilation_state()->set_compilation_id(compilation_id); CompileNativeModule(isolate, context_id, thrower, native_module, pgo_info); if (thrower->error()) { engine->UpdateNativeModuleCache(true, std::move(native_module), isolate); return {}; } std::shared_ptr cached_native_module = engine->UpdateNativeModuleCache(false, native_module, isolate); if (cached_native_module != native_module) { // Do not use {module} or {native_module} any more; use // {cached_native_module} instead. module.reset(); native_module.reset(); return cached_native_module; } // Ensure that the code objects are logged before returning. engine->LogOutstandingCodesForIsolate(isolate); return native_module; } AsyncCompileJob::AsyncCompileJob( Isolate* isolate, WasmFeatures enabled_features, base::OwnedVector bytes, Handle context, Handle incumbent_context, const char* api_method_name, std::shared_ptr resolver, int compilation_id) : isolate_(isolate), api_method_name_(api_method_name), enabled_features_(enabled_features), dynamic_tiering_(DynamicTiering{v8_flags.wasm_dynamic_tiering.value()}), start_time_(base::TimeTicks::Now()), bytes_copy_(std::move(bytes)), wire_bytes_(bytes_copy_.as_vector()), resolver_(std::move(resolver)), compilation_id_(compilation_id) { TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("v8.wasm.detailed"), "wasm.AsyncCompileJob"); CHECK(v8_flags.wasm_async_compilation); CHECK(!v8_flags.jitless); v8::Isolate* v8_isolate = reinterpret_cast(isolate); v8::Platform* platform = V8::GetCurrentPlatform(); foreground_task_runner_ = platform->GetForegroundTaskRunner(v8_isolate); native_context_ = isolate->global_handles()->Create(context->native_context()); incumbent_context_ = isolate->global_handles()->Create(*incumbent_context); DCHECK(IsNativeContext(*native_context_)); context_id_ = isolate->GetOrRegisterRecorderContextId(native_context_); metrics_event_.async = true; } void AsyncCompileJob::Start() { DoAsync(isolate_->counters(), isolate_->metrics_recorder()); // -- } void AsyncCompileJob::Abort() { // Removing this job will trigger the destructor, which will cancel all // compilation. GetWasmEngine()->RemoveCompileJob(this); } // {ValidateFunctionsStreamingJobData} holds information that is shared between // the {AsyncStreamingProcessor} and the {ValidateFunctionsStreamingJob}. It // lives in the {AsyncStreamingProcessor} and is updated from both classes. struct ValidateFunctionsStreamingJobData { struct Unit { // {func_index == -1} represents an "invalid" unit. int func_index = -1; base::Vector code; // Check whether the unit is valid. operator bool() const { DCHECK_LE(-1, func_index); return func_index >= 0; } }; void Initialize(int num_declared_functions) { DCHECK_NULL(units); units = base::OwnedVector::NewForOverwrite(num_declared_functions); // Initially {next == end}. next_available_unit.store(units.begin(), std::memory_order_relaxed); end_of_available_units.store(units.begin(), std::memory_order_relaxed); } void AddUnit(int declared_func_index, base::Vector code, JobHandle* job_handle) { DCHECK_NOT_NULL(units); // Write new unit to {*end}, then increment {end}. There is only one thread // adding new units, so no further synchronization needed. Unit* ptr = end_of_available_units.load(std::memory_order_relaxed); // Check invariant: {next <= end}. DCHECK_LE(next_available_unit.load(std::memory_order_relaxed), ptr); *ptr++ = {declared_func_index, code}; // Use release semantics, so whoever loads this pointer (using acquire // semantics) sees all our previous stores. end_of_available_units.store(ptr, std::memory_order_release); size_t total_units_added = ptr - units.begin(); // Periodically notify concurrency increase. This has overhead, so avoid // calling it too often. As long as threads are still running they will // continue processing new units anyway, and if background threads validate // faster than we can add units, then only notifying after increasingly long // delays is the right thing to do to avoid too many small validation tasks. // We notify on each power of two after 16 units, and every 16k units (just // to have *some* upper limit and avoiding to pile up too many units). // Additionally, notify after receiving the last unit of the module. if ((total_units_added >= 16 && base::bits::IsPowerOfTwo(total_units_added)) || (total_units_added % (16 * 1024)) == 0 || ptr == units.end()) { job_handle->NotifyConcurrencyIncrease(); } } size_t NumOutstandingUnits() const { Unit* next = next_available_unit.load(std::memory_order_relaxed); Unit* end = end_of_available_units.load(std::memory_order_relaxed); DCHECK_LE(next, end); return end - next; } // Retrieve one unit to validate; returns an "invalid" unit if nothing is in // the queue. Unit GetUnit() { // Use an acquire load to synchronize with the store in {AddUnit}. All units // before this {end} are fully initialized and ready to execute. Unit* end = end_of_available_units.load(std::memory_order_acquire); Unit* next = next_available_unit.load(std::memory_order_relaxed); while (next < end) { if (next_available_unit.compare_exchange_weak( next, next + 1, std::memory_order_relaxed)) { return *next; } // Otherwise retry with updated {next} pointer. } return {}; } base::OwnedVector units; std::atomic next_available_unit; std::atomic end_of_available_units; std::atomic found_error{false}; }; class ValidateFunctionsStreamingJob final : public JobTask { public: ValidateFunctionsStreamingJob(const WasmModule* module, WasmFeatures enabled_features, ValidateFunctionsStreamingJobData* data) : module_(module), enabled_features_(enabled_features), data_(data) {} void Run(JobDelegate* delegate) override { TRACE_EVENT0("v8.wasm", "wasm.ValidateFunctionsStreaming"); using Unit = ValidateFunctionsStreamingJobData::Unit; while (Unit unit = data_->GetUnit()) { DecodeResult result = ValidateSingleFunction( module_, unit.func_index, unit.code, enabled_features_); if (result.failed()) { data_->found_error.store(true, std::memory_order_relaxed); break; } // After validating one function, check if we should yield. if (delegate->ShouldYield()) break; } } size_t GetMaxConcurrency(size_t worker_count) const override { return worker_count + data_->NumOutstandingUnits(); } private: const WasmModule* const module_; const WasmFeatures enabled_features_; ValidateFunctionsStreamingJobData* data_; }; class AsyncStreamingProcessor final : public StreamingProcessor { public: explicit AsyncStreamingProcessor(AsyncCompileJob* job); bool ProcessModuleHeader(base::Vector bytes) override; bool ProcessSection(SectionCode section_code, base::Vector bytes, uint32_t offset) override; bool ProcessCodeSectionHeader(int num_functions, uint32_t functions_mismatch_error_offset, std::shared_ptr, int code_section_start, int code_section_length) override; bool ProcessFunctionBody(base::Vector bytes, uint32_t offset) override; void OnFinishedChunk() override; void OnFinishedStream(base::OwnedVector bytes, bool after_error) override; void OnAbort() override; bool Deserialize(base::Vector wire_bytes, base::Vector module_bytes) override; private: void CommitCompilationUnits(); ModuleDecoder decoder_; AsyncCompileJob* job_; std::unique_ptr compilation_unit_builder_; int num_functions_ = 0; bool prefix_cache_hit_ = false; bool before_code_section_ = true; ValidateFunctionsStreamingJobData validate_functions_job_data_; std::unique_ptr validate_functions_job_handle_; // Running hash of the wire bytes up to code section size, but excluding the // code section itself. Used by the {NativeModuleCache} to detect potential // duplicate modules. size_t prefix_hash_ = 0; }; std::shared_ptr AsyncCompileJob::CreateStreamingDecoder() { DCHECK_NULL(stream_); stream_ = StreamingDecoder::CreateAsyncStreamingDecoder( std::make_unique(this)); return stream_; } AsyncCompileJob::~AsyncCompileJob() { // Note: This destructor always runs on the foreground thread of the isolate. background_task_manager_.CancelAndWait(); // If initial compilation did not finish yet we can abort it. if (native_module_) { Impl(native_module_->compilation_state()) ->CancelCompilation(CompilationStateImpl::kCancelInitialCompilation); } // Tell the streaming decoder that the AsyncCompileJob is not available // anymore. if (stream_) stream_->NotifyCompilationDiscarded(); CancelPendingForegroundTask(); isolate_->global_handles()->Destroy(native_context_.location()); isolate_->global_handles()->Destroy(incumbent_context_.location()); if (!module_object_.is_null()) { isolate_->global_handles()->Destroy(module_object_.location()); } } void AsyncCompileJob::CreateNativeModule( std::shared_ptr module, size_t code_size_estimate) { // Embedder usage count for declared shared memories. const bool has_shared_memory = std::any_of(module->memories.begin(), module->memories.end(), [](auto& memory) { return memory.is_shared; }); if (has_shared_memory) { isolate_->CountUsage(v8::Isolate::UseCounterFeature::kWasmSharedMemory); } // Create the module object and populate with compiled functions and // information needed at instantiation time. native_module_ = GetWasmEngine()->NewNativeModule( isolate_, enabled_features_, std::move(module), code_size_estimate); native_module_->SetWireBytes(std::move(bytes_copy_)); native_module_->compilation_state()->set_compilation_id(compilation_id_); } bool AsyncCompileJob::GetOrCreateNativeModule( std::shared_ptr module, size_t code_size_estimate) { native_module_ = GetWasmEngine()->MaybeGetNativeModule( module->origin, wire_bytes_.module_bytes(), isolate_); if (native_module_ == nullptr) { CreateNativeModule(std::move(module), code_size_estimate); return false; } return true; } void AsyncCompileJob::PrepareRuntimeObjects() { // Create heap objects for script and module bytes to be stored in the // module object. Asm.js is not compiled asynchronously. DCHECK(module_object_.is_null()); auto source_url = stream_ ? base::VectorOf(stream_->url()) : base::Vector(); auto script = GetWasmEngine()->GetOrCreateScript(isolate_, native_module_, source_url); Handle module_object = WasmModuleObject::New(isolate_, native_module_, script); module_object_ = isolate_->global_handles()->Create(*module_object); } // This function assumes that it is executed in a HandleScope, and that a // context is set on the isolate. void AsyncCompileJob::FinishCompile(bool is_after_cache_hit) { TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("v8.wasm.detailed"), "wasm.FinishAsyncCompile"); if (stream_) { stream_->NotifyNativeModuleCreated(native_module_); } const WasmModule* module = native_module_->module(); auto compilation_state = Impl(native_module_->compilation_state()); // If experimental PGO via files is enabled, load profile information now that // we have all wire bytes and know that the module is valid. if (V8_UNLIKELY(v8_flags.experimental_wasm_pgo_from_file)) { std::unique_ptr pgo_info = LoadProfileFromFile(module, native_module_->wire_bytes()); if (pgo_info) { compilation_state->ApplyPgoInfoLate(pgo_info.get()); } } bool is_after_deserialization = !module_object_.is_null(); if (!is_after_deserialization) { PrepareRuntimeObjects(); } // Measure duration of baseline compilation or deserialization from cache. if (base::TimeTicks::IsHighResolution()) { base::TimeDelta duration = base::TimeTicks::Now() - start_time_; int duration_usecs = static_cast(duration.InMicroseconds()); isolate_->counters()->wasm_streaming_finish_wasm_module_time()->AddSample( duration_usecs); if (is_after_cache_hit || is_after_deserialization) { v8::metrics::WasmModuleCompiled event{ true, // async true, // streamed is_after_cache_hit, // cached is_after_deserialization, // deserialized v8_flags.wasm_lazy_compilation, // lazy !compilation_state->failed(), // success native_module_->turbofan_code_size(), // code_size_in_bytes native_module_->liftoff_bailout_count(), // liftoff_bailout_count duration.InMicroseconds()}; // wall_clock_duration_in_us isolate_->metrics_recorder()->DelayMainThreadEvent(event, context_id_); } } DCHECK(!isolate_->context().is_null()); // Finish the wasm script now and make it public to the debugger. Handle