diff --git a/BUILD.gn b/BUILD.gn index f30d6b9498..57e9ef10ce 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -3348,6 +3348,8 @@ v8_source_set("v8_base_without_compiler") { "src/strings/uri.h", "src/tasks/cancelable-task.cc", "src/tasks/cancelable-task.h", + "src/tasks/operations-barrier.cc", + "src/tasks/operations-barrier.h", "src/tasks/task-utils.cc", "src/tasks/task-utils.h", "src/third_party/siphash/halfsiphash.cc", diff --git a/src/libplatform/default-job.cc b/src/libplatform/default-job.cc index 8a8064c24c..b54764fbdf 100644 --- a/src/libplatform/default-job.cc +++ b/src/libplatform/default-job.cc @@ -123,7 +123,6 @@ void DefaultJobState::CancelAndWait() { } void DefaultJobState::CancelAndDetach() { - base::MutexGuard guard(&mutex_); is_canceled_.store(true, std::memory_order_relaxed); } diff --git a/src/tasks/operations-barrier.cc b/src/tasks/operations-barrier.cc new file mode 100644 index 0000000000..289b4d61fc --- /dev/null +++ b/src/tasks/operations-barrier.cc @@ -0,0 +1,34 @@ +// Copyright 2020 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/tasks/operations-barrier.h" + +namespace v8 { +namespace internal { + +OperationsBarrier::Token OperationsBarrier::TryLock() { + base::MutexGuard guard(&mutex_); + if (cancelled_) return Token(nullptr); + ++operations_count_; + return Token(this); +} + +void OperationsBarrier::CancelAndWait() { + base::MutexGuard guard(&mutex_); + DCHECK(!cancelled_); + cancelled_ = true; + while (operations_count_ > 0) { + release_condition_.Wait(&mutex_); + } +} + +void OperationsBarrier::Release() { + base::MutexGuard guard(&mutex_); + if (--operations_count_ == 0 && cancelled_) { + release_condition_.NotifyOne(); + } +} + +} // namespace internal +} // namespace v8 diff --git a/src/tasks/operations-barrier.h b/src/tasks/operations-barrier.h new file mode 100644 index 0000000000..3237d585a0 --- /dev/null +++ b/src/tasks/operations-barrier.h @@ -0,0 +1,108 @@ +// Copyright 2020 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. + +#ifndef V8_TASKS_OPERATIONS_BARRIER_H_ +#define V8_TASKS_OPERATIONS_BARRIER_H_ + +#include + +#include "src/base/macros.h" +#include "src/base/platform/condition-variable.h" +#include "src/base/platform/mutex.h" + +namespace v8 { +namespace internal { + +// A thread-safe barrier to manage lifetime of muti-threaded operations. +// +// The barrier is used to determine if operations are allowed, and to keep track +// of how many are currently active. Users will call TryLock() before starting +// such operations. If the call succeeds the user can run the operation and the +// barrier will keep track of it until the user signals that the operation is +// completed. No operations are allowed after CancelAndWait() is called. +// +// There is no explicit way of telling the barrier when an operation is +// completed, instead for convenience TryLock() will return a RAII +// like object that will do so on destruction. +// +// For example: +// +// OperationsBarrier barrier_; +// +// void TearDown() { +// barrier_.CancelAndWait(); +// } +// +// void MaybeRunOperation() { +// auto token = barrier_.TryLock(); +// if (token) +// Process(); +// } +// +class V8_EXPORT_PRIVATE OperationsBarrier { + public: + // The owner of a Token which evaluates to true can safely perform an + // operation while being certain it happens-before CancelAndWait(). Releasing + // this Token relinquishes this right. + // + // This class is thread-safe + class Token { + public: + Token() = default; + ~Token() { + if (outer_) outer_->Release(); + } + Token(const Token&) = delete; + Token(Token&& other) V8_NOEXCEPT { + this->outer_ = other.outer_; + other.outer_ = nullptr; + } + + operator bool() const { return !!outer_; } + + private: + friend class OperationsBarrier; + explicit Token(OperationsBarrier* outer) : outer_(outer) {} + OperationsBarrier* outer_ = nullptr; + }; + + OperationsBarrier() = default; + + // Users must call CancelAndWait() before destroying an instance of this + // class. + ~OperationsBarrier() { DCHECK(cancelled_); } + + OperationsBarrier(const OperationsBarrier&) = delete; + OperationsBarrier& operator=(const OperationsBarrier&) = delete; + + // Returns a RAII like object that implicitly converts to true if operations + // are allowed i.e. if this call happens-before CancelAndWait(), otherwise the + // object will convert to false. On successful return, this OperationsBarrier + // will keep track of the operation until the returned object goes out of + // scope. + Token TryLock(); + + // Prevents further calls to TryLock() from succeeding and waits for + // all the ongoing operations to complete. + // + // Attention: Can only be called once. + void CancelAndWait(); + + bool cancelled() const { return cancelled_; } + + private: + void Release(); + + // Mutex and condition variable enabling concurrent register and removing, as + // well as waiting for background tasks on {CancelAndWait}. + base::Mutex mutex_; + base::ConditionVariable release_condition_; + bool cancelled_ = false; + size_t operations_count_{0}; +}; + +} // namespace internal +} // namespace v8 + +#endif // V8_TASKS_OPERATIONS_BARRIER_H_ diff --git a/src/wasm/module-compiler.cc b/src/wasm/module-compiler.cc index e708ae6c5e..407ff1e10f 100644 --- a/src/wasm/module-compiler.cc +++ b/src/wasm/module-compiler.cc @@ -20,6 +20,7 @@ #include "src/logging/counters.h" #include "src/logging/metrics.h" #include "src/objects/property-descriptor.h" +#include "src/tasks/operations-barrier.h" #include "src/tasks/task-utils.h" #include "src/tracing/trace-event.h" #include "src/trap-handler/trap-handler.h" @@ -530,6 +531,13 @@ class CompilationStateImpl { public: CompilationStateImpl(const std::shared_ptr& native_module, std::shared_ptr async_counters); + ~CompilationStateImpl() { + // It is safe to access current_compile_job_ without a lock since this is + // the last reference. + if (current_compile_job_ && current_compile_job_->IsValid()) { + current_compile_job_->CancelAndDetach(); + } + } // Cancel all background compilation, without waiting for compile tasks to // finish. @@ -650,6 +658,8 @@ class CompilationStateImpl { std::weak_ptr const native_module_weak_; const CompileMode compile_mode_; const std::shared_ptr async_counters_; + // Keeps engine alive as long as this is alive. + OperationsBarrier::Token engine_scope_; // Compilation error, atomically updated. This flag can be updated and read // using relaxed semantics. @@ -677,7 +687,7 @@ class CompilationStateImpl { bool has_priority_ = false; - std::shared_ptr current_compile_job_; + std::unique_ptr current_compile_job_; // Features detected to be used in this module. Features can be detected // as a module is being compiled. @@ -1175,7 +1185,7 @@ CompilationExecutionResult ExecuteJSToWasmWrapperCompilationUnits( { BackgroundCompileScope compile_scope(native_module); - if (compile_scope.cancelled()) return kNoMoreUnits; + if (compile_scope.cancelled()) return kYield; wrapper_unit = compile_scope.compilation_state() ->GetNextJSToWasmWrapperCompilationUnit(); if (!wrapper_unit) return kNoMoreUnits; @@ -1187,7 +1197,7 @@ CompilationExecutionResult ExecuteJSToWasmWrapperCompilationUnits( ++num_processed_wrappers; bool yield = delegate && delegate->ShouldYield(); BackgroundCompileScope compile_scope(native_module); - if (compile_scope.cancelled()) return kNoMoreUnits; + if (compile_scope.cancelled()) return kYield; if (yield || !(wrapper_unit = compile_scope.compilation_state() ->GetNextJSToWasmWrapperCompilationUnit())) { @@ -1218,8 +1228,9 @@ const char* GetCompilationEventName(const WasmCompilationUnit& unit, // Run by the {BackgroundCompileJob} (on any thread). CompilationExecutionResult ExecuteCompilationUnits( - std::weak_ptr native_module, Counters* counters, - JobDelegate* delegate, CompileBaselineOnly baseline_only) { + std::weak_ptr native_module, WasmEngine* wasm_engine, + Counters* counters, JobDelegate* delegate, + CompileBaselineOnly baseline_only) { TRACE_EVENT0("v8.wasm", "wasm.ExecuteCompilationUnits"); // Execute JS to Wasm wrapper units first, so that they are ready to be @@ -1235,7 +1246,6 @@ CompilationExecutionResult ExecuteCompilationUnits( base::Optional env; std::shared_ptr wire_bytes; std::shared_ptr module; - WasmEngine* wasm_engine; // Task 0 is any main thread (there might be multiple from multiple isolates), // worker threads start at 1 (thus the "+ 1"). int task_id = delegate ? (int{delegate->GetTaskId()} + 1) : 0; @@ -1249,14 +1259,13 @@ CompilationExecutionResult ExecuteCompilationUnits( // compilation unit. { BackgroundCompileScope compile_scope(native_module); - if (compile_scope.cancelled()) return kNoMoreUnits; - auto* compilation_state = compile_scope.compilation_state(); + if (compile_scope.cancelled()) return kYield; env.emplace(compile_scope.native_module()->CreateCompilationEnv()); - wire_bytes = compilation_state->GetWireBytesStorage(); + wire_bytes = compile_scope.compilation_state()->GetWireBytesStorage(); module = compile_scope.native_module()->shared_module(); - wasm_engine = compile_scope.native_module()->engine(); - queue = compilation_state->GetQueueForCompileTask(task_id); - unit = compilation_state->GetNextCompilationUnit(queue, baseline_only); + queue = compile_scope.compilation_state()->GetQueueForCompileTask(task_id); + unit = compile_scope.compilation_state()->GetNextCompilationUnit( + queue, baseline_only); if (!unit) return kNoMoreUnits; } TRACE_COMPILE("ExecuteCompilationUnits (task id %d)\n", task_id); @@ -1276,7 +1285,7 @@ CompilationExecutionResult ExecuteCompilationUnits( // (synchronized): Publish the compilation result and get the next unit. BackgroundCompileScope compile_scope(native_module); - if (compile_scope.cancelled()) return kNoMoreUnits; + if (compile_scope.cancelled()) return kYield; if (!results_to_publish.back().succeeded()) { compile_scope.compilation_state()->SetError(); @@ -1574,29 +1583,37 @@ void CompileNativeModule(Isolate* isolate, class BackgroundCompileJob final : public JobTask { public: explicit BackgroundCompileJob(std::weak_ptr native_module, + WasmEngine* engine, std::shared_ptr async_counters) : native_module_(std::move(native_module)), + engine_(engine), + engine_barrier_(engine_->GetBarrierForBackgroundCompile()), async_counters_(std::move(async_counters)) {} void Run(JobDelegate* delegate) override { - ExecuteCompilationUnits(native_module_, async_counters_.get(), delegate, - kBaselineOrTopTier); + auto engine_scope = engine_barrier_->TryLock(); + if (!engine_scope) return; + ExecuteCompilationUnits(native_module_, engine_, async_counters_.get(), + delegate, kBaselineOrTopTier); } size_t GetMaxConcurrency(size_t worker_count) const override { - BackgroundCompileScope scope(native_module_); - if (scope.cancelled()) return 0; + BackgroundCompileScope compile_scope(native_module_); + if (compile_scope.cancelled()) return 0; // NumOutstandingCompilations() does not reflect the units that running // workers are processing, thus add the current worker count to that number. size_t flag_limit = static_cast(std::max(1, FLAG_wasm_num_compilation_tasks)); return std::min( flag_limit, - worker_count + scope.compilation_state()->NumOutstandingCompilations()); + worker_count + + compile_scope.compilation_state()->NumOutstandingCompilations()); } private: - const std::weak_ptr native_module_; + std::weak_ptr native_module_; + WasmEngine* engine_; + std::shared_ptr engine_barrier_; const std::shared_ptr async_counters_; }; @@ -2722,14 +2739,20 @@ CompilationStateImpl::CompilationStateImpl( ? CompileMode::kTiering : CompileMode::kRegular), async_counters_(std::move(async_counters)), - compilation_unit_queues_(native_module->num_functions()) {} + engine_scope_(native_module_->engine() + ->GetBarrierForBackgroundCompile() + ->TryLock()), + compilation_unit_queues_(native_module->num_functions()) { + DCHECK(engine_scope_); +} void CompilationStateImpl::CancelCompilation() { - // No more callbacks after abort. - base::MutexGuard callbacks_guard(&callbacks_mutex_); // std::memory_order_relaxed is sufficient because no other state is // synchronized with |compile_cancelled_|. compile_cancelled_.store(true, std::memory_order_relaxed); + + // No more callbacks after abort. + base::MutexGuard callbacks_guard(&callbacks_mutex_); callbacks_.clear(); } @@ -3233,32 +3256,28 @@ void CompilationStateImpl::SchedulePublishCompilationResults( void CompilationStateImpl::ScheduleCompileJobForNewUnits() { if (failed()) return; - std::shared_ptr new_job_handle; { base::MutexGuard guard(&mutex_); - if (current_compile_job_ && current_compile_job_->IsValid()) { - current_compile_job_->NotifyConcurrencyIncrease(); + if (!current_compile_job_ || !current_compile_job_->IsValid()) { + WasmEngine* engine = native_module_->engine(); + std::unique_ptr new_compile_job = + std::make_unique(native_module_weak_, engine, + async_counters_); + // TODO(wasm): Lower priority for TurboFan-only jobs. + current_compile_job_ = V8::GetCurrentPlatform()->PostJob( + has_priority_ ? TaskPriority::kUserBlocking + : TaskPriority::kUserVisible, + std::move(new_compile_job)); + + // Reset the priority. Later uses of the compilation state, e.g. for + // debugging, should compile with the default priority again. + has_priority_ = false; return; } - - std::unique_ptr new_compile_job = - std::make_unique(native_module_weak_, - async_counters_); - // TODO(wasm): Lower priority for TurboFan-only jobs. - new_job_handle = V8::GetCurrentPlatform()->PostJob( - has_priority_ ? TaskPriority::kUserBlocking - : TaskPriority::kUserVisible, - std::move(new_compile_job)); - current_compile_job_ = new_job_handle; - // Reset the priority. Later uses of the compilation state, e.g. for - // debugging, should compile with the default priority again. - has_priority_ = false; - } - - if (new_job_handle) { - native_module_->engine()->ShepherdCompileJobHandle( - std::move(new_job_handle)); } + // Once initialized, |current_compile_job_| is never cleared (except in tests, + // where it's done synchronously). + current_compile_job_->NotifyConcurrencyIncrease(); } size_t CompilationStateImpl::NumOutstandingCompilations() const { @@ -3297,8 +3316,8 @@ void CompilationStateImpl::WaitForCompilationEvent( } constexpr JobDelegate* kNoDelegate = nullptr; - ExecuteCompilationUnits(native_module_weak_, async_counters_.get(), - kNoDelegate, kBaselineOnly); + ExecuteCompilationUnits(native_module_weak_, native_module_->engine(), + async_counters_.get(), kNoDelegate, kBaselineOnly); compilation_event_semaphore->Wait(); } diff --git a/src/wasm/wasm-engine.cc b/src/wasm/wasm-engine.cc index fb18573353..522521489a 100644 --- a/src/wasm/wasm-engine.cc +++ b/src/wasm/wasm-engine.cc @@ -396,32 +396,7 @@ WasmEngine::~WasmEngine() { gdb_server_.reset(); #endif // V8_ENABLE_WASM_GDB_REMOTE_DEBUGGING - // Collect the live modules into a vector first, then cancel them while - // releasing our lock. This will allow the background tasks to finish. - std::vector> live_modules; - { - base::MutexGuard guard(&mutex_); - for (auto& entry : native_modules_) { - if (auto shared_ptr = entry.second->weak_ptr.lock()) { - live_modules.emplace_back(std::move(shared_ptr)); - } - } - } - - for (auto& native_module : live_modules) { - native_module->compilation_state()->CancelCompilation(); - } - live_modules.clear(); - - // Now wait for all background compile tasks to actually finish. - std::vector> compile_job_handles; - { - base::MutexGuard guard(&mutex_); - compile_job_handles = compile_job_handles_; - } - for (auto& job_handle : compile_job_handles) { - if (job_handle->IsValid()) job_handle->Cancel(); - } + operations_barrier_->CancelAndWait(); // All AsyncCompileJobs have been canceled. DCHECK(async_compile_jobs_.empty()); @@ -1335,12 +1310,9 @@ Handle