blob: eda5ba6c23dc8423638598c7b13cd4e5afb7c32c [file] [log] [blame]
// Copyright 2017 The Chromium 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 "third_party/blink/renderer/bindings/core/v8/v8_wasm_response_extensions.h"
#include "base/macros.h"
#include "base/memory/scoped_refptr.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_response.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/fetch/body_stream_buffer.h"
#include "third_party/blink/renderer/core/fetch/fetch_data_loader.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/bindings/v8_per_isolate_data.h"
#include "third_party/blink/renderer/platform/heap/handle.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
#include "third_party/blink/renderer/platform/loader/fetch/cached_metadata.h"
#include "third_party/blink/renderer/platform/loader/fetch/script_cached_metadata_handler.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
namespace blink {
namespace {
// Wasm only has a single metadata type, but we need to tag it.
static const int kWasmModuleTag = 1;
// The |FetchDataLoader| for streaming compilation of WebAssembly code. The
// received bytes get forwarded to the V8 API class |WasmStreaming|.
class FetchDataLoaderForWasmStreaming final : public FetchDataLoader,
public BytesConsumer::Client {
public:
FetchDataLoaderForWasmStreaming(std::shared_ptr<v8::WasmStreaming> streaming,
ScriptState* script_state)
: streaming_(std::move(streaming)), script_state_(script_state) {}
v8::WasmStreaming* streaming() const { return streaming_.get(); }
void Start(BytesConsumer* consumer,
FetchDataLoader::Client* client) override {
DCHECK(!consumer_);
DCHECK(!client_);
client_ = client;
consumer_ = consumer;
consumer_->SetClient(this);
OnStateChange();
}
void OnStateChange() override {
while (true) {
// |buffer| is owned by |consumer_|.
const char* buffer = nullptr;
size_t available = 0;
BytesConsumer::Result result = consumer_->BeginRead(&buffer, &available);
if (result == BytesConsumer::Result::kShouldWait)
return;
if (result == BytesConsumer::Result::kOk) {
if (available > 0) {
DCHECK_NE(buffer, nullptr);
streaming_->OnBytesReceived(reinterpret_cast<const uint8_t*>(buffer),
available);
}
result = consumer_->EndRead(available);
}
switch (result) {
case BytesConsumer::Result::kShouldWait:
NOTREACHED();
return;
case BytesConsumer::Result::kOk: {
break;
}
case BytesConsumer::Result::kDone: {
{
ScriptState::Scope scope(script_state_);
streaming_->Finish();
}
client_->DidFetchDataLoadedCustomFormat();
return;
}
case BytesConsumer::Result::kError: {
return AbortCompilation();
}
}
}
}
String DebugName() const override { return "FetchDataLoaderForWasmModule"; }
void Cancel() override {
consumer_->Cancel();
return AbortCompilation();
}
void Trace(Visitor* visitor) const override {
visitor->Trace(consumer_);
visitor->Trace(client_);
visitor->Trace(script_state_);
FetchDataLoader::Trace(visitor);
BytesConsumer::Client::Trace(visitor);
}
void AbortFromClient() {
auto* exception =
MakeGarbageCollected<DOMException>(DOMExceptionCode::kAbortError);
ScriptState::Scope scope(script_state_);
// Calling ToV8 in a ScriptForbiddenScope will trigger a CHECK and
// cause a crash. ToV8 just invokes a constructor for wrapper creation,
// which is safe (no author script can be run). Adding AllowUserAgentScript
// directly inside createWrapper could cause a perf impact (calling
// isMainThread() every time a wrapper is created is expensive). Ideally,
// resolveOrReject shouldn't be called inside a ScriptForbiddenScope.
{
ScriptForbiddenScope::AllowUserAgentScript allow_script;
v8::Local<v8::Value> v8_exception =
ToV8(exception, script_state_->GetContext()->Global(),
script_state_->GetIsolate());
streaming_->Abort(v8_exception);
}
}
private:
// TODO(ahaas): replace with spec-ed error types, once spec clarifies
// what they are.
void AbortCompilation() {
if (script_state_->ContextIsValid()) {
ScriptState::Scope scope(script_state_);
streaming_->Abort(V8ThrowException::CreateTypeError(
script_state_->GetIsolate(), "Could not download wasm module"));
} else {
// We are not allowed to execute a script, which indicates that we should
// not reject the promise of the streaming compilation. By passing no
// abort reason, we indicate the V8 side that the promise should not get
// rejected.
streaming_->Abort(v8::Local<v8::Value>());
}
}
Member<BytesConsumer> consumer_;
Member<FetchDataLoader::Client> client_;
std::shared_ptr<v8::WasmStreaming> streaming_;
const Member<ScriptState> script_state_;
};
// TODO(mtrofin): WasmDataLoaderClient is necessary so we may provide an
// argument to BodyStreamBuffer::startLoading, however, it fulfills
// a very small role. Consider refactoring to avoid it.
class WasmDataLoaderClient final
: public GarbageCollected<WasmDataLoaderClient>,
public FetchDataLoader::Client {
public:
explicit WasmDataLoaderClient(FetchDataLoaderForWasmStreaming* loader)
: loader_(loader) {}
void DidFetchDataLoadedCustomFormat() override {}
void DidFetchDataLoadFailed() override { NOTREACHED(); }
void Abort() override { loader_->AbortFromClient(); }
void Trace(Visitor* visitor) const override {
visitor->Trace(loader_);
FetchDataLoader::Client::Trace(visitor);
}
private:
Member<FetchDataLoaderForWasmStreaming> loader_;
DISALLOW_COPY_AND_ASSIGN(WasmDataLoaderClient);
};
// ExceptionToAbortStreamingScope converts a possible exception to an abort
// message for WasmStreaming instead of throwing the exception.
//
// All exceptions which happen in the setup of WebAssembly streaming compilation
// have to be passed as an abort message to V8 so that V8 can reject the promise
// associated to the streaming compilation.
class ExceptionToAbortStreamingScope {
STACK_ALLOCATED();
public:
ExceptionToAbortStreamingScope(std::shared_ptr<v8::WasmStreaming> streaming,
ExceptionState& exception_state)
: streaming_(streaming), exception_state_(exception_state) {}
~ExceptionToAbortStreamingScope() {
if (!exception_state_.HadException())
return;
streaming_->Abort(exception_state_.GetException());
exception_state_.ClearException();
}
private:
std::shared_ptr<v8::WasmStreaming> streaming_;
ExceptionState& exception_state_;
DISALLOW_COPY_AND_ASSIGN(ExceptionToAbortStreamingScope);
};
class WasmStreamingClient : public v8::WasmStreaming::Client {
public:
WasmStreamingClient(const String& response_url,
const base::Time& response_time)
: response_url_(response_url.IsolatedCopy()),
response_time_(response_time) {}
void OnModuleCompiled(v8::CompiledWasmModule compiled_module) override {
TRACE_EVENT_INSTANT1(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"),
"v8.wasm.compiledModule", TRACE_EVENT_SCOPE_THREAD,
"url", response_url_.Utf8());
v8::MemorySpan<const uint8_t> wire_bytes =
compiled_module.GetWireBytesRef();
// Our heuristic for whether it's worthwhile to cache is that the module
// was fully compiled and the size is such that loading from the cache will
// improve startup time. Use wire bytes size since it should be correlated
// with module size.
// TODO(bbudge) This is set very low to compare performance of caching with
// baseline compilation. Adjust this test once we know which sizes benefit.
const size_t kWireBytesSizeThresholdBytes = 1UL << 10; // 1 KB.
if (wire_bytes.size() < kWireBytesSizeThresholdBytes)
return;
v8::OwnedBuffer serialized_module = compiled_module.Serialize();
// V8 might not be able to serialize the module.
if (serialized_module.size == 0)
return;
TRACE_EVENT_INSTANT1(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"),
"v8.wasm.cachedModule", TRACE_EVENT_SCOPE_THREAD,
"producedCacheSize", serialized_module.size);
// The resources needed for caching may have been GC'ed, but we should still
// save the compiled module. Use the platform API directly.
scoped_refptr<CachedMetadata> cached_metadata = CachedMetadata::Create(
kWasmModuleTag,
reinterpret_cast<const uint8_t*>(serialized_module.buffer.get()),
serialized_module.size);
base::span<const uint8_t> serialized_data =
cached_metadata->SerializedData();
// Make sure the data could be copied.
if (serialized_data.size() < serialized_module.size)
return;
Platform::Current()->CacheMetadata(
mojom::CodeCacheType::kWebAssembly, KURL(response_url_), response_time_,
serialized_data.data(), serialized_data.size());
}
void SetBuffer(scoped_refptr<CachedMetadata> cached_module) {
cached_module_ = cached_module;
}
private:
String response_url_;
base::Time response_time_;
scoped_refptr<CachedMetadata> cached_module_;
DISALLOW_COPY_AND_ASSIGN(WasmStreamingClient);
};
void StreamFromResponseCallback(
const v8::FunctionCallbackInfo<v8::Value>& args) {
TRACE_EVENT_INSTANT0(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"),
"v8.wasm.streamFromResponseCallback",
TRACE_EVENT_SCOPE_THREAD);
ExceptionState exception_state(args.GetIsolate(),
ExceptionState::kExecutionContext,
"WebAssembly", "compile");
std::shared_ptr<v8::WasmStreaming> streaming =
v8::WasmStreaming::Unpack(args.GetIsolate(), args.Data());
ExceptionToAbortStreamingScope exception_scope(streaming, exception_state);
ScriptState* script_state = ScriptState::ForCurrentRealm(args);
if (!script_state->ContextIsValid()) {
// We do not have an execution context, we just abort streaming compilation
// immediately without error.
streaming->Abort(v8::Local<v8::Value>());
return;
}
Response* response =
V8Response::ToImplWithTypeCheck(args.GetIsolate(), args[0]);
if (!response) {
exception_state.ThrowTypeError(
"An argument must be provided, which must be a "
"Response or Promise<Response> object");
return;
}
if (!response->ok()) {
exception_state.ThrowTypeError("HTTP status code is not ok");
return;
}
// The spec explicitly disallows any extras on the Content-Type header,
// so we check against ContentType() rather than MimeType(), which
// implicitly strips extras.
if (response->ContentType().LowerASCII() != "application/wasm") {
exception_state.ThrowTypeError(
"Incorrect response MIME type. Expected 'application/wasm'.");
return;
}
if (response->IsBodyLocked() || response->IsBodyUsed()) {
exception_state.ThrowTypeError(
"Cannot compile WebAssembly.Module from an already read Response");
return;
}
if (!response->BodyBuffer()) {
// Since the status is 2xx (ok), this must be status 204 (No Content),
// status 205 (Reset Content) or a malformed status 200 (OK).
exception_state.ThrowWasmCompileError("Empty WebAssembly module");
return;
}
String url = response->url();
const std::string& url_utf8 = url.Utf8();
streaming->SetUrl(url_utf8.c_str(), url_utf8.size());
if (auto* cache_handler =
response->BodyBuffer()->GetCachedMetadataHandler()) {
auto client = std::make_shared<WasmStreamingClient>(
url, response->GetResponse()->InternalResponse()->ResponseTime());
streaming->SetClient(client);
scoped_refptr<CachedMetadata> cached_module =
cache_handler->GetCachedMetadata(kWasmModuleTag);
if (cached_module) {
TRACE_EVENT_INSTANT2(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"),
"v8.wasm.moduleCacheHit", TRACE_EVENT_SCOPE_THREAD,
"url", url.Utf8(), "consumedCacheSize",
cached_module->size());
bool is_valid = streaming->SetCompiledModuleBytes(
reinterpret_cast<const uint8_t*>(cached_module->Data()),
cached_module->size());
if (is_valid) {
// Keep the buffer alive until V8 is ready to deserialize it.
// TODO(bbudge) V8 should notify us if deserialization fails, so we
// can release the data and reset the cache.
client->SetBuffer(cached_module);
} else {
TRACE_EVENT_INSTANT0(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"),
"v8.wasm.moduleCacheInvalid",
TRACE_EVENT_SCOPE_THREAD);
cache_handler->ClearCachedMetadata(
CachedMetadataHandler::kClearPersistentStorage);
}
}
}
FetchDataLoaderForWasmStreaming* loader =
MakeGarbageCollected<FetchDataLoaderForWasmStreaming>(streaming,
script_state);
response->BodyBuffer()->StartLoading(
loader, MakeGarbageCollected<WasmDataLoaderClient>(loader),
exception_state);
}
} // namespace
void WasmResponseExtensions::Initialize(v8::Isolate* isolate) {
isolate->SetWasmStreamingCallback(StreamFromResponseCallback);
}
} // namespace blink