summaryrefslogtreecommitdiff
path: root/chromium/third_party/blink/renderer/bindings/core/v8/v8_wasm_response_extensions.cc
blob: 984f82b6aa51e6eecd267df6c33f61018664b81d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
// 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/mojom/loader/code_cache.mojom.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/core/workers/worker_backing_thread.h"
#include "third_party/blink/renderer/core/workers/worker_or_worklet_global_scope.h"
#include "third_party/blink/renderer/core/workers/worker_thread.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/scheduler/public/thread.h"
#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.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);
};

void SendCachedData(String response_url,
                    base::Time response_time,
                    ExecutionContext* execution_context,
                    Vector<uint8_t> serialized_module) {
  if (!execution_context)
    return;
  scoped_refptr<CachedMetadata> cached_metadata =
      CachedMetadata::CreateFromSerializedData(std::move(serialized_module));

  blink::mojom::CodeCacheHost* code_cache_host =
      ExecutionContext::GetCodeCacheHostFromContext(execution_context);
  base::span<const uint8_t> serialized_data = cached_metadata->SerializedData();
  CachedMetadataSender::SendToCodeCacheHost(
      code_cache_host, mojom::blink::CodeCacheType::kWebAssembly, response_url,
      response_time, serialized_data.data(), serialized_data.size());
}

class WasmStreamingClient : public v8::WasmStreaming::Client {
 public:
  WasmStreamingClient(const String& response_url,
                      const base::Time& response_time,
                      scoped_refptr<base::SingleThreadTaskRunner> task_runner,
                      ExecutionContext* execution_context)
      : response_url_(response_url.IsolatedCopy()),
        response_time_(response_time),
        main_thread_task_runner_(std::move(task_runner)),
        execution_context_(execution_context) {}

  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.
    Vector<uint8_t> serialized_data = CachedMetadata::GetSerializedData(
        kWasmModuleTag,
        reinterpret_cast<const uint8_t*>(serialized_module.buffer.get()),
        serialized_module.size);

    // Make sure the data could be copied.
    if (serialized_data.size() < serialized_module.size)
      return;

    // TODO(mythria): Add support for worklets and remove this code that uses
    // per-process interface.
    if (!main_thread_task_runner_.get()) {
      SendCachedData(response_url_, response_time_, execution_context_.Lock(),
                     std::move(serialized_data));
      return;
    }

    main_thread_task_runner_->PostTask(
        FROM_HERE, ConvertToBaseOnceCallback(WTF::CrossThreadBindOnce(
                       &SendCachedData, response_url_, response_time_,
                       execution_context_, std::move(serialized_data))));
  }

  void SetBuffer(scoped_refptr<CachedMetadata> cached_module) {
    cached_module_ = cached_module;
  }

 private:
  String response_url_;
  base::Time response_time_;
  scoped_refptr<CachedMetadata> cached_module_;
  scoped_refptr<base::SingleThreadTaskRunner> main_thread_task_runner_;
  CrossThreadWeakPersistent<ExecutionContext> execution_context_;

  DISALLOW_COPY_AND_ASSIGN(WasmStreamingClient);
};

scoped_refptr<base::SingleThreadTaskRunner> GetContextTaskRunner(
    ExecutionContext& execution_context) {
  if (execution_context.IsWorkerGlobalScope()) {
    WorkerOrWorkletGlobalScope& global_scope =
        To<WorkerOrWorkletGlobalScope>(execution_context);
    return global_scope.GetThread()
        ->GetWorkerBackingThread()
        .BackingThread()
        .GetTaskRunner();
  }

  if (execution_context.IsWindow()) {
    return Thread::MainThread()->GetTaskRunner();
  }

  DCHECK(execution_context.IsWorkletGlobalScope());
  return nullptr;
}

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* execution_context = ExecutionContext::From(script_state);
    DCHECK_NE(execution_context, nullptr);

    auto client = std::make_shared<WasmStreamingClient>(
        url, response->GetResponse()->InternalResponse()->ResponseTime(),
        GetContextTaskRunner(*execution_context), execution_context);
    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);
        // TODO(mythria): Also support using context specific code cache host
        // here. When we pass nullptr for CodeCacheHost we use per-process
        // interface. Currently this code is run on a thread started via a
        // Platform::PostJob. So it isn't safe to use CodeCacheHost interface
        // that was bound on the frame / worker threads. We should instead post
        // a task back to the frame / worker threads with the required data
        // which can then write to generated code caches.
        cache_handler->ClearCachedMetadata(
            /*code_cache_host*/ nullptr,
            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