blob: 36ec360825bcf7fb632120fdff168c7bf2302b8d [file] [log] [blame]
// Copyright 2014 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 "bindings/core/v8/ScriptStreamer.h"
#include "bindings/core/v8/ScriptStreamerThread.h"
#include "bindings/core/v8/V8ScriptRunner.h"
#include "core/dom/Document.h"
#include "core/dom/Element.h"
#include "core/dom/PendingScript.h"
#include "core/fetch/CachedMetadata.h"
#include "core/fetch/ScriptResource.h"
#include "core/frame/Settings.h"
#include "core/html/parser/TextResourceDecoder.h"
#include "platform/CrossThreadFunctional.h"
#include "platform/Histogram.h"
#include "platform/SharedBuffer.h"
#include "platform/TraceEvent.h"
#include "public/platform/WebScheduler.h"
#include "wtf/Deque.h"
#include "wtf/PtrUtil.h"
#include "wtf/text/TextEncodingRegistry.h"
#include <memory>
namespace blink {
namespace {
void recordStartedStreamingHistogram(ScriptStreamer::Type scriptType,
int reason) {
switch (scriptType) {
case ScriptStreamer::ParsingBlocking: {
DEFINE_STATIC_LOCAL(
EnumerationHistogram, parseBlockingHistogram,
("WebCore.Scripts.ParsingBlocking.StartedStreaming", 2));
parseBlockingHistogram.count(reason);
break;
}
case ScriptStreamer::Deferred: {
DEFINE_STATIC_LOCAL(EnumerationHistogram, deferredHistogram,
("WebCore.Scripts.Deferred.StartedStreaming", 2));
deferredHistogram.count(reason);
break;
}
case ScriptStreamer::Async: {
DEFINE_STATIC_LOCAL(EnumerationHistogram, asyncHistogram,
("WebCore.Scripts.Async.StartedStreaming", 2));
asyncHistogram.count(reason);
break;
}
default:
NOTREACHED();
break;
}
}
// For tracking why some scripts are not streamed. Not streaming is part of
// normal operation (e.g., script already loaded, script too small) and doesn't
// necessarily indicate a failure.
enum NotStreamingReason {
AlreadyLoaded,
NotHTTP,
Reload,
ContextNotValid,
EncodingNotSupported,
ThreadBusy,
V8CannotStream,
ScriptTooSmall,
NotStreamingReasonEnd
};
void recordNotStreamingReasonHistogram(ScriptStreamer::Type scriptType,
NotStreamingReason reason) {
switch (scriptType) {
case ScriptStreamer::ParsingBlocking: {
DEFINE_STATIC_LOCAL(EnumerationHistogram, parseBlockingHistogram,
("WebCore.Scripts.ParsingBlocking.NotStreamingReason",
NotStreamingReasonEnd));
parseBlockingHistogram.count(reason);
break;
}
case ScriptStreamer::Deferred: {
DEFINE_STATIC_LOCAL(EnumerationHistogram, deferredHistogram,
("WebCore.Scripts.Deferred.NotStreamingReason",
NotStreamingReasonEnd));
deferredHistogram.count(reason);
break;
}
case ScriptStreamer::Async: {
DEFINE_STATIC_LOCAL(
EnumerationHistogram, asyncHistogram,
("WebCore.Scripts.Async.NotStreamingReason", NotStreamingReasonEnd));
asyncHistogram.count(reason);
break;
}
default:
NOTREACHED();
break;
}
}
} // namespace
// For passing data between the main thread (producer) and the streamer thread
// (consumer). The main thread prepares the data (copies it from Resource) and
// the streamer thread feeds it to V8.
class SourceStreamDataQueue {
WTF_MAKE_NONCOPYABLE(SourceStreamDataQueue);
public:
SourceStreamDataQueue() : m_finished(false) {}
~SourceStreamDataQueue() { discardQueuedData(); }
void clear() {
MutexLocker locker(m_mutex);
m_finished = false;
discardQueuedData();
}
void produce(const uint8_t* data, size_t length) {
MutexLocker locker(m_mutex);
DCHECK(!m_finished);
m_data.append(std::make_pair(data, length));
m_haveData.signal();
}
void finish() {
MutexLocker locker(m_mutex);
m_finished = true;
m_haveData.signal();
}
void consume(const uint8_t** data, size_t* length) {
MutexLocker locker(m_mutex);
while (!tryGetData(data, length))
m_haveData.wait(m_mutex);
}
private:
bool tryGetData(const uint8_t** data, size_t* length) {
ASSERT(m_mutex.locked());
if (!m_data.isEmpty()) {
std::pair<const uint8_t*, size_t> next_data = m_data.takeFirst();
*data = next_data.first;
*length = next_data.second;
return true;
}
if (m_finished) {
*length = 0;
return true;
}
return false;
}
void discardQueuedData() {
while (!m_data.isEmpty()) {
std::pair<const uint8_t*, size_t> next_data = m_data.takeFirst();
delete[] next_data.first;
}
}
Deque<std::pair<const uint8_t*, size_t>> m_data;
bool m_finished;
Mutex m_mutex;
ThreadCondition m_haveData;
};
// SourceStream implements the streaming interface towards V8. The main
// functionality is preparing the data to give to V8 on main thread, and
// actually giving the data (via GetMoreData which is called on a background
// thread).
class SourceStream : public v8::ScriptCompiler::ExternalSourceStream {
WTF_MAKE_NONCOPYABLE(SourceStream);
public:
explicit SourceStream(WebTaskRunner* loadingTaskRunner)
: v8::ScriptCompiler::ExternalSourceStream(),
m_cancelled(false),
m_finished(false),
m_queueLeadPosition(0),
m_queueTailPosition(0),
m_loadingTaskRunner(loadingTaskRunner->clone()) {}
virtual ~SourceStream() override {}
// Called by V8 on a background thread. Should block until we can return
// some data.
size_t GetMoreData(const uint8_t** src) override {
DCHECK(!isMainThread());
{
MutexLocker locker(m_mutex);
if (m_cancelled)
return 0;
}
size_t length = 0;
// This will wait until there is data.
m_dataQueue.consume(src, &length);
{
MutexLocker locker(m_mutex);
if (m_cancelled)
return 0;
}
m_queueLeadPosition += length;
return length;
}
void didFinishLoading() {
DCHECK(isMainThread());
m_finished = true;
m_dataQueue.finish();
}
void didReceiveData(ScriptStreamer* streamer) {
DCHECK(isMainThread());
prepareDataOnMainThread(streamer);
}
void cancel() {
DCHECK(isMainThread());
// The script is no longer needed by the upper layers. Stop streaming
// it. The next time GetMoreData is called (or woken up), it will return
// 0, which will be interpreted as EOS by V8 and the parsing will
// fail. ScriptStreamer::streamingComplete will be called, and at that
// point we will release the references to SourceStream.
{
MutexLocker locker(m_mutex);
m_cancelled = true;
}
m_dataQueue.finish();
}
private:
void prepareDataOnMainThread(ScriptStreamer* streamer) {
DCHECK(isMainThread());
// The Resource must still be alive; otherwise we should've cancelled
// the streaming (if we have cancelled, the background thread is not
// waiting).
DCHECK(streamer->resource());
if (!streamer->resource()->response().cacheStorageCacheName().isNull()) {
streamer->suppressStreaming();
cancel();
return;
}
CachedMetadataHandler* cacheHandler = streamer->resource()->cacheHandler();
RefPtr<CachedMetadata> codeCache(
cacheHandler
? cacheHandler->cachedMetadata(
V8ScriptRunner::tagForCodeCache(cacheHandler))
: nullptr);
if (codeCache.get()) {
// The resource has a code cache, so it's unnecessary to stream and
// parse the code. Cancel the streaming and resume the non-streaming
// code path.
streamer->suppressStreaming();
cancel();
return;
}
if (!m_resourceBuffer) {
// We don't have a buffer yet. Try to get it from the resource.
m_resourceBuffer = streamer->resource()->resourceBuffer();
}
fetchDataFromResourceBuffer();
}
void fetchDataFromResourceBuffer() {
DCHECK(isMainThread());
MutexLocker locker(m_mutex);
DCHECK(!m_finished);
if (m_cancelled) {
m_dataQueue.finish();
return;
}
// Get as much data from the ResourceBuffer as we can.
const char* data = nullptr;
while (size_t length =
m_resourceBuffer->getSomeData(data, m_queueTailPosition)) {
// Copy the data chunks into a new buffer, since we're going to
// give the data to a background thread.
uint8_t* copiedData = new uint8_t[length];
memcpy(copiedData, data, length);
m_dataQueue.produce(copiedData, length);
m_queueTailPosition += length;
}
}
// For coordinating between the main thread and background thread tasks.
// Guards m_cancelled and m_queueTailPosition.
Mutex m_mutex;
// The shared buffer containing the resource data + state variables.
// Used by both threads, guarded by m_mutex.
bool m_cancelled;
bool m_finished;
RefPtr<const SharedBuffer> m_resourceBuffer; // Only used by the main thread.
// The queue contains the data to be passed to the V8 thread.
// queueLeadPosition: data we have handed off to the V8 thread.
// queueTailPosition: end of data we have enqued in the queue.
// bookmarkPosition: position of the bookmark.
SourceStreamDataQueue m_dataQueue; // Thread safe.
size_t m_queueLeadPosition; // Only used by v8 thread.
size_t m_queueTailPosition; // Used by both threads; guarded by m_mutex.
std::unique_ptr<WebTaskRunner> m_loadingTaskRunner;
};
size_t ScriptStreamer::s_smallScriptThreshold = 30 * 1024;
void ScriptStreamer::startStreaming(PendingScript* script,
Type scriptType,
Settings* settings,
ScriptState* scriptState,
WebTaskRunner* loadingTaskRunner) {
// We don't yet know whether the script will really be streamed. E.g.,
// suppressing streaming for short scripts is done later. Record only the
// sure negative cases here.
bool startedStreaming = startStreamingInternal(
script, scriptType, settings, scriptState, loadingTaskRunner);
if (!startedStreaming)
recordStartedStreamingHistogram(scriptType, 0);
}
bool ScriptStreamer::convertEncoding(
const char* encodingName,
v8::ScriptCompiler::StreamedSource::Encoding* encoding) {
// Here's a list of encodings we can use for streaming. These are
// the canonical names.
if (strcmp(encodingName, "windows-1252") == 0 ||
strcmp(encodingName, "ISO-8859-1") == 0 ||
strcmp(encodingName, "US-ASCII") == 0) {
*encoding = v8::ScriptCompiler::StreamedSource::ONE_BYTE;
return true;
}
if (strcmp(encodingName, "UTF-8") == 0) {
*encoding = v8::ScriptCompiler::StreamedSource::UTF8;
return true;
}
// We don't stream other encodings; especially we don't stream two
// byte scripts to avoid the handling of endianness. Most scripts
// are Latin1 or UTF-8 anyway, so this should be enough for most
// real world purposes.
return false;
}
bool ScriptStreamer::isFinished() const {
MutexLocker locker(m_mutex);
return m_loadingFinished && (m_parsingFinished || m_streamingSuppressed);
}
void ScriptStreamer::streamingCompleteOnBackgroundThread() {
DCHECK(!isMainThread());
{
MutexLocker locker(m_mutex);
m_parsingFinished = true;
}
// notifyFinished might already be called, or it might be called in the
// future (if the parsing finishes earlier because of a parse error).
m_loadingTaskRunner->postTask(
BLINK_FROM_HERE, crossThreadBind(&ScriptStreamer::streamingComplete,
wrapCrossThreadPersistent(this)));
// The task might delete ScriptStreamer, so it's not safe to do anything
// after posting it. Note that there's no way to guarantee that this
// function has returned before the task is ran - however, we should not
// access the "this" object after posting the task. (Especially, we should
// not be holding the mutex at this point.)
}
void ScriptStreamer::cancel() {
DCHECK(isMainThread());
// The upper layer doesn't need the script any more, but streaming might
// still be ongoing. Tell SourceStream to try to cancel it whenever it gets
// the control the next time. It can also be that V8 has already completed
// its operations and streamingComplete will be called soon.
m_detached = true;
m_resource = 0;
if (m_stream)
m_stream->cancel();
}
void ScriptStreamer::suppressStreaming() {
MutexLocker locker(m_mutex);
DCHECK(!m_loadingFinished);
// It can be that the parsing task has already finished (e.g., if there was
// a parse error).
m_streamingSuppressed = true;
}
void ScriptStreamer::notifyAppendData(ScriptResource* resource) {
DCHECK(isMainThread());
DCHECK_EQ(m_resource, resource);
{
MutexLocker locker(m_mutex);
if (m_streamingSuppressed)
return;
}
if (!m_haveEnoughDataForStreaming) {
// Even if the first data chunk is small, the script can still be big
// enough - wait until the next data chunk comes before deciding whether
// to start the streaming.
DCHECK(resource->resourceBuffer());
if (resource->resourceBuffer()->size() < s_smallScriptThreshold)
return;
m_haveEnoughDataForStreaming = true;
{
// Check for BOM (byte order marks), because that might change our
// understanding of the data encoding.
constexpr size_t maximumLengthOfBOM = 4;
char maybeBOM[maximumLengthOfBOM] = {};
if (!resource->resourceBuffer()->getPartAsBytes(
maybeBOM, static_cast<size_t>(0), maximumLengthOfBOM)) {
NOTREACHED();
return;
}
std::unique_ptr<TextResourceDecoder> decoder(TextResourceDecoder::create(
"application/javascript", resource->encoding()));
decoder->checkForBOM(maybeBOM, maximumLengthOfBOM);
// The encoding may change when we see the BOM. Check for BOM now
// and update the encoding from the decoder when necessary. Supress
// streaming if the encoding is unsupported.
//
// Also note that have at least s_smallScriptThreshold worth of
// data, which is more than enough for detecting a BOM.
if (!convertEncoding(decoder->encoding().name(), &m_encoding)) {
suppressStreaming();
recordNotStreamingReasonHistogram(m_scriptType, EncodingNotSupported);
recordStartedStreamingHistogram(m_scriptType, 0);
return;
}
}
if (ScriptStreamerThread::shared()->isRunningTask()) {
// At the moment we only have one thread for running the tasks. A
// new task shouldn't be queued before the running task completes,
// because the running task can block and wait for data from the
// network.
suppressStreaming();
recordNotStreamingReasonHistogram(m_scriptType, ThreadBusy);
recordStartedStreamingHistogram(m_scriptType, 0);
return;
}
if (!m_scriptState->contextIsValid()) {
suppressStreaming();
recordNotStreamingReasonHistogram(m_scriptType, ContextNotValid);
recordStartedStreamingHistogram(m_scriptType, 0);
return;
}
DCHECK(!m_stream);
DCHECK(!m_source);
m_stream = new SourceStream(m_loadingTaskRunner.get());
// m_source takes ownership of m_stream.
m_source = wrapUnique(
new v8::ScriptCompiler::StreamedSource(m_stream, m_encoding));
ScriptState::Scope scope(m_scriptState.get());
std::unique_ptr<v8::ScriptCompiler::ScriptStreamingTask>
scriptStreamingTask(wrapUnique(v8::ScriptCompiler::StartStreamingScript(
m_scriptState->isolate(), m_source.get(), m_compileOptions)));
if (!scriptStreamingTask) {
// V8 cannot stream the script.
suppressStreaming();
m_stream = 0;
m_source.reset();
recordNotStreamingReasonHistogram(m_scriptType, V8CannotStream);
recordStartedStreamingHistogram(m_scriptType, 0);
return;
}
ScriptStreamerThread::shared()->postTask(
crossThreadBind(&ScriptStreamerThread::runScriptStreamingTask,
passed(std::move(scriptStreamingTask)),
wrapCrossThreadPersistent(this)));
recordStartedStreamingHistogram(m_scriptType, 1);
}
if (m_stream)
m_stream->didReceiveData(this);
}
void ScriptStreamer::notifyFinished(Resource* resource) {
DCHECK(isMainThread());
DCHECK_EQ(m_resource, resource);
// A special case: empty and small scripts. We didn't receive enough data to
// start the streaming before this notification. In that case, there won't
// be a "parsing complete" notification either, and we should not wait for
// it.
if (!m_haveEnoughDataForStreaming) {
recordNotStreamingReasonHistogram(m_scriptType, ScriptTooSmall);
recordStartedStreamingHistogram(m_scriptType, 0);
suppressStreaming();
}
if (m_stream)
m_stream->didFinishLoading();
m_loadingFinished = true;
notifyFinishedToClient();
}
ScriptStreamer::ScriptStreamer(
PendingScript* script,
Type scriptType,
ScriptState* scriptState,
v8::ScriptCompiler::CompileOptions compileOptions,
WebTaskRunner* loadingTaskRunner)
: m_pendingScript(script),
m_resource(script->resource()),
m_detached(false),
m_stream(0),
m_loadingFinished(false),
m_parsingFinished(false),
m_haveEnoughDataForStreaming(false),
m_streamingSuppressed(false),
m_compileOptions(compileOptions),
m_scriptState(scriptState),
m_scriptType(scriptType),
m_scriptURLString(m_resource->url().copy().getString()),
m_scriptResourceIdentifier(m_resource->identifier()),
m_encoding(
v8::ScriptCompiler::StreamedSource::
TWO_BYTE) // Unfortunately there's no dummy encoding value in the enum; let's use one we don't stream.
,
m_loadingTaskRunner(loadingTaskRunner->clone()) {}
ScriptStreamer::~ScriptStreamer() {}
DEFINE_TRACE(ScriptStreamer) {
visitor->trace(m_pendingScript);
visitor->trace(m_resource);
}
void ScriptStreamer::streamingComplete() {
// The background task is completed; do the necessary ramp-down in the main
// thread.
DCHECK(isMainThread());
// It's possible that the corresponding Resource was deleted before V8
// finished streaming. In that case, the data or the notification is not
// needed. In addition, if the streaming is suppressed, the non-streaming
// code path will resume after the resource has loaded, before the
// background task finishes.
if (m_detached || m_streamingSuppressed)
return;
// We have now streamed the whole script to V8 and it has parsed the
// script. We're ready for the next step: compiling and executing the
// script.
notifyFinishedToClient();
}
void ScriptStreamer::notifyFinishedToClient() {
DCHECK(isMainThread());
// Usually, the loading will be finished first, and V8 will still need some
// time to catch up. But the other way is possible too: if V8 detects a
// parse error, the V8 side can complete before loading has finished. Send
// the notification after both loading and V8 side operations have
// completed. Here we also check that we have a client: it can happen that a
// function calling notifyFinishedToClient was already scheduled in the task
// queue and the upper layer decided that it's not interested in the script
// and called removeClient.
if (!isFinished())
return;
m_pendingScript->streamingFinished();
}
bool ScriptStreamer::startStreamingInternal(PendingScript* script,
Type scriptType,
Settings* settings,
ScriptState* scriptState,
WebTaskRunner* loadingTaskRunner) {
DCHECK(isMainThread());
DCHECK(scriptState->contextIsValid());
ScriptResource* resource = script->resource();
if (resource->isLoaded()) {
recordNotStreamingReasonHistogram(scriptType, AlreadyLoaded);
return false;
}
if (!resource->url().protocolIsInHTTPFamily()) {
recordNotStreamingReasonHistogram(scriptType, NotHTTP);
return false;
}
if (resource->isCacheValidator()) {
recordNotStreamingReasonHistogram(scriptType, Reload);
// This happens e.g., during reloads. We're actually not going to load
// the current Resource of the PendingScript but switch to another
// Resource -> don't stream.
return false;
}
// We cannot filter out short scripts, even if we wait for the HTTP headers
// to arrive: the Content-Length HTTP header is not sent for chunked
// downloads.
// Decide what kind of cached data we should produce while streaming. Only
// produce parser cache if the non-streaming compile takes advantage of it.
v8::ScriptCompiler::CompileOptions compileOption =
v8::ScriptCompiler::kNoCompileOptions;
if (settings->v8CacheOptions() == V8CacheOptionsParse)
compileOption = v8::ScriptCompiler::kProduceParserCache;
// The Resource might go out of scope if the script is no longer
// needed. This makes PendingScript notify the ScriptStreamer when it is
// destroyed.
script->setStreamer(ScriptStreamer::create(script, scriptType, scriptState,
compileOption, loadingTaskRunner));
return true;
}
} // namespace blink