Commit ab6c4669 authored by Thibaud Michaud's avatar Thibaud Michaud Committed by Commit Bot

Reland "Reland "[wasm] Cache streaming compilation result""

This is a reland of 9781aa07

Original change's description:
> Reland "[wasm] Cache streaming compilation result"
>
> This is a reland of 015f379a
>
> Original change's description:
> > [wasm] Cache streaming compilation result
> >
> > Before compiling the code section, check whether the
> > bytes received so far match a cached module. If they do, delay
> > compilation until we receive the full bytes, since we are likely to find
> > a cache entry for them.
> >
> > R=clemensb@chromium.org
> >
> > Bug: v8:6847
> > Change-Id: Ie5170d1274da3da6d52ff1b408abc7cb441bbe3c
> > Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2002823
> > Commit-Queue: Thibaud Michaud <thibaudm@chromium.org>
> > Reviewed-by: Clemens Backes <clemensb@chromium.org>
> > Cr-Commit-Position: refs/heads/master@{#66000}
>
> Bug: v8:6847
> Change-Id: I0b5acffa01aeb7dade3dc966392814383d900015
> Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2022951
> Commit-Queue: Thibaud Michaud <thibaudm@chromium.org>
> Reviewed-by: Clemens Backes <clemensb@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#66047}

Bug: v8:6847
Change-Id: I272f56eee28010f34cc99df475164581c8b63036
Cq-Include-Trybots: luci.v8.try:v8_linux64_tsan_rel
Cq-Include-Trybots: luci.v8.try:v8_linux64_msan_rel
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2030741
Commit-Queue: Thibaud Michaud <thibaudm@chromium.org>
Reviewed-by: 's avatarClemens Backes <clemensb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#66081}
parent aa376ae0
......@@ -1401,14 +1401,18 @@ std::shared_ptr<NativeModule> CompileToNativeModule(
wasm::WasmCodeManager::EstimateNativeModuleCodeSize(module.get(),
uses_liftoff);
native_module = isolate->wasm_engine()->NewNativeModule(
isolate, enabled, std::move(module), code_size_estimate);
isolate, enabled, module, code_size_estimate);
native_module->SetWireBytes(std::move(wire_bytes_copy));
CompileNativeModule(isolate, thrower, wasm_module, native_module.get());
isolate->wasm_engine()->UpdateNativeModuleCache(native_module,
thrower->error());
bool cache_hit = !isolate->wasm_engine()->UpdateNativeModuleCache(
thrower->error(), &native_module);
if (thrower->error()) return {};
if (cache_hit) {
CompileJsToWasmWrappers(isolate, wasm_module, export_wrappers_out);
return native_module;
}
Impl(native_module->compilation_state())
->FinalizeJSToWasmWrappers(isolate, native_module->module(),
export_wrappers_out);
......@@ -1488,7 +1492,9 @@ void AsyncCompileJob::Abort() {
class AsyncStreamingProcessor final : public StreamingProcessor {
public:
explicit AsyncStreamingProcessor(AsyncCompileJob* job);
explicit AsyncStreamingProcessor(AsyncCompileJob* job,
std::shared_ptr<Counters> counters,
AccountingAllocator* allocator);
bool ProcessModuleHeader(Vector<const uint8_t> bytes,
uint32_t offset) override;
......@@ -1525,12 +1531,20 @@ class AsyncStreamingProcessor final : public StreamingProcessor {
WasmEngine* wasm_engine_;
std::unique_ptr<CompilationUnitBuilder> compilation_unit_builder_;
int num_functions_ = 0;
bool prefix_cache_hit_ = false;
std::shared_ptr<Counters> async_counters_;
AccountingAllocator* allocator_;
// 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_;
};
std::shared_ptr<StreamingDecoder> AsyncCompileJob::CreateStreamingDecoder() {
DCHECK_NULL(stream_);
stream_.reset(
new StreamingDecoder(std::make_unique<AsyncStreamingProcessor>(this)));
stream_.reset(new StreamingDecoder(std::make_unique<AsyncStreamingProcessor>(
this, isolate_->async_counters(), isolate_->allocator())));
return stream_;
}
......@@ -1566,10 +1580,6 @@ void AsyncCompileJob::CreateNativeModule(
// Create the module object and populate with compiled functions and
// information needed at instantiation time.
// TODO(clemensb): For the same module (same bytes / same hash), we should
// only have one {WasmModuleObject}. Otherwise, we might only set
// breakpoints on a (potentially empty) subset of the instances.
// Create the module object.
native_module_ = isolate_->wasm_engine()->NewNativeModule(
isolate_, enabled_features_, std::move(module), code_size_estimate);
......@@ -1578,15 +1588,26 @@ void AsyncCompileJob::CreateNativeModule(
if (stream_) stream_->NotifyNativeModuleCreated(native_module_);
}
bool AsyncCompileJob::GetOrCreateNativeModule(
std::shared_ptr<const WasmModule> module, size_t code_size_estimate) {
native_module_ = isolate_->wasm_engine()->MaybeGetNativeModule(
module->origin, wire_bytes_.module_bytes());
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());
const WasmModule* module = native_module_->module();
auto source_url = stream_ ? stream_->url() : Vector<const char>();
Handle<Script> script =
CreateWasmScript(isolate_, wire_bytes_, VectorOf(module->source_map_url),
module->name, source_url);
Handle<Script> script = CreateWasmScript(
isolate_, native_module_->wire_bytes(), VectorOf(module->source_map_url),
module->name, source_url);
Handle<WasmModuleObject> module_object =
WasmModuleObject::New(isolate_, native_module_, script);
......@@ -1596,9 +1617,11 @@ void AsyncCompileJob::PrepareRuntimeObjects() {
// This function assumes that it is executed in a HandleScope, and that a
// context is set on the isolate.
void AsyncCompileJob::FinishCompile() {
void AsyncCompileJob::FinishCompile(bool is_after_cache_hit) {
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("v8.wasm"),
"AsyncCompileJob::FinishCompile");
// TODO(v8:10165): Ensure that the stream's top tier callback is eventually
// triggered even if the native module comes from the cache.
bool is_after_deserialization = !module_object_.is_null();
if (!is_after_deserialization) {
PrepareRuntimeObjects();
......@@ -1633,8 +1656,14 @@ void AsyncCompileJob::FinishCompile() {
// just compile wrappers here.
if (!is_after_deserialization) {
Handle<FixedArray> export_wrappers;
compilation_state->FinalizeJSToWasmWrappers(
isolate_, module_object_->module(), &export_wrappers);
if (is_after_cache_hit) {
// TODO(thibaudm): Look into sharing wrappers.
CompileJsToWasmWrappers(isolate_, module_object_->module(),
&export_wrappers);
} else {
compilation_state->FinalizeJSToWasmWrappers(
isolate_, module_object_->module(), &export_wrappers);
}
module_object_->set_export_wrappers(*export_wrappers);
}
// We can only update the feature counts once the entire compile is done.
......@@ -1682,12 +1711,16 @@ class AsyncCompileJob::CompilationStateCallback {
case CompilationEvent::kFinishedBaselineCompilation:
DCHECK(!last_event_.has_value());
if (job_->DecrementAndCheckFinisherCount()) {
// TODO(v8:6847): Also share streaming compilation result.
if (job_->stream_ == nullptr) {
job_->isolate_->wasm_engine()->UpdateNativeModuleCache(
job_->native_module_, false);
}
job_->DoSync<CompileFinished>();
// Install the native module in the cache, or reuse a conflicting one.
// If we get a conflicting module, wait until we are back in the
// main thread to update {job_->native_module_} to avoid a data race.
std::shared_ptr<NativeModule> native_module = job_->native_module_;
bool cache_hit =
!job_->isolate_->wasm_engine()->UpdateNativeModuleCache(
false, &native_module);
DCHECK_EQ(cache_hit, native_module != job_->native_module_);
job_->DoSync<CompileFinished>(cache_hit ? std::move(native_module)
: nullptr);
}
break;
case CompilationEvent::kFinishedTopTierCompilation:
......@@ -1698,11 +1731,8 @@ class AsyncCompileJob::CompilationStateCallback {
case CompilationEvent::kFailedCompilation:
DCHECK(!last_event_.has_value());
if (job_->DecrementAndCheckFinisherCount()) {
// TODO(v8:6847): Also share streaming compilation result.
if (job_->stream_ == nullptr) {
job_->isolate_->wasm_engine()->UpdateNativeModuleCache(
job_->native_module_, true);
}
job_->isolate_->wasm_engine()->UpdateNativeModuleCache(
true, &job_->native_module_);
job_->DoSync<CompileFailed>();
}
break;
......@@ -1959,29 +1989,19 @@ class AsyncCompileJob::PrepareAndStartCompile : public CompileStep {
void RunInForeground(AsyncCompileJob* job) override {
TRACE_COMPILE("(2) Prepare and start compile...\n");
// TODO(v8:6847): Also share streaming compilation result.
if (job->stream_ == nullptr) {
auto cached_native_module =
job->isolate_->wasm_engine()->MaybeGetNativeModule(
module_->origin, job->wire_bytes_.module_bytes());
if (cached_native_module != nullptr) {
job->native_module_ = std::move(cached_native_module);
job->PrepareRuntimeObjects();
Handle<FixedArray> export_wrappers;
CompileJsToWasmWrappers(job->isolate_, job->native_module_->module(),
&export_wrappers);
job->module_object_->set_export_wrappers(*export_wrappers);
job->FinishCompile();
return;
}
if (job->stream_ != nullptr) {
// Streaming compilation already checked for cache hits.
job->CreateNativeModule(module_, code_size_estimate_);
} else if (job->GetOrCreateNativeModule(std::move(module_),
code_size_estimate_)) {
job->FinishCompile(true);
return;
}
// Make sure all compilation tasks stopped running. Decoding (async step)
// is done.
job->background_task_manager_.CancelAndWait();
job->CreateNativeModule(module_, code_size_estimate_);
CompilationStateImpl* compilation_state =
Impl(job->native_module_->compilation_state());
compilation_state->AddCallback(CompilationStateCallback{job});
......@@ -2046,20 +2066,30 @@ class SampleTopTierCodeSizeCallback {
// Step 3b (sync): Compilation finished.
//==========================================================================
class AsyncCompileJob::CompileFinished : public CompileStep {
public:
explicit CompileFinished(std::shared_ptr<NativeModule> cached_native_module)
: cached_native_module_(std::move(cached_native_module)) {}
private:
void RunInForeground(AsyncCompileJob* job) override {
TRACE_COMPILE("(3b) Compilation finished\n");
DCHECK(!job->native_module_->compilation_state()->failed());
// Sample the generated code size when baseline compilation finished.
job->native_module_->SampleCodeSize(job->isolate_->counters(),
NativeModule::kAfterBaseline);
// Also, set a callback to sample the code size after top-tier compilation
// finished. This callback will *not* keep the NativeModule alive.
job->native_module_->compilation_state()->AddCallback(
SampleTopTierCodeSizeCallback{job->native_module_});
if (cached_native_module_) {
job->native_module_ = cached_native_module_;
} else {
DCHECK(!job->native_module_->compilation_state()->failed());
// Sample the generated code size when baseline compilation finished.
job->native_module_->SampleCodeSize(job->isolate_->counters(),
NativeModule::kAfterBaseline);
// Also, set a callback to sample the code size after top-tier compilation
// finished. This callback will *not* keep the NativeModule alive.
job->native_module_->compilation_state()->AddCallback(
SampleTopTierCodeSizeCallback{job->native_module_});
}
// Then finalize and publish the generated module.
job->FinishCompile();
job->FinishCompile(cached_native_module_ != nullptr);
}
std::shared_ptr<NativeModule> cached_native_module_;
};
void AsyncCompileJob::FinishModule() {
......@@ -2068,11 +2098,15 @@ void AsyncCompileJob::FinishModule() {
isolate_->wasm_engine()->RemoveCompileJob(this);
}
AsyncStreamingProcessor::AsyncStreamingProcessor(AsyncCompileJob* job)
AsyncStreamingProcessor::AsyncStreamingProcessor(
AsyncCompileJob* job, std::shared_ptr<Counters> async_counters,
AccountingAllocator* allocator)
: decoder_(job->enabled_features_),
job_(job),
wasm_engine_(job_->isolate_->wasm_engine()),
compilation_unit_builder_(nullptr) {}
compilation_unit_builder_(nullptr),
async_counters_(async_counters),
allocator_(allocator) {}
void AsyncStreamingProcessor::FinishAsyncCompileJobWithError(
const WasmError& error) {
......@@ -2080,6 +2114,9 @@ void AsyncStreamingProcessor::FinishAsyncCompileJobWithError(
// Make sure all background tasks stopped executing before we change the state
// of the AsyncCompileJob to DecodeFail.
job_->background_task_manager_.CancelAndWait();
if (!prefix_cache_hit_ && job_->native_module_) {
job_->isolate_->wasm_engine()->StreamingCompilationFailed(prefix_hash_);
}
// Check if there is already a CompiledModule, in which case we have to clean
// up the CompilationStateImpl as well.
......@@ -2109,6 +2146,7 @@ bool AsyncStreamingProcessor::ProcessModuleHeader(Vector<const uint8_t> bytes,
FinishAsyncCompileJobWithError(decoder_.FinishDecoding(false).error());
return false;
}
prefix_hash_ = NativeModuleCache::WireBytesHash(bytes);
return true;
}
......@@ -2122,6 +2160,10 @@ bool AsyncStreamingProcessor::ProcessSection(SectionCode section_code,
// compilation_unit_builder_ anymore.
CommitCompilationUnits();
compilation_unit_builder_.reset();
} else {
// Combine section hashes until code section.
prefix_hash_ = base::hash_combine(prefix_hash_,
NativeModuleCache::WireBytesHash(bytes));
}
if (section_code == SectionCode::kUnknownSectionCode) {
Decoder decoder(bytes, offset);
......@@ -2157,6 +2199,15 @@ bool AsyncStreamingProcessor::ProcessCodeSectionHeader(
FinishAsyncCompileJobWithError(decoder_.FinishDecoding(false).error());
return false;
}
prefix_hash_ = base::hash_combine(prefix_hash_,
static_cast<uint32_t>(code_section_length));
if (!wasm_engine_->GetStreamingCompilationOwnership(prefix_hash_)) {
// Known prefix, wait until the end of the stream and check the cache.
prefix_cache_hit_ = true;
return true;
}
// Execute the PrepareAndStartCompile step immediately and not in a separate
// task.
int num_imported_functions =
......@@ -2202,14 +2253,12 @@ bool AsyncStreamingProcessor::ProcessFunctionBody(Vector<const uint8_t> bytes,
decoder_.DecodeFunctionBody(
num_functions_, static_cast<uint32_t>(bytes.length()), offset, false);
NativeModule* native_module = job_->native_module_.get();
const WasmModule* module = native_module->module();
const WasmModule* module = decoder_.module();
auto enabled_features = job_->enabled_features_;
uint32_t func_index =
num_functions_ + decoder_.module()->num_imported_functions;
DCHECK_EQ(module->origin, kWasmOrigin);
const bool lazy_module = job_->wasm_lazy_compilation_;
CompileStrategy strategy =
GetCompileStrategy(module, enabled_features, func_index, lazy_module);
bool validate_lazily_compiled_function =
......@@ -2217,13 +2266,11 @@ bool AsyncStreamingProcessor::ProcessFunctionBody(Vector<const uint8_t> bytes,
(strategy == CompileStrategy::kLazy ||
strategy == CompileStrategy::kLazyBaselineEagerTopTier);
if (validate_lazily_compiled_function) {
Counters* counters = Impl(native_module->compilation_state())->counters();
AccountingAllocator* allocator = native_module->engine()->allocator();
// The native module does not own the wire bytes until {SetWireBytes} is
// called in {OnFinishedStream}. Validation must use {bytes} parameter.
DecodeResult result = ValidateSingleFunction(
module, func_index, bytes, counters, allocator, enabled_features);
DecodeResult result =
ValidateSingleFunction(module, func_index, bytes, async_counters_.get(),
allocator_, enabled_features);
if (result.failed()) {
FinishAsyncCompileJobWithError(result.error());
......@@ -2231,6 +2278,13 @@ bool AsyncStreamingProcessor::ProcessFunctionBody(Vector<const uint8_t> bytes,
}
}
// Don't compile yet if we might have a cache hit.
if (prefix_cache_hit_) {
num_functions_++;
return true;
}
NativeModule* native_module = job_->native_module_.get();
if (strategy == CompileStrategy::kLazy) {
native_module->UseLazyStub(func_index);
} else if (strategy == CompileStrategy::kLazyBaselineEagerTopTier) {
......@@ -2264,6 +2318,22 @@ void AsyncStreamingProcessor::OnFinishedStream(OwnedVector<uint8_t> bytes) {
FinishAsyncCompileJobWithError(result.error());
return;
}
job_->wire_bytes_ = ModuleWireBytes(bytes.as_vector());
job_->bytes_copy_ = bytes.ReleaseData();
if (prefix_cache_hit_) {
// Restart as an asynchronous, non-streaming compilation. Most likely
// {PrepareAndStartCompile} will get the native module from the cache.
job_->stream_ = nullptr;
size_t code_size_estimate =
wasm::WasmCodeManager::EstimateNativeModuleCodeSize(
result.value().get(), FLAG_liftoff);
job_->DoSync<AsyncCompileJob::PrepareAndStartCompile>(
std::move(result).value(), true, code_size_estimate);
return;
}
// We have to open a HandleScope and prepare the Context for
// CreateNativeModule, PrepareRuntimeObjects and FinishCompile as this is a
// callback from the embedder.
......@@ -2273,23 +2343,33 @@ void AsyncStreamingProcessor::OnFinishedStream(OwnedVector<uint8_t> bytes) {
// Record the size of the wire bytes. In synchronous and asynchronous
// (non-streaming) compilation, this happens in {DecodeWasmModule}.
auto* histogram = job_->isolate_->counters()->wasm_wasm_module_size_bytes();
histogram->AddSample(static_cast<int>(bytes.size()));
histogram->AddSample(job_->wire_bytes_.module_bytes().length());
bool needs_finish = job_->DecrementAndCheckFinisherCount();
if (job_->native_module_ == nullptr) {
const bool has_code_section = job_->native_module_ != nullptr;
bool cache_hit = false;
if (!has_code_section) {
// We are processing a WebAssembly module without code section. Create the
// runtime objects now (would otherwise happen in {PrepareAndStartCompile}).
// native module now (would otherwise happen in {PrepareAndStartCompile} or
// {ProcessCodeSectionHeader}).
constexpr size_t kCodeSizeEstimate = 0;
job_->CreateNativeModule(std::move(result).value(), kCodeSizeEstimate);
DCHECK(needs_finish);
cache_hit = job_->GetOrCreateNativeModule(std::move(result).value(),
kCodeSizeEstimate);
} else {
job_->native_module_->SetWireBytes(
{std::move(job_->bytes_copy_), job_->wire_bytes_.length()});
}
job_->wire_bytes_ = ModuleWireBytes(bytes.as_vector());
job_->native_module_->SetWireBytes(std::move(bytes));
const bool needs_finish = job_->DecrementAndCheckFinisherCount();
DCHECK_IMPLIES(!has_code_section, needs_finish);
if (needs_finish) {
if (job_->native_module_->compilation_state()->failed()) {
const bool failed = job_->native_module_->compilation_state()->failed();
if (!cache_hit) {
cache_hit = !job_->isolate_->wasm_engine()->UpdateNativeModuleCache(
failed, &job_->native_module_);
}
if (failed) {
job_->AsyncCompileFailed();
} else {
job_->FinishCompile();
job_->FinishCompile(cache_hit);
}
}
}
......@@ -2321,7 +2401,7 @@ bool AsyncStreamingProcessor::Deserialize(Vector<const uint8_t> module_bytes,
job_->isolate_->global_handles()->Create(*result.ToHandleChecked());
job_->native_module_ = job_->module_object_->shared_native_module();
job_->wire_bytes_ = ModuleWireBytes(job_->native_module_->wire_bytes());
job_->FinishCompile();
job_->FinishCompile(false);
return true;
}
......@@ -2859,7 +2939,7 @@ WasmCode* CompileImportWrapper(
}
Handle<Script> CreateWasmScript(Isolate* isolate,
const ModuleWireBytes& wire_bytes,
Vector<const uint8_t> wire_bytes,
Vector<const char> source_map_url,
WireBytesRef name,
Vector<const char> source_url) {
......@@ -2870,8 +2950,8 @@ Handle<Script> CreateWasmScript(Isolate* isolate,
script->set_type(Script::TYPE_WASM);
int hash = StringHasher::HashSequentialString(
reinterpret_cast<const char*>(wire_bytes.start()),
static_cast<int>(wire_bytes.length()), kZeroHashSeed);
reinterpret_cast<const char*>(wire_bytes.begin()), wire_bytes.length(),
kZeroHashSeed);
const int kBufferSize = 32;
char buffer[kBufferSize];
......@@ -2890,7 +2970,7 @@ Handle<Script> CreateWasmScript(Isolate* isolate,
.ToHandleChecked();
Handle<String> module_name =
WasmModuleObject::ExtractUtf8StringFromModuleBytes(
isolate, wire_bytes.module_bytes(), name, kNoInternalize);
isolate, wire_bytes, name, kNoInternalize);
name_str = isolate->factory()
->NewConsString(module_name, name_hash)
.ToHandleChecked();
......
......@@ -61,7 +61,7 @@ WasmCode* CompileImportWrapper(
WasmImportWrapperCache::ModificationScope* cache_scope);
V8_EXPORT_PRIVATE Handle<Script> CreateWasmScript(
Isolate* isolate, const ModuleWireBytes& wire_bytes,
Isolate* isolate, Vector<const uint8_t> wire_bytes,
Vector<const char> source_map_url, WireBytesRef name,
Vector<const char> source_url = {});
......@@ -149,9 +149,12 @@ class AsyncCompileJob {
void CreateNativeModule(std::shared_ptr<const WasmModule> module,
size_t code_size_estimate);
// Return true for cache hit, false for cache miss.
bool GetOrCreateNativeModule(std::shared_ptr<const WasmModule> module,
size_t code_size_estimate);
void PrepareRuntimeObjects();
void FinishCompile();
void FinishCompile(bool is_after_cache_hit);
void DecodeFailed(const WasmError&);
void AsyncCompileFailed();
......
......@@ -4,6 +4,7 @@
#include "src/wasm/wasm-engine.h"
#include "src/base/functional.h"
#include "src/base/platform/time.h"
#include "src/diagnostics/code-tracer.h"
#include "src/diagnostics/compilation-statistics.h"
......@@ -130,18 +131,27 @@ std::shared_ptr<NativeModule> NativeModuleCache::MaybeGetNativeModule(
ModuleOrigin origin, Vector<const uint8_t> wire_bytes) {
if (origin != kWasmOrigin) return nullptr;
base::MutexGuard lock(&mutex_);
size_t prefix_hash = PrefixHash(wire_bytes);
NativeModuleCache::Key key{prefix_hash, wire_bytes};
while (true) {
auto it = map_.find(wire_bytes);
auto it = map_.find(key);
if (it == map_.end()) {
// Even though this exact key is not in the cache, there might be a
// matching prefix hash indicating that a streaming compilation is
// currently compiling a module with the same prefix. {OnFinishedStream}
// happens on the main thread too, so waiting for streaming compilation to
// finish would create a deadlock. Instead, compile the module twice and
// handle the conflict in {UpdateNativeModuleCache}.
// Insert a {nullopt} entry to let other threads know that this
// {NativeModule} is already being created on another thread.
map_.emplace(wire_bytes, base::nullopt);
auto p = map_.emplace(key, base::nullopt);
USE(p);
DCHECK(p.second);
return nullptr;
}
auto maybe_native_module = it->second;
if (maybe_native_module.has_value()) {
auto weak_ptr = maybe_native_module.value();
if (auto shared_native_module = weak_ptr.lock()) {
if (it->second.has_value()) {
if (auto shared_native_module = it->second.value().lock()) {
return shared_native_module;
}
}
......@@ -149,28 +159,62 @@ std::shared_ptr<NativeModule> NativeModuleCache::MaybeGetNativeModule(
}
}
void NativeModuleCache::Update(std::shared_ptr<NativeModule> native_module,
bool error) {
bool NativeModuleCache::GetStreamingCompilationOwnership(size_t prefix_hash) {
base::MutexGuard lock(&mutex_);
auto it = map_.lower_bound(Key{prefix_hash, {}});
if (it != map_.end() && it->first.prefix_hash == prefix_hash) {
return false;
}
Key key{prefix_hash, {}};
return map_.emplace(key, base::nullopt).second;
}
void NativeModuleCache::StreamingCompilationFailed(size_t prefix_hash) {
base::MutexGuard lock(&mutex_);
map_.erase({prefix_hash, {}});
cache_cv_.NotifyAll();
}
std::shared_ptr<NativeModule> NativeModuleCache::Update(
std::shared_ptr<NativeModule> native_module, bool error) {
DCHECK_NOT_NULL(native_module);
if (native_module->module()->origin != kWasmOrigin) return;
if (native_module->module()->origin != kWasmOrigin) return native_module;
Vector<const uint8_t> wire_bytes = native_module->wire_bytes();
size_t prefix_hash = PrefixHash(native_module->wire_bytes());
base::MutexGuard lock(&mutex_);
auto it = map_.find(wire_bytes);
DCHECK_NE(it, map_.end());
DCHECK(!it->second.has_value());
// The lifetime of the temporary entry's bytes is unknown. Use the new native
// module's owned copy of the bytes for the key instead.
map_.erase(it);
map_.erase(Key{prefix_hash, {}});
const Key key{prefix_hash, wire_bytes};
auto it = map_.find(key);
if (it != map_.end()) {
if (it->second.has_value()) {
auto conflicting_module = it->second.value().lock();
if (conflicting_module != nullptr) {
return conflicting_module;
}
}
map_.erase(it);
}
if (!error) {
map_.emplace(wire_bytes, base::Optional<std::weak_ptr<NativeModule>>(
std::move(native_module)));
DCHECK_LT(0, native_module->wire_bytes().length());
// The key now points to the new native module's owned copy of the bytes,
// so that it stays valid until the native module is freed and erased from
// the map.
auto p = map_.emplace(
key, base::Optional<std::weak_ptr<NativeModule>>(native_module));
USE(p);
DCHECK(p.second);
}
cache_cv_.NotifyAll();
return native_module;
}
void NativeModuleCache::Erase(NativeModule* native_module) {
if (native_module->module()->origin != kWasmOrigin) return;
// Happens in some tests where bytes are set directly.
if (native_module->wire_bytes().length() == 0) return;
base::MutexGuard lock(&mutex_);
auto cache_it = map_.find(native_module->wire_bytes());
size_t prefix_hash = PrefixHash(native_module->wire_bytes());
auto cache_it = map_.find(Key{prefix_hash, native_module->wire_bytes()});
// Not all native modules are stored in the cache currently. In particular
// streaming compilation and asmjs compilation results are not. So make
// sure that we only delete existing and expired entries.
......@@ -183,13 +227,41 @@ void NativeModuleCache::Erase(NativeModule* native_module) {
}
}
size_t NativeModuleCache::WireBytesHasher::operator()(
const Vector<const uint8_t>& bytes) const {
// static
size_t NativeModuleCache::WireBytesHash(Vector<const uint8_t> bytes) {
return StringHasher::HashSequentialString(
reinterpret_cast<const char*>(bytes.begin()), bytes.length(),
kZeroHashSeed);
}
// static
size_t NativeModuleCache::PrefixHash(Vector<const uint8_t> wire_bytes) {
// Compute the hash as a combined hash of the sections up to the code section
// header, to mirror the way streaming compilation does it.
Decoder decoder(wire_bytes.begin(), wire_bytes.end());
decoder.consume_bytes(8, "module header");
size_t hash = NativeModuleCache::WireBytesHash(wire_bytes.SubVector(0, 8));
SectionCode section_id = SectionCode::kUnknownSectionCode;
while (decoder.ok() && decoder.more()) {
section_id = static_cast<SectionCode>(decoder.consume_u8());
uint32_t section_size = decoder.consume_u32v("section size");
if (section_id == SectionCode::kCodeSectionCode) {
hash = base::hash_combine(hash, section_size);
break;
}
const uint8_t* payload_start = decoder.pc();
// TODO(v8:10126): Remove this check, bytes have been validated already.
if (decoder.position() + section_size >= wire_bytes.size()) {
return hash;
}
decoder.consume_bytes(section_size, "section payload");
size_t section_hash = NativeModuleCache::WireBytesHash(
Vector<const uint8_t>(payload_start, section_size));
hash = base::hash_combine(hash, section_hash);
}
return hash;
}
struct WasmEngine::CurrentGCInfo {
explicit CurrentGCInfo(int8_t gc_sequence_index)
: gc_sequence_index(gc_sequence_index) {
......@@ -362,9 +434,10 @@ MaybeHandle<WasmModuleObject> WasmEngine::SyncCompile(
std::move(result).value(), bytes, &export_wrappers);
if (!native_module) return {};
Handle<Script> script = CreateWasmScript(
isolate, bytes, VectorOf(native_module->module()->source_map_url),
native_module->module()->name);
Handle<Script> script =
CreateWasmScript(isolate, bytes.module_bytes(),
VectorOf(native_module->module()->source_map_url),
native_module->module()->name);
// Create the compiled module object and populate with compiled functions
// and information needed at instantiation time. This object needs to be
......@@ -503,9 +576,10 @@ Handle<WasmModuleObject> WasmEngine::ImportNativeModule(
Isolate* isolate, std::shared_ptr<NativeModule> shared_native_module) {
NativeModule* native_module = shared_native_module.get();
ModuleWireBytes wire_bytes(native_module->wire_bytes());
Handle<Script> script = CreateWasmScript(
isolate, wire_bytes, VectorOf(native_module->module()->source_map_url),
native_module->module()->name);
Handle<Script> script =
CreateWasmScript(isolate, wire_bytes.module_bytes(),
VectorOf(native_module->module()->source_map_url),
native_module->module()->name);
Handle<FixedArray> export_wrappers;
CompileJsToWasmWrappers(isolate, native_module->module(), &export_wrappers);
Handle<WasmModuleObject> module_object = WasmModuleObject::New(
......@@ -756,9 +830,22 @@ std::shared_ptr<NativeModule> WasmEngine::MaybeGetNativeModule(
return native_module_cache_.MaybeGetNativeModule(origin, wire_bytes);
}
void WasmEngine::UpdateNativeModuleCache(
std::shared_ptr<NativeModule> native_module, bool error) {
native_module_cache_.Update(native_module, error);
bool WasmEngine::UpdateNativeModuleCache(
bool error, std::shared_ptr<NativeModule>* native_module) {
// Pass {native_module} by value here to keep it alive until at least after
// we returned from {Update}. Otherwise, we might {Erase} it inside {Update}
// which would lock the mutex twice.
auto prev = native_module->get();
*native_module = native_module_cache_.Update(*native_module, error);
return prev == native_module->get();
}
bool WasmEngine::GetStreamingCompilationOwnership(size_t prefix_hash) {
return native_module_cache_.GetStreamingCompilationOwnership(prefix_hash);
}
void WasmEngine::StreamingCompilationFailed(size_t prefix_hash) {
native_module_cache_.StreamingCompilationFailed(prefix_hash);
}
void WasmEngine::FreeNativeModule(NativeModule* native_module) {
......
......@@ -5,6 +5,8 @@
#ifndef V8_WASM_WASM_ENGINE_H_
#define V8_WASM_WASM_ENGINE_H_
#include <algorithm>
#include <map>
#include <memory>
#include <unordered_map>
#include <unordered_set>
......@@ -51,15 +53,43 @@ class V8_EXPORT_PRIVATE InstantiationResultResolver {
// Native modules cached by their wire bytes.
class NativeModuleCache {
public:
struct WireBytesHasher {
size_t operator()(const Vector<const uint8_t>& bytes) const;
struct Key {
// Store the prefix hash as part of the key for faster lookup, and to
// quickly check existing prefixes for streaming compilation.
size_t prefix_hash;
Vector<const uint8_t> bytes;
bool operator==(const Key& other) const {
bool eq = bytes == other.bytes;
DCHECK_IMPLIES(eq, prefix_hash == other.prefix_hash);
return eq;
}
bool operator<(const Key& other) const {
if (prefix_hash != other.prefix_hash) {
return prefix_hash < other.prefix_hash;
}
return std::lexicographical_compare(
bytes.begin(), bytes.end(), other.bytes.begin(), other.bytes.end());
}
};
std::shared_ptr<NativeModule> MaybeGetNativeModule(
ModuleOrigin origin, Vector<const uint8_t> wire_bytes);
void Update(std::shared_ptr<NativeModule> native_module, bool error);
bool GetStreamingCompilationOwnership(size_t prefix_hash);
void StreamingCompilationFailed(size_t prefix_hash);
std::shared_ptr<NativeModule> Update(
std::shared_ptr<NativeModule> native_module, bool error);
void Erase(NativeModule* native_module);
static size_t WireBytesHash(Vector<const uint8_t> bytes);
// Hash the wire bytes up to the code section header. Used as a heuristic to
// avoid streaming compilation of modules that are likely already in the
// cache. See {GetStreamingCompilationOwnership}. Assumes that the bytes have
// already been validated.
static size_t PrefixHash(Vector<const uint8_t> wire_bytes);
private:
// Each key points to the corresponding native module's wire bytes, so they
// should always be valid as long as the native module is alive. When
......@@ -72,10 +102,7 @@ class NativeModuleCache {
// before trying to get it from the cache.
// By contrast, an expired {weak_ptr} indicates that the native module died
// and will soon be cleaned up from the cache.
std::unordered_map<Vector<const uint8_t>,
base::Optional<std::weak_ptr<NativeModule>>,
WireBytesHasher>
map_;
std::map<Key, base::Optional<std::weak_ptr<NativeModule>>> map_;
base::Mutex mutex_;
......@@ -226,21 +253,40 @@ class V8_EXPORT_PRIVATE WasmEngine {
Isolate* isolate, const WasmFeatures& enabled_features,
std::shared_ptr<const WasmModule> module, size_t code_size_estimate);
// Try getting a cached {NativeModule}. The {wire_bytes}' underlying array
// should be valid at least until the next call to {UpdateNativeModuleCache}.
// Return nullptr if no {NativeModule} exists for these bytes. In this case,
// an empty entry is added to let other threads know that a {NativeModule} for
// these bytes is currently being created. The caller should eventually call
// {UpdateNativeModuleCache} to update the entry and wake up other threads.
// Try getting a cached {NativeModule}, or get ownership for its creation.
// Return {nullptr} if no {NativeModule} exists for these bytes. In this case,
// a {nullopt} entry is added to let other threads know that a {NativeModule}
// for these bytes is currently being created. The caller should eventually
// call {UpdateNativeModuleCache} to update the entry and wake up other
// threads. The {wire_bytes}' underlying array should be valid at least until
// the call to {UpdateNativeModuleCache}.
std::shared_ptr<NativeModule> MaybeGetNativeModule(
ModuleOrigin origin, Vector<const uint8_t> wire_bytes);
// Update the temporary entry inserted by {MaybeGetNativeModule}.
// If {error} is true, the entry is erased. Otherwise the entry is updated to
// match the {native_module} argument. Wake up threads waiting for this native
// Replace the temporary {nullopt} with the new native module, or
// erase it if any error occurred. Wake up blocked threads waiting for this
// module.
void UpdateNativeModuleCache(std::shared_ptr<NativeModule> native_module,
bool error);
// To avoid a deadlock on the main thread between synchronous and streaming
// compilation, two compilation jobs might compile the same native module at
// the same time. In this case the first call to {UpdateNativeModuleCache}
// will insert the native module in the cache, and the last call will discard
// its {native_module} argument and replace it with the existing entry.
// Return true in the former case, and false in the latter.
bool UpdateNativeModuleCache(bool error,
std::shared_ptr<NativeModule>* native_module);
// Register this prefix hash for a streaming compilation job.
// If the hash is not in the cache yet, the function returns true and the
// caller owns the compilation of this module.
// Otherwise another compilation job is currently preparing or has already
// prepared a module with the same prefix hash. The caller should wait until
// the stream is finished and call {MaybeGetNativeModule} to either get the
// module from the cache or get ownership for the compilation of these bytes.
bool GetStreamingCompilationOwnership(size_t prefix_hash);
// Remove the prefix hash from the cache when compilation failed. If
// compilation succeeded, {UpdateNativeModuleCache} should be called instead.
void StreamingCompilationFailed(size_t prefix_hash);
void FreeNativeModule(NativeModule*);
......
......@@ -616,9 +616,9 @@ MaybeHandle<WasmModuleObject> DeserializeNativeModule(
if (decode_result.failed()) return {};
std::shared_ptr<WasmModule> module = std::move(decode_result.value());
CHECK_NOT_NULL(module);
Handle<Script> script =
CreateWasmScript(isolate, wire_bytes, VectorOf(module->source_map_url),
module->name, source_url);
Handle<Script> script = CreateWasmScript(isolate, wire_bytes_vec,
VectorOf(module->source_map_url),
module->name, source_url);
auto shared_native_module =
wasm_engine->MaybeGetNativeModule(module->origin, wire_bytes_vec);
......@@ -637,7 +637,7 @@ MaybeHandle<WasmModuleObject> DeserializeNativeModule(
Reader reader(data + WasmSerializer::kHeaderSize);
bool error = !deserializer.Read(&reader);
wasm_engine->UpdateNativeModuleCache(shared_native_module, error);
wasm_engine->UpdateNativeModuleCache(error, &shared_native_module);
if (error) return {};
}
......
......@@ -455,7 +455,7 @@
'test-api-wasm/WasmStreaming*': [SKIP],
'test-backing-store/Run_WasmModule_Buffer_Externalized_Regression_UseAfterFree': [SKIP],
'test-c-wasm-entry/*': [SKIP],
'test-compilation-cache/TestAsyncCache': [SKIP],
'test-compilation-cache/*': [SKIP],
'test-jump-table-assembler/*': [SKIP],
'test-grow-memory/*': [SKIP],
'test-run-wasm-64/*': [SKIP],
......
......@@ -5,6 +5,7 @@
#include "src/api/api-inl.h"
#include "src/init/v8.h"
#include "src/wasm/streaming-decoder.h"
#include "src/wasm/wasm-code-manager.h"
#include "src/wasm/wasm-engine.h"
#include "src/wasm/wasm-module-builder.h"
......@@ -43,6 +44,35 @@ class TestResolver : public CompilationResultResolver {
std::atomic<int>* pending_;
};
class StreamTester {
public:
explicit StreamTester(std::shared_ptr<TestResolver> test_resolver)
: internal_scope_(CcTest::i_isolate()), test_resolver_(test_resolver) {
i::Isolate* i_isolate = CcTest::i_isolate();
Handle<Context> context = i_isolate->native_context();
stream_ = i_isolate->wasm_engine()->StartStreamingCompilation(
i_isolate, WasmFeatures::All(), context,
"WebAssembly.compileStreaming()", test_resolver_);
}
void OnBytesReceived(const uint8_t* start, size_t length) {
stream_->OnBytesReceived(Vector<const uint8_t>(start, length));
}
void FinishStream() { stream_->Finish(); }
void SetCompiledModuleBytes(const uint8_t* start, size_t length) {
stream_->SetCompiledModuleBytes(Vector<const uint8_t>(start, length));
}
private:
i::HandleScope internal_scope_;
std::shared_ptr<StreamingDecoder> stream_;
std::shared_ptr<TestResolver> test_resolver_;
};
// Create a valid module such that the bytes depend on {n}.
ZoneBuffer GetValidModuleBytes(Zone* zone, int n) {
ZoneBuffer buffer(zone);
......@@ -57,11 +87,51 @@ ZoneBuffer GetValidModuleBytes(Zone* zone, int n) {
return buffer;
}
std::shared_ptr<NativeModule> SyncCompile(Vector<const uint8_t> bytes) {
ErrorThrower thrower(CcTest::i_isolate(), "Test");
auto enabled_features = WasmFeatures::FromIsolate(CcTest::i_isolate());
auto wire_bytes = ModuleWireBytes(bytes.begin(), bytes.end());
Handle<WasmModuleObject> module =
CcTest::i_isolate()
->wasm_engine()
->SyncCompile(CcTest::i_isolate(), enabled_features, &thrower,
wire_bytes)
.ToHandleChecked();
return module->shared_native_module();
}
// Shared prefix.
constexpr uint8_t kPrefix[] = {
WASM_MODULE_HEADER, // module header
kTypeSectionCode, // section code
U32V_1(1 + SIZEOF_SIG_ENTRY_v_v), // section size
U32V_1(1), // type count
SIG_ENTRY_v_v, // signature entry
kFunctionSectionCode, // section code
U32V_1(2), // section size
U32V_1(1), // functions count
0, // signature index
kCodeSectionCode, // section code
U32V_1(7), // section size
U32V_1(1), // functions count
5, // body size
};
constexpr uint8_t kFunctionA[] = {
U32V_1(0), kExprI32Const, U32V_1(0), kExprDrop, kExprEnd,
};
constexpr uint8_t kFunctionB[] = {
U32V_1(0), kExprI32Const, U32V_1(1), kExprDrop, kExprEnd,
};
constexpr size_t kPrefixSize = arraysize(kPrefix);
constexpr size_t kFunctionSize = arraysize(kFunctionA);
} // namespace
TEST(TestAsyncCache) {
CcTest::InitializeVM();
i::HandleScope internal_scope_(CcTest::i_isolate());
i::HandleScope internal_scope(CcTest::i_isolate());
AccountingAllocator allocator;
Zone zone(&allocator, "CompilationCacheTester");
......@@ -95,6 +165,74 @@ TEST(TestAsyncCache) {
CHECK_NE(resolverA1->native_module(), resolverB->native_module());
}
TEST(TestStreamingCache) {
CcTest::InitializeVM();
std::atomic<int> pending(3);
auto resolverA1 = std::make_shared<TestResolver>(&pending);
auto resolverA2 = std::make_shared<TestResolver>(&pending);
auto resolverB = std::make_shared<TestResolver>(&pending);
StreamTester testerA1(resolverA1);
StreamTester testerA2(resolverA2);
StreamTester testerB(resolverB);
// Start receiving kPrefix bytes.
testerA1.OnBytesReceived(kPrefix, kPrefixSize);
testerA2.OnBytesReceived(kPrefix, kPrefixSize);
testerB.OnBytesReceived(kPrefix, kPrefixSize);
// Receive function bytes and start streaming compilation.
testerA1.OnBytesReceived(kFunctionA, kFunctionSize);
testerA1.FinishStream();
testerA2.OnBytesReceived(kFunctionA, kFunctionSize);
testerA2.FinishStream();
testerB.OnBytesReceived(kFunctionB, kFunctionSize);
testerB.FinishStream();
while (pending > 0) {
v8::platform::PumpMessageLoop(i::V8::GetCurrentPlatform(),
CcTest::isolate());
}
std::shared_ptr<NativeModule> native_module_A1 = resolverA1->native_module();
std::shared_ptr<NativeModule> native_module_A2 = resolverA2->native_module();
std::shared_ptr<NativeModule> native_module_B = resolverB->native_module();
CHECK_EQ(native_module_A1, native_module_A2);
CHECK_NE(native_module_A1, native_module_B);
}
TEST(TestStreamingAndSyncCache) {
CcTest::InitializeVM();
std::atomic<int> pending(1);
auto resolver = std::make_shared<TestResolver>(&pending);
StreamTester tester(resolver);
tester.OnBytesReceived(kPrefix, kPrefixSize);
// Compile the same module synchronously to make sure we don't deadlock
// waiting for streaming compilation to finish.
auto full_bytes = OwnedVector<uint8_t>::New(kPrefixSize + kFunctionSize);
memcpy(full_bytes.begin(), kPrefix, kPrefixSize);
memcpy(full_bytes.begin() + kPrefixSize, kFunctionA, kFunctionSize);
auto native_module_sync = SyncCompile(full_bytes.as_vector());
// Streaming compilation should just discard its native module now and use the
// one inserted in the cache by sync compilation.
tester.OnBytesReceived(kFunctionA, kFunctionSize);
tester.FinishStream();
while (pending > 0) {
v8::platform::PumpMessageLoop(i::V8::GetCurrentPlatform(),
CcTest::isolate());
}
std::shared_ptr<NativeModule> native_module_streaming =
resolver->native_module();
CHECK_EQ(native_module_streaming, native_module_sync);
}
} // namespace wasm
} // namespace internal
} // namespace v8
......@@ -69,6 +69,12 @@ TestingModuleBuilder::TestingModuleBuilder(
}
}
TestingModuleBuilder::~TestingModuleBuilder() {
// When the native module dies and is erased from the cache, it is expected to
// have either valid bytes or no bytes at all.
native_module_->SetWireBytes({});
}
byte* TestingModuleBuilder::AddMemory(uint32_t size, SharedFlag shared) {
CHECK(!test_module_->has_memory);
CHECK_NULL(mem_start_);
......
......@@ -89,6 +89,7 @@ class TestingModuleBuilder {
public:
TestingModuleBuilder(Zone*, ManuallyImportedJSFunction*, ExecutionTier,
RuntimeExceptionSupport, LowerSimd);
~TestingModuleBuilder();
void ChangeOriginToAsmjs() { test_module_->origin = kAsmJsSloppyOrigin; }
......
......@@ -157,6 +157,9 @@
# OOM with too many isolates/memory objects (https://crbug.com/1010272)
# Predictable tests fail due to race between postMessage and GrowMemory
'regress/wasm/regress-1010272': [PASS, NO_VARIANTS, ['system == android', SKIP], ['predictable', SKIP]],
# https://crbug.com/v8/10126
'regress/wasm/regress-789952': [SKIP]
}], # ALWAYS
##############################################################################
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment