blob: c1cea1802f0d321dda9bca29f9196c6d450b2495 [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 "config.h"
#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/ScriptResource.h"
#include "core/frame/Settings.h"
#include "platform/SharedBuffer.h"
#include "platform/TraceEvent.h"
#include "public/platform/Platform.h"
#include "wtf/MainThread.h"
#include "wtf/text/TextEncodingRegistry.h"
namespace blink {
// 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()
{
while (!m_data.isEmpty()) {
std::pair<const uint8_t*, size_t> next_data = m_data.takeFirst();
delete[] next_data.first;
}
}
void produce(const uint8_t* data, size_t length)
{
MutexLocker locker(m_mutex);
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)
{
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;
}
WTF::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:
SourceStream(ScriptStreamer* streamer)
: v8::ScriptCompiler::ExternalSourceStream()
, m_streamer(streamer)
, m_cancelled(false)
, m_dataPosition(0) { }
virtual ~SourceStream() { }
// Called by V8 on a background thread. Should block until we can return
// some data.
virtual size_t GetMoreData(const uint8_t** src) override
{
ASSERT(!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;
}
return length;
}
void didFinishLoading()
{
ASSERT(isMainThread());
m_dataQueue.finish();
}
void didReceiveData()
{
ASSERT(isMainThread());
prepareDataOnMainThread();
}
void cancel()
{
ASSERT(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()
{
ASSERT(isMainThread());
// The Resource must still be alive; otherwise we should've cancelled
// the streaming (if we have cancelled, the background thread is not
// waiting).
ASSERT(m_streamer->resource());
if (m_streamer->resource()->cachedMetadata(V8ScriptRunner::tagForCodeCache())) {
// 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.
m_streamer->suppressStreaming();
{
MutexLocker locker(m_mutex);
m_cancelled = true;
}
m_dataQueue.finish();
return;
}
if (!m_resourceBuffer) {
// We don't have a buffer yet. Try to get it from the resource.
SharedBuffer* buffer = m_streamer->resource()->resourceBuffer();
if (!buffer)
return;
m_resourceBuffer = RefPtr<SharedBuffer>(buffer);
}
// Get as much data from the ResourceBuffer as we can.
const char* data = 0;
Vector<const char*> chunks;
Vector<unsigned> chunkLengths;
size_t dataLength = 0;
while (unsigned length = m_resourceBuffer->getSomeData(data, m_dataPosition)) {
// FIXME: Here we can limit based on the total length, if it turns
// out that we don't want to give all the data we have (memory
// vs. speed).
chunks.append(data);
chunkLengths.append(length);
dataLength += length;
m_dataPosition += length;
}
// Copy the data chunks into a new buffer, since we're going to give the
// data to a background thread.
if (dataLength > 0) {
uint8_t* copiedData = new uint8_t[dataLength];
unsigned offset = 0;
for (size_t i = 0; i < chunks.size(); ++i) {
memcpy(copiedData + offset, chunks[i], chunkLengths[i]);
offset += chunkLengths[i];
}
m_dataQueue.produce(copiedData, dataLength);
}
}
ScriptStreamer* m_streamer;
// For coordinating between the main thread and background thread tasks.
// Guarded by m_mutex.
bool m_cancelled;
Mutex m_mutex;
unsigned m_dataPosition; // Only used by the main thread.
RefPtr<SharedBuffer> m_resourceBuffer; // Only used by the main thread.
SourceStreamDataQueue m_dataQueue; // Thread safe.
};
size_t ScriptStreamer::kSmallScriptThreshold = 30 * 1024;
void ScriptStreamer::startStreaming(PendingScript& script, Settings* settings, ScriptState* scriptState, PendingScript::Type scriptType)
{
// 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, settings, scriptState, scriptType);
if (!startedStreaming)
blink::Platform::current()->histogramEnumeration(startedStreamingHistogramName(scriptType), 0, 2);
}
void ScriptStreamer::streamingCompleteOnBackgroundThread()
{
ASSERT(!isMainThread());
MutexLocker locker(m_mutex);
m_parsingFinished = true;
// In the blocking case, the main thread is normally waiting at this
// point, but it can also happen that the load is not yet finished
// (e.g., a parse error). In that case, notifyFinished will be called
// eventually and it will not wait on m_parsingFinishedCondition.
// In the non-blocking case, notifyFinished might already be called, or it
// might be called in the future. In any case, do the cleanup here.
if (m_mainThreadWaitingForParserThread) {
m_parsingFinishedCondition.signal();
} else {
callOnMainThread(WTF::bind(&ScriptStreamer::streamingComplete, this));
}
}
void ScriptStreamer::cancel()
{
ASSERT(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;
m_stream->cancel();
}
void ScriptStreamer::suppressStreaming()
{
MutexLocker locker(m_mutex);
ASSERT(!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)
{
ASSERT(isMainThread());
ASSERT(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.
if (resource->resourceBuffer()->size() < kSmallScriptThreshold) {
return;
}
m_haveEnoughDataForStreaming = true;
const char* histogramName = startedStreamingHistogramName(m_scriptType);
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();
blink::Platform::current()->histogramEnumeration(histogramName, 0, 2);
return;
}
ASSERT(m_task);
// ScriptStreamer needs to stay alive as long as the background task is
// running. This is taken care of with a manual ref() & deref() pair;
// the corresponding deref() is in streamingComplete or in
// notifyFinished.
ref();
ScriptStreamingTask* task = new ScriptStreamingTask(m_task.release(), this);
ScriptStreamerThread::shared()->postTask(task);
blink::Platform::current()->histogramEnumeration(histogramName, 1, 2);
}
m_stream->didReceiveData();
}
void ScriptStreamer::notifyFinished(Resource* resource)
{
ASSERT(isMainThread());
ASSERT(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) {
const char* histogramName = startedStreamingHistogramName(m_scriptType);
blink::Platform::current()->histogramEnumeration(histogramName, 0, 2);
suppressStreaming();
}
m_stream->didFinishLoading();
m_loadingFinished = true;
if (shouldBlockMainThread()) {
// Make the main thead wait until the streaming is complete, to make
// sure that the script gets the main thread's attention as early as
// possible (for possible compiling, if the client wants to do it
// right away). Note that blocking here is not any worse than the
// non-streaming code path where the main thread eventually blocks
// to parse the script.
TRACE_EVENT0("v8", "v8.mainThreadWaitingForParserThread");
MutexLocker locker(m_mutex);
while (!isFinished()) {
ASSERT(!m_parsingFinished);
ASSERT(!m_streamingSuppressed);
m_mainThreadWaitingForParserThread = true;
m_parsingFinishedCondition.wait(m_mutex);
}
}
// Calling notifyFinishedToClient can result into the upper layers dropping
// references to ScriptStreamer. Keep it alive until this function ends.
RefPtr<ScriptStreamer> protect(this);
notifyFinishedToClient();
if (m_mainThreadWaitingForParserThread) {
ASSERT(m_parsingFinished);
ASSERT(!m_streamingSuppressed);
// streamingComplete won't be called, so do the ramp-down work
// here.
deref();
}
}
ScriptStreamer::ScriptStreamer(ScriptResource* resource, v8::ScriptCompiler::StreamedSource::Encoding encoding, PendingScript::Type scriptType, ScriptStreamingMode mode)
: m_resource(resource)
, m_detached(false)
, m_stream(new SourceStream(this))
, m_source(m_stream, encoding) // m_source takes ownership of m_stream.
, m_client(0)
, m_loadingFinished(false)
, m_parsingFinished(false)
, m_haveEnoughDataForStreaming(false)
, m_streamingSuppressed(false)
, m_scriptType(scriptType)
, m_scriptStreamingMode(mode)
, m_mainThreadWaitingForParserThread(false)
{
}
void ScriptStreamer::streamingComplete()
{
// The background task is completed; do the necessary ramp-down in the main
// thread.
ASSERT(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) {
deref();
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();
// The background thread no longer holds an implicit reference.
deref();
}
void ScriptStreamer::notifyFinishedToClient()
{
ASSERT(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.
{
MutexLocker locker(m_mutex);
if (!isFinished())
return;
}
if (m_client)
m_client->notifyFinished(m_resource);
}
const char* ScriptStreamer::startedStreamingHistogramName(PendingScript::Type scriptType)
{
switch (scriptType) {
case PendingScript::ParsingBlocking:
return "WebCore.Scripts.ParsingBlocking.StartedStreaming";
break;
case PendingScript::Deferred:
return "WebCore.Scripts.Deferred.StartedStreaming";
break;
case PendingScript::Async:
return "WebCore.Scripts.Async.StartedStreaming";
break;
default:
ASSERT_NOT_REACHED();
break;
}
return 0;
}
bool ScriptStreamer::startStreamingInternal(PendingScript& script, Settings* settings, ScriptState* scriptState, PendingScript::Type scriptType)
{
ASSERT(isMainThread());
if (!settings || !settings->v8ScriptStreamingEnabled())
return false;
if (settings->v8ScriptStreamingMode() == ScriptStreamingModeOnlyAsyncAndDefer
&& scriptType == PendingScript::ParsingBlocking)
return false;
ScriptResource* resource = script.resource();
ASSERT(!resource->isLoaded());
if (!resource->url().protocolIsInHTTPFamily())
return false;
if (resource->resourceToRevalidate()) {
// 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. In general, the web servers don't seem to send the
// Content-Length HTTP header for scripts.
WTF::TextEncoding textEncoding(resource->encoding());
const char* encodingName = textEncoding.name();
// Here's a list of encodings we can use for streaming. These are
// the canonical names.
v8::ScriptCompiler::StreamedSource::Encoding encoding;
if (strcmp(encodingName, "windows-1252") == 0
|| strcmp(encodingName, "ISO-8859-1") == 0
|| strcmp(encodingName, "US-ASCII") == 0) {
encoding = v8::ScriptCompiler::StreamedSource::ONE_BYTE;
} else if (strcmp(encodingName, "UTF-8") == 0) {
encoding = v8::ScriptCompiler::StreamedSource::UTF8;
} else {
// We don't stream other encodings; especially we don't stream two byte
// scripts to avoid the handling of byte order marks. Most scripts are
// Latin1 or UTF-8 anyway, so this should be enough for most real world
// purposes.
return false;
}
if (!scriptState->contextIsValid())
return false;
ScriptState::Scope scope(scriptState);
// The Resource might go out of scope if the script is no longer needed. We
// will soon call PendingScript::setStreamer, which makes the PendingScript
// notify the ScriptStreamer when it is destroyed.
RefPtr<ScriptStreamer> streamer = adoptRef(new ScriptStreamer(resource, encoding, scriptType, settings->v8ScriptStreamingMode()));
// Decide what kind of cached data we should produce while streaming. By
// default, we generate the parser cache for streamed scripts, to emulate
// the non-streaming behavior (see V8ScriptRunner::compileScript).
v8::ScriptCompiler::CompileOptions compileOption = v8::ScriptCompiler::kProduceParserCache;
if (settings->v8CacheOptions() == V8CacheOptionsCode)
compileOption = v8::ScriptCompiler::kProduceCodeCache;
v8::ScriptCompiler::ScriptStreamingTask* scriptStreamingTask = v8::ScriptCompiler::StartStreamingScript(scriptState->isolate(), &(streamer->m_source), compileOption);
if (scriptStreamingTask) {
streamer->m_task = adoptPtr(scriptStreamingTask);
script.setStreamer(streamer.release());
return true;
}
// Otherwise, V8 cannot stream the script.
return false;
}
} // namespace blink